mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
library: fall back to the plural artist names for display
Keep the server's combined legacy artist/albumArtist string when it is present, prefer the OpenSubsonic displayArtist when it is not, and otherwise join the plural artist refs with Navidrome's separator. The normalized value is stored in the display columns, so song rows, album cards, now playing, menu subtitles, downloads, search and artist ordering all get it.
This commit is contained in:
@@ -44,6 +44,7 @@ object SubsonicXml {
|
|||||||
artistId = attrs.attr("artistId").ifEmpty { null },
|
artistId = attrs.attr("artistId").ifEmpty { null },
|
||||||
name = attrs.attr("name"),
|
name = attrs.attr("name"),
|
||||||
albumArtist = attrs.attr("artist").ifEmpty { null },
|
albumArtist = attrs.attr("artist").ifEmpty { null },
|
||||||
|
displayArtist = attrs.attr("displayArtist").ifEmpty { null },
|
||||||
created = attrs.dateAttr("created") ?: 0L,
|
created = attrs.dateAttr("created") ?: 0L,
|
||||||
coverArt = attrs.attr("coverArt").ifEmpty { null },
|
coverArt = attrs.attr("coverArt").ifEmpty { null },
|
||||||
genre = attrs.attr("genre").ifEmpty { null },
|
genre = attrs.attr("genre").ifEmpty { null },
|
||||||
@@ -69,6 +70,7 @@ object SubsonicXml {
|
|||||||
title = attrs.attr("title"),
|
title = attrs.attr("title"),
|
||||||
album = attrs.attr("album").ifEmpty { null },
|
album = attrs.attr("album").ifEmpty { null },
|
||||||
artist = attrs.attr("artist").ifEmpty { null },
|
artist = attrs.attr("artist").ifEmpty { null },
|
||||||
|
displayArtist = attrs.attr("displayArtist").ifEmpty { null },
|
||||||
duration = attrs.longAttr("duration"),
|
duration = attrs.longAttr("duration"),
|
||||||
track = attrs.longAttr("track"),
|
track = attrs.longAttr("track"),
|
||||||
disc = attrs.longAttr("discNumber"),
|
disc = attrs.longAttr("discNumber"),
|
||||||
@@ -168,6 +170,7 @@ internal class AlbumDraft(
|
|||||||
var artistId: String? = null,
|
var artistId: String? = null,
|
||||||
var name: String = "",
|
var name: String = "",
|
||||||
var albumArtist: String? = null,
|
var albumArtist: String? = null,
|
||||||
|
var displayArtist: String? = null,
|
||||||
var created: Long = 0L,
|
var created: Long = 0L,
|
||||||
var coverArt: String? = null,
|
var coverArt: String? = null,
|
||||||
var genre: String? = null,
|
var genre: String? = null,
|
||||||
@@ -203,13 +206,14 @@ internal class AlbumDraft(
|
|||||||
artistContainer = false
|
artistContainer = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toAlbum(): Album =
|
fun toAlbum(): Album {
|
||||||
Album(
|
val refs = artistFallback(artists, artistId, albumArtist)
|
||||||
|
return Album(
|
||||||
sourceId = sourceId,
|
sourceId = sourceId,
|
||||||
id = id,
|
id = id,
|
||||||
artistId = artistId,
|
artistId = artistId,
|
||||||
name = name,
|
name = name,
|
||||||
albumArtist = albumArtist,
|
albumArtist = displayName(albumArtist, displayArtist, refs),
|
||||||
created = created,
|
created = created,
|
||||||
coverArt = coverArt,
|
coverArt = coverArt,
|
||||||
genre = genre,
|
genre = genre,
|
||||||
@@ -217,9 +221,10 @@ internal class AlbumDraft(
|
|||||||
starred = starred,
|
starred = starred,
|
||||||
songCount = songCount,
|
songCount = songCount,
|
||||||
discTitles = discTitles.toMap(),
|
discTitles = discTitles.toMap(),
|
||||||
artists = artistFallback(artists, artistId, albumArtist),
|
artists = refs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal class SongDraft(
|
internal class SongDraft(
|
||||||
private val sourceId: Long,
|
private val sourceId: Long,
|
||||||
@@ -229,6 +234,7 @@ internal class SongDraft(
|
|||||||
private val title: String = "",
|
private val title: String = "",
|
||||||
private val album: String? = null,
|
private val album: String? = null,
|
||||||
private val artist: String? = null,
|
private val artist: String? = null,
|
||||||
|
private val displayArtist: String? = null,
|
||||||
private val duration: Long? = null,
|
private val duration: Long? = null,
|
||||||
private val track: Long? = null,
|
private val track: Long? = null,
|
||||||
private val disc: Long? = null,
|
private val disc: Long? = null,
|
||||||
@@ -279,15 +285,16 @@ internal class SongDraft(
|
|||||||
nestedArtists?.let { list -> attrs.artistRef()?.let { list += it } }
|
nestedArtists?.let { list -> attrs.artistRef()?.let { list += it } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toSong(): Song =
|
fun toSong(): Song {
|
||||||
Song(
|
val refs = artistFallback(artists, artistId, artist)
|
||||||
|
return Song(
|
||||||
sourceId = sourceId,
|
sourceId = sourceId,
|
||||||
id = id,
|
id = id,
|
||||||
albumId = albumId,
|
albumId = albumId,
|
||||||
artistId = artistId,
|
artistId = artistId,
|
||||||
title = title,
|
title = title,
|
||||||
album = album,
|
album = album,
|
||||||
artist = artist,
|
artist = displayName(artist, displayArtist, refs),
|
||||||
duration = duration,
|
duration = duration,
|
||||||
track = track,
|
track = track,
|
||||||
disc = disc,
|
disc = disc,
|
||||||
@@ -301,9 +308,10 @@ internal class SongDraft(
|
|||||||
trackPeak = trackPeak,
|
trackPeak = trackPeak,
|
||||||
albumPeak = albumPeak,
|
albumPeak = albumPeak,
|
||||||
genres = genres.toList(),
|
genres = genres.toList(),
|
||||||
artists = artistFallback(artists, artistId, artist),
|
artists = refs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun Attributes.artistRef(): ArtistRef? {
|
private fun Attributes.artistRef(): ArtistRef? {
|
||||||
val id = attr("id")
|
val id = attr("id")
|
||||||
@@ -323,6 +331,22 @@ private fun artistFallback(
|
|||||||
return listOf(ArtistRef(artistId, name.orEmpty()))
|
return listOf(ArtistRef(artistId, name.orEmpty()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val ARTIST_JOINER = " • "
|
||||||
|
|
||||||
|
private fun displayName(
|
||||||
|
legacy: String?,
|
||||||
|
openSubsonic: String?,
|
||||||
|
refs: List<ArtistRef>,
|
||||||
|
): String? =
|
||||||
|
legacy?.takeIf { it.isNotBlank() }
|
||||||
|
?: openSubsonic?.takeIf { it.isNotBlank() }
|
||||||
|
?: refs
|
||||||
|
.map { it.name }
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.distinct()
|
||||||
|
.joinToString(ARTIST_JOINER)
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
internal fun Attributes.attr(name: String): String = getValue(name) ?: ""
|
internal fun Attributes.attr(name: String): String = getValue(name) ?: ""
|
||||||
|
|
||||||
internal fun Attributes.longAttr(name: String): Long? = attr(name).takeIf { it.isNotEmpty() }?.toLongOrNull()
|
internal fun Attributes.longAttr(name: String): Long? = attr(name).takeIf { it.isNotEmpty() }?.toLongOrNull()
|
||||||
|
|||||||
@@ -240,6 +240,80 @@ class SubsonicXmlTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fallsBackToThePluralNamesWhenTheLegacyDisplayArtistIsBlank() {
|
||||||
|
val albums =
|
||||||
|
albums(
|
||||||
|
"""
|
||||||
|
<subsonic-response status="ok">
|
||||||
|
<albumList2>
|
||||||
|
<album id="al1" name="Split" songCount="1">
|
||||||
|
<artists id="ar1" name="A"/>
|
||||||
|
<artists id="ar2" name="B"/>
|
||||||
|
</album>
|
||||||
|
</albumList2>
|
||||||
|
</subsonic-response>
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("A • B", albums.single().albumArtist)
|
||||||
|
assertEquals(listOf(ArtistRef("ar1", "A"), ArtistRef("ar2", "B")), albums.single().artists)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prefersTheOpenSubsonicDisplayArtistOverThePluralNames() {
|
||||||
|
val albums =
|
||||||
|
albums(
|
||||||
|
"""
|
||||||
|
<subsonic-response status="ok">
|
||||||
|
<albumList2>
|
||||||
|
<album id="al1" name="Split" displayArtist="A feat. B" songCount="1">
|
||||||
|
<artists id="ar1" name="A"/>
|
||||||
|
<artists id="ar2" name="B"/>
|
||||||
|
</album>
|
||||||
|
</albumList2>
|
||||||
|
</subsonic-response>
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("A feat. B", albums.single().albumArtist)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fallsBackToThePluralSongArtistsWhenTheLegacyNameIsBlank() {
|
||||||
|
val songs =
|
||||||
|
songs(
|
||||||
|
"""
|
||||||
|
<subsonic-response status="ok">
|
||||||
|
<song id="s1" title="Collab" albumId="al1">
|
||||||
|
<artists id="ar1" name="A"/>
|
||||||
|
<artists id="ar2" name="B"/>
|
||||||
|
</song>
|
||||||
|
</subsonic-response>
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("A • B", songs.single().artist)
|
||||||
|
assertEquals(listOf(ArtistRef("ar1", "A"), ArtistRef("ar2", "B")), songs.single().artists)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prefersTheOpenSubsonicDisplayArtistForSongs() {
|
||||||
|
val songs =
|
||||||
|
songs(
|
||||||
|
"""
|
||||||
|
<subsonic-response status="ok">
|
||||||
|
<song id="s1" title="Collab" albumId="al1" displayArtist="A feat. B">
|
||||||
|
<artists id="ar1" name="A"/>
|
||||||
|
<artists id="ar2" name="B"/>
|
||||||
|
</song>
|
||||||
|
</subsonic-response>
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals("A feat. B", songs.single().artist)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parsesTimestampsWithFractionsAndOffsets() {
|
fun parsesTimestampsWithFractionsAndOffsets() {
|
||||||
assertEquals(1789906657L, IsoDate.parse("2026-09-20T12:17:37.370827413Z"))
|
assertEquals(1789906657L, IsoDate.parse("2026-09-20T12:17:37.370827413Z"))
|
||||||
|
|||||||
Reference in New Issue
Block a user