diff --git a/CHANGELOG.md b/CHANGELOG.md index 06b885e9..69546c33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,12 +34,26 @@ Any important notes regarding the update. ## Unreleased +### Update notes + +The Dakara server now handles instrumental version of songs differently (by differentiating a specific instrumental file from an instrumental track within the media file). +You should apply the migrations and re-feed the database: + +```sh +# using Dakara feeder +dakara-feeder feed songs --force +``` + ### Added - Allow to fetch full lyrics of a song at URL `api/library/songs/lyrics//`. - Allow to search playlist entries, player errors, users, and song tags. - Allow to search songs, playlist entries, player errors and users by ID. +### Changed + +- The `has_instrumental` field of songs was replaced by the `instrumental_file` and `instrumental_track` fields. The `has_instrumental` field remains in the song represention in `api/library/songs/` as a read-only field, while the two new fields are write-only. + ### Fixed - Fixed a bug when creating a player token with a MariaDB database. diff --git a/dakara_server/library/migrations/0013_improve_instrumental.py b/dakara_server/library/migrations/0013_improve_instrumental.py new file mode 100644 index 00000000..d10bc89f --- /dev/null +++ b/dakara_server/library/migrations/0013_improve_instrumental.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.12 on 2026-07-06 23:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("library", "0012_id_fields"), + ] + + operations = [ + migrations.RemoveField( + model_name="song", + name="has_instrumental", + ), + migrations.AddField( + model_name="song", + name="instrumental_file", + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name="song", + name="instrumental_track", + field=models.IntegerField(null=True), + ), + ] diff --git a/dakara_server/library/models.py b/dakara_server/library/models.py index 002b51e9..907b5bdf 100644 --- a/dakara_server/library/models.py +++ b/dakara_server/library/models.py @@ -18,13 +18,19 @@ class Song(models.Model): artists = models.ManyToManyField("Artist") works = models.ManyToManyField("Work", through="SongWorkLink") lyrics = models.TextField(blank=True) - has_instrumental = models.BooleanField(default=False) + instrumental_file = models.CharField(max_length=255, null=True) + instrumental_track = models.IntegerField(null=True) date_created = models.DateTimeField(auto_now_add=True) date_updated = models.DateTimeField(auto_now=True) def __str__(self): return self.title + @property + def has_instrumental(self): + """bool: Check if a song has an instrumental file or an instrumental track.""" + return self.instrumental_file or self.instrumental_track + class Artist(models.Model): """Artist object.""" diff --git a/dakara_server/library/serializers.py b/dakara_server/library/serializers.py index cf76978d..3adc9ea7 100644 --- a/dakara_server/library/serializers.py +++ b/dakara_server/library/serializers.py @@ -335,6 +335,7 @@ class SongSerializer(serializers.ModelSerializer): tags = SongTagForSongSerializer(many=True, required=False) works = SongWorkLinkSerializer(many=True, source="songworklink_set", required=False) lyrics_preview = serializers.SerializerMethodField() + has_instrumental = serializers.ReadOnlyField() class Meta: model = Song @@ -353,10 +354,16 @@ class Meta: "lyrics", "lyrics_preview", "has_instrumental", + "instrumental_file", + "instrumental_track", "date_created", "date_updated", ) - extra_kwargs = {"lyrics": {"write_only": True}} + extra_kwargs = { + "lyrics": {"write_only": True}, + "instrumental_file": {"write_only": True}, + "instrumental_track": {"write_only": True}, + } @staticmethod def get_lyrics_preview(song, max_lines=5): @@ -430,7 +437,8 @@ class Meta: "artists", "works", "file_path", - "has_instrumental", + "instrumental_file", + "instrumental_track", ) @staticmethod diff --git a/dakara_server/library/tests/base_test.py b/dakara_server/library/tests/base_test.py index 728d4024..2a5ee9e6 100644 --- a/dakara_server/library/tests/base_test.py +++ b/dakara_server/library/tests/base_test.py @@ -62,7 +62,8 @@ def create_test_data(self): version="Version2", detail="Detail2", detail_video="Detail_Video2", - has_instrumental=True, + instrumental_file=None, + instrumental_track=1, ) self.song2.save() self.song2.tags.add(self.tag1) @@ -80,7 +81,17 @@ def check_song_json(self, json, expected_song): self.assertEqual(json["version"], expected_song.version) self.assertEqual(json["detail"], expected_song.detail) self.assertEqual(json["detail_video"], expected_song.detail_video) - self.assertEqual(json["has_instrumental"], expected_song.has_instrumental) + + if "has_instrumental" in json: + self.assertEqual( + json["has_instrumental"], + expected_song.instrumental_file or expected_song.instrumental_track, + ) + else: + self.assertEqual(json["instrumental_file"], expected_song.instrumental_file) + self.assertEqual( + json["instrumental_track"], expected_song.instrumental_track + ) # tags expected_tags = expected_song.tags.all() diff --git a/dakara_server/library/tests/test_song.py b/dakara_server/library/tests/test_song.py index 84e4b824..ff16b578 100644 --- a/dakara_server/library/tests/test_song.py +++ b/dakara_server/library/tests/test_song.py @@ -36,6 +36,22 @@ def test_get_song_list(self): self.check_song_json(response.data["results"][0], self.song1) self.check_song_json(response.data["results"][1], self.song2) + def test_get_song_instrumental(self): + """Test to verify song list with one instrumental song.""" + # Login as simple user + self.authenticate(self.user) + + # Pre assert + self.assertIsNotNone(self.song2.instrumental_track) + + # Get songs list + response = self.client.get(self.url) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 2) + + # Song 2 has an instrumental version + self.assertTrue(response.data["results"][1]["has_instrumental"]) + def test_get_song_long_lyrics(self): """Test to get a song with few lyrics.""" # Login as simple user @@ -380,6 +396,70 @@ def test_post_song_simple(self): self.assertEqual(song.version, "version 1") self.assertEqual(song.detail, "test") self.assertEqual(song.detail_video, "here") + self.assertIsNone(song.instrumental_file) + self.assertIsNone(song.instrumental_track) + + def test_post_song_with_instrumental_file(self): + """Test to create a song an instrumental file.""" + # login as manager + self.authenticate(self.manager) + + # pre assert the amount of songs + self.assertEqual(Song.objects.count(), 2) + + # create a new song + song = { + "title": "Song3", + "filename": "song3", + "directory": "directory", + "duration": 0, + "instrumental_file": "song3.ogg", + "instrumental_track": None, + } + response = self.client.post(self.url, song) + + # assert the response + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # assert the created song + song = Song.objects.get(title="Song3") + self.assertIsNotNone(song) + self.assertEqual(song.filename, "song3") + self.assertEqual(song.directory, "directory") + self.assertEqual(song.duration, timedelta(0)) + self.assertEqual(song.instrumental_file, "song3.ogg") + self.assertIsNone(song.instrumental_track) + + def test_post_song_with_instrumental_track(self): + """Test to create a song an instrumental track.""" + # login as manager + self.authenticate(self.manager) + + # pre assert the amount of songs + self.assertEqual(Song.objects.count(), 2) + + # create a new song + song = { + "title": "Song3", + "filename": "song3", + "directory": "directory", + "duration": 0, + "instrumental_file": None, + "instrumental_track": 1, + } + response = self.client.post(self.url, song) + + # assert the response + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # assert the created song + song = Song.objects.get(title="Song3") + self.assertIsNotNone(song) + self.assertEqual(song.filename, "song3") + self.assertEqual(song.directory, "directory") + self.assertEqual(song.duration, timedelta(0)) + self.assertIsNone(song.instrumental_file) + self.assertEqual(song.instrumental_track, 1) def test_post_song_with_tag(self): """Test to create a song with nested tags.""" @@ -677,7 +757,8 @@ def test_put_song_simple(self): "version": "version 1", "detail": "test", "detail_video": "here", - "has_instrumental": True, + "instrumental_file": "song1 new.ogg", + "instrumental_track": None, } response = self.client.put(self.url_song1, song) @@ -694,7 +775,8 @@ def test_put_song_simple(self): self.assertEqual(song.version, "version 1") self.assertEqual(song.detail, "test") self.assertEqual(song.detail_video, "here") - self.assertTrue(song.has_instrumental) + self.assertEqual(song.instrumental_file, "song1 new.ogg") + self.assertIsNone(song.instrumental_track) def test_put_song_embedded(self): """Test to update a song with nested artists, tags and works.""" diff --git a/dakara_server/playlist/tests/base_test.py b/dakara_server/playlist/tests/base_test.py index c740872d..be833f2e 100644 --- a/dakara_server/playlist/tests/base_test.py +++ b/dakara_server/playlist/tests/base_test.py @@ -38,7 +38,7 @@ def create_test_data(self): self.song1.save() self.song1.tags.add(self.tag1) self.song2 = Song( - title="Song2", duration=timedelta(seconds=10), has_instrumental=True + title="Song2", duration=timedelta(seconds=10), instrumental_track=1 ) self.song2.save()