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
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class AiMetadataGenerator @Inject constructor(
$systemPrompt
Song title: "${song.title}"
Song artist: "${song.artist}"
Song artist: "${song.displayArtist}"
$albumInfo
Fields to complete: [$fieldsJson]
""".trimIndent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class AiPlaylistGenerator @Inject constructor(
{
"id": "${song.id}",
"title": "${song.title.replace("\"", "'")}",
"artist": "${song.artist.replace("\"", "'")}",
"artist": "${song.displayArtist.replace("\"", "'")}",
"genre": "${song.genre?.replace("\"", "'") ?: "unknown"}",
"relevance_score": $score
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ interface MusicDao {
applyDirectoryFilter: Boolean
): Flow<List<ArtistEntity>>

/**
* Unfiltered list of all artists (including those only reachable via cross-refs).
*/
@Query("SELECT * FROM artists ORDER BY name ASC")
fun getAllArtistsRaw(): Flow<List<ArtistEntity>>

@Query("SELECT * FROM artists WHERE id = :artistId")
fun getArtistById(artistId: Long): Flow<ArtistEntity?>

Expand All @@ -186,6 +192,12 @@ interface MusicDao {
applyDirectoryFilter: Boolean
): List<ArtistEntity>

/**
* Unfiltered list of all artists (one-shot).
*/
@Query("SELECT * FROM artists ORDER BY name ASC")
suspend fun getAllArtistsListRaw(): List<ArtistEntity>

@Query("""
SELECT DISTINCT artists.* FROM artists
INNER JOIN songs ON artists.id = songs.artist_id
Expand Down Expand Up @@ -301,6 +313,9 @@ interface MusicDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertSongArtistCrossRefs(crossRefs: List<SongArtistCrossRef>)

@Query("SELECT * FROM song_artist_cross_ref")
fun getAllSongArtistCrossRefs(): Flow<List<SongArtistCrossRef>>

@Query("DELETE FROM song_artist_cross_ref")
suspend fun clearAllSongArtistCrossRefs()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ fun SongEntity.toSong(): Song {
* Converts a SongEntity to Song with artists from the junction table.
*/
fun SongEntity.toSongWithArtistRefs(artists: List<ArtistEntity>, crossRefs: List<SongArtistCrossRef>): Song {
val crossRefByArtistId = crossRefs.associateBy { it.artistId }
val artistRefs = artists.map { artist ->
val crossRef = crossRefs.find { it.artistId == artist.id }
val crossRef = crossRefByArtistId[artist.id]
ArtistRef(
id = artist.id,
name = artist.name.normalizeMetadataTextOrEmpty(),
Expand Down
21 changes: 13 additions & 8 deletions app/src/main/java/com/theveloper/pixelplay/data/model/Song.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ package com.theveloper.pixelplay.data.model

import android.net.Uri
import androidx.compose.runtime.Immutable
import com.theveloper.pixelplay.utils.splitArtistsByDelimiters

@Immutable
data class Song(
val id: String,
val title: String,
/**
* Legacy artist display string.
* - If multi-artist support (e.g., artistSeparationEnabled) is disabled, this contains the primary artist or a combined artist string from metadata.
* - If multi-artist support is enabled, this typically contains only the primary artist for backward compatibility.
* - With multi-artist parsing enabled by default, this typically contains only the primary artist for backward compatibility.
* For accurate display of all artists, use the [artists] list and [displayArtist] property.
*/
val artist: String,
Expand All @@ -33,16 +33,21 @@ data class Song(
val bitrate: Int?,
val sampleRate: Int?,
) {
private val defaultArtistDelimiters = listOf("/", ";", ",", "+", "&")

/**
* Returns the display string for artists.
* If multiple artists exist, joins them with ", ".
* Falls back to the artist field if artists list is empty.
* Falls back to splitting the legacy artist string using common delimiters,
* and finally the raw artist field if nothing else is available.
*/
val displayArtist: String
get() = if (artists.isNotEmpty()) {
artists.sortedByDescending { it.isPrimary }.joinToString(", ") { it.name }
} else {
artist
get() {
if (artists.isNotEmpty()) {
return artists.sortedByDescending { it.isPrimary }.joinToString(", ") { it.name }
}
val split = artist.splitArtistsByDelimiters(defaultArtistDelimiters)
return if (split.isNotEmpty()) split.joinToString(", ") else artist
}
Comment on lines 44 to 51
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The displayArtist property computes string splitting every time it's accessed. When the artists list is empty, it calls splitArtistsByDelimiters on each access. Since this is used in UI components that may render frequently, consider caching this computed value or making displayArtist a regular property initialized during object creation to avoid repeated computation.

Copilot uses AI. Check for mistakes.

/**
Expand Down Expand Up @@ -81,4 +86,4 @@ data class Song(
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import com.theveloper.pixelplay.data.model.TransitionSettings
import com.theveloper.pixelplay.data.preferences.FullPlayerLoadingTweaks
import dagger.hilt.android.qualifiers.ApplicationContext
import androidx.datastore.preferences.core.MutablePreferences
import androidx.lifecycle.viewModelScope
import androidx.media3.common.Player
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
Expand Down Expand Up @@ -107,7 +106,6 @@ class UserPreferencesRepository @Inject constructor(
val FULL_PLAYER_PLACEHOLDER_TRANSPARENT = booleanPreferencesKey("full_player_placeholder_transparent")

// Multi-Artist Settings
val ARTIST_SEPARATION_ENABLED = booleanPreferencesKey("artist_separation_enabled")
val ARTIST_DELIMITERS = stringPreferencesKey("artist_delimiters")
val GROUP_BY_ALBUM_ARTIST = booleanPreferencesKey("group_by_album_artist")
val ARTIST_SETTINGS_RESCAN_REQUIRED = booleanPreferencesKey("artist_settings_rescan_required")
Expand Down Expand Up @@ -153,17 +151,6 @@ class UserPreferencesRepository @Inject constructor(

// ===== Multi-Artist Settings =====

val artistSeparationEnabledFlow: Flow<Boolean> = dataStore.data
.map { preferences -> preferences[PreferencesKeys.ARTIST_SEPARATION_ENABLED] ?: false }

suspend fun setArtistSeparationEnabled(enabled: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.ARTIST_SEPARATION_ENABLED] = enabled
// Mark rescan as required when this setting changes
preferences[PreferencesKeys.ARTIST_SETTINGS_RESCAN_REQUIRED] = true
}
}

val artistDelimitersFlow: Flow<List<String>> = dataStore.data
.map { preferences ->
val stored = preferences[PreferencesKeys.ARTIST_DELIMITERS]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class LyricsRepositoryImpl @Inject constructor(
LogUtils.d(this@LyricsRepositoryImpl, "Fetching lyrics from remote for: ${song.title}")
val response = lrcLibApiService.getLyrics(
trackName = song.title,
artistName = song.artist,
artistName = song.displayArtist,
albumName = song.album,
duration = (song.duration / 1000).toInt()
)
Expand Down
Loading