Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/`.
- 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.
Expand Down
27 changes: 27 additions & 0 deletions dakara_server/library/migrations/0013_improve_instrumental.py
Original file line number Diff line number Diff line change
@@ -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),
),
]
8 changes: 7 additions & 1 deletion dakara_server/library/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
12 changes: 10 additions & 2 deletions dakara_server/library/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -430,7 +437,8 @@ class Meta:
"artists",
"works",
"file_path",
"has_instrumental",
"instrumental_file",
"instrumental_track",
)

@staticmethod
Expand Down
15 changes: 13 additions & 2 deletions dakara_server/library/tests/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down
86 changes: 84 additions & 2 deletions dakara_server/library/tests/test_song.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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)

Expand All @@ -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."""
Expand Down
2 changes: 1 addition & 1 deletion dakara_server/playlist/tests/base_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Loading