Skip to content

Commit 3b1cfae

Browse files
authored
Merge pull request #2082 from Amonoman/master
Enhance library performance, genre separation, and better TTML support
2 parents dbc038d + 69b8054 commit 3b1cfae

23 files changed

Lines changed: 1063 additions & 1263 deletions

.github/workflows/codeql.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,22 @@ jobs:
3232
- name: Setup Gradle
3333
uses: gradle/actions/setup-gradle@v6
3434

35+
- name: Pin Kotlin version for CodeQL compatibility
36+
# CodeQL does not yet support Kotlin 2.4.0 (max: 2.3.x).
37+
# This overrides the version only for this CI job without touching source files.
38+
# TODO: remove once CodeQL adds Kotlin 2.4.0 support.
39+
run: echo "kotlin.version=2.3.20" >> gradle.properties
40+
3541
- name: Initialize CodeQL
3642
uses: github/codeql-action/init@v4
3743
with:
3844
languages: java-kotlin
3945
build-mode: manual
4046

4147
- name: Build with Gradle
42-
run: ./gradlew assembleDebug # Use the wrapper for consistency
48+
run: ./gradlew assembleDebug
4349

4450
- name: Perform CodeQL Analysis
4551
uses: github/codeql-action/analyze@v4
4652
with:
47-
# Match the languages defined in the init step
4853
category: "/language:java-kotlin"

app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,35 @@ interface MusicDao {
15521552
applyDirectoryFilter: Boolean
15531553
): Flow<List<SongEntity>>
15541554

1555+
// Multi-genre aware query: matches songs where the genre column equals the name (case-
1556+
// insensitively, via LIKE), or contains it as part of a comma-separated list.
1557+
// SQLite LIKE is case-insensitive for ASCII letters by default, which is sufficient
1558+
// for genre names. All six arms use LIKE so that "rock" matches "Rock", "Rock,Pop",
1559+
// "Rock, Pop", "Pop,Rock", "Pop, Rock", and "Pop,Rock,Jazz".
1560+
@Query("""
1561+
SELECT * FROM songs
1562+
WHERE (:applyDirectoryFilter = 0 OR id < 0 OR parent_directory_path IN (:allowedParentDirs))
1563+
AND (
1564+
genre LIKE :genreName
1565+
OR genre LIKE :genrePrefix
1566+
OR genre LIKE :genreSuffixWithSpace
1567+
OR genre LIKE :genreSuffix
1568+
OR genre LIKE :genreMiddleWithSpace
1569+
OR genre LIKE :genreMiddle
1570+
)
1571+
ORDER BY title ASC
1572+
""")
1573+
fun getSongsByGenreContaining(
1574+
genreName: String,
1575+
genrePrefix: String,
1576+
genreSuffixWithSpace: String,
1577+
genreSuffix: String,
1578+
genreMiddleWithSpace: String,
1579+
genreMiddle: String,
1580+
allowedParentDirs: List<String>,
1581+
applyDirectoryFilter: Boolean
1582+
): Flow<List<SongEntity>>
1583+
15551584
@Query("""
15561585
SELECT * FROM songs
15571586
WHERE (:applyDirectoryFilter = 0 OR id < 0 OR parent_directory_path IN (:allowedParentDirs))

app/src/main/java/com/theveloper/pixelplay/data/preferences/EqualizerPreferencesRepository.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,17 @@ class EqualizerPreferencesRepository @Inject constructor(
3939
val PINNED_PRESETS = stringPreferencesKey("pinned_presets_json")
4040
}
4141

42-
val equalizerViewModeFlow: Flow<UserPreferencesRepository.EqualizerViewMode> = dataStore.data.map { preferences ->
42+
val equalizerViewModeFlow: Flow<EqualizerViewMode> = dataStore.data.map { preferences ->
4343
val modeString = preferences[Keys.VIEW_MODE]
4444
if (modeString != null) {
4545
try {
46-
UserPreferencesRepository.EqualizerViewMode.valueOf(modeString)
46+
EqualizerViewMode.valueOf(modeString)
4747
} catch (_: Exception) {
48-
UserPreferencesRepository.EqualizerViewMode.SLIDERS
48+
EqualizerViewMode.SLIDERS
4949
}
5050
} else {
5151
val isGraph = preferences[booleanPreferencesKey("is_graph_view")] ?: false
52-
if (isGraph) UserPreferencesRepository.EqualizerViewMode.GRAPH else UserPreferencesRepository.EqualizerViewMode.SLIDERS
52+
if (isGraph) EqualizerViewMode.GRAPH else EqualizerViewMode.SLIDERS
5353
}
5454
}
5555

@@ -141,7 +141,7 @@ class EqualizerPreferencesRepository @Inject constructor(
141141
}
142142
}
143143

144-
suspend fun setEqualizerViewMode(mode: UserPreferencesRepository.EqualizerViewMode) =
144+
suspend fun setEqualizerViewMode(mode: EqualizerViewMode) =
145145
dataStore.edit { preferences ->
146146
preferences[Keys.VIEW_MODE] = mode.name
147147
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.theveloper.pixelplay.data.preferences
2+
3+
enum class EqualizerViewMode {
4+
SLIDERS, GRAPH, HYBRID
5+
}

app/src/main/java/com/theveloper/pixelplay/data/preferences/UserPreferencesRepository.kt

Lines changed: 596 additions & 1104 deletions
Large diffs are not rendered by default.

app/src/main/java/com/theveloper/pixelplay/data/repository/LyricsRepositoryImpl.kt

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,29 @@ import kotlinx.coroutines.coroutineScope
5353
import kotlinx.coroutines.sync.Semaphore
5454
import kotlinx.coroutines.sync.withPermit
5555

56+
private val EMBEDDED_LYRICS_KEYS = listOf("LYRICS", "SYNCEDLYRICS", "TTML", "UNSYNCEDLYRICS")
57+
5658
private fun Lyrics.isValid(): Boolean = !synced.isNullOrEmpty() || !plain.isNullOrEmpty()
5759

60+
internal fun parseBestEmbeddedLyricsField(propertyMap: Map<String, Array<String>>?): Lyrics? {
61+
var firstPlainLyrics: Lyrics? = null
62+
63+
EMBEDDED_LYRICS_KEYS.forEach { key ->
64+
propertyMap?.get(key).orEmpty().forEach { field ->
65+
if (field.isBlank()) return@forEach
66+
67+
val parsedLyrics = LyricsUtils.parseLyrics(field)
68+
if (!parsedLyrics.isValid()) return@forEach
69+
70+
val localLyrics = parsedLyrics.copy(areFromRemote = false)
71+
if (!localLyrics.synced.isNullOrEmpty()) return localLyrics
72+
if (firstPlainLyrics == null) firstPlainLyrics = localLyrics
73+
}
74+
}
75+
76+
return firstPlainLyrics
77+
}
78+
5879
/**
5980
* LyricsData for JSON disk cache (matches Rhythm's format)
6081
*/
@@ -1082,17 +1103,11 @@ class LyricsRepositoryImpl @Inject constructor(
10821103
ParcelFileDescriptor.open(tempFile, ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
10831104
val metadata = TagLib.getMetadata(fd.detachFd())
10841105
val propertyMap = metadata?.propertyMap
1085-
val lyricsField = propertyMap?.get("LYRICS")?.firstOrNull()
1086-
?: propertyMap?.get("UNSYNCEDLYRICS")?.firstOrNull()
1087-
1088-
if (!lyricsField.isNullOrBlank()) {
1089-
val parsedLyrics = LyricsUtils.parseLyrics(lyricsField)
1090-
if (parsedLyrics.isValid()) {
1091-
Log.d(TAG, "===== FOUND EMBEDDED LYRICS =====")
1092-
parsedLyrics.copy(areFromRemote = false)
1093-
} else {
1094-
null
1095-
}
1106+
val parsedLyrics = parseBestEmbeddedLyricsField(propertyMap)
1107+
1108+
if (parsedLyrics != null) {
1109+
Log.d(TAG, "===== FOUND EMBEDDED LYRICS =====")
1110+
parsedLyrics
10961111
} else {
10971112
null
10981113
}

app/src/main/java/com/theveloper/pixelplay/data/repository/MusicRepositoryImpl.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,8 +688,15 @@ class MusicRepositoryImpl @Inject constructor(
688688
applyDirectoryFilter = applyDirectoryFilter
689689
)
690690
} else {
691-
musicDao.getSongsByGenre(
691+
// getSongsByGenreContaining uses a LIKE query so that a song stored as
692+
// "Rock, Pop" is returned when browsing either "Rock" or "Pop".
693+
musicDao.getSongsByGenreContaining(
692694
genreName = genreName,
695+
genrePrefix = "$genreName,%", // "Rock,..." / "Rock, ..."
696+
genreSuffixWithSpace = "%, $genreName", // "..., Rock"
697+
genreSuffix = "%,$genreName", // "...,Rock"
698+
genreMiddleWithSpace = "%, $genreName,%", // "..., Rock,..."
699+
genreMiddle = "%,$genreName,%", // "...,Rock,..."
693700
allowedParentDirs = allowedParentDirs,
694701
applyDirectoryFilter = applyDirectoryFilter
695702
)
@@ -877,6 +884,7 @@ class MusicRepositoryImpl @Inject constructor(
877884
) { genreNames, hasUnknown ->
878885
val knownGenres = genreNames
879886
.asSequence()
887+
.flatMap { raw -> raw.split(",") } // split "Rock, Pop" → ["Rock", "Pop"]
880888
.map { it.trim() }
881889
.filter { it.isNotBlank() }
882890
.map { buildGenre(it) }

app/src/main/java/com/theveloper/pixelplay/data/service/MusicService.kt

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -921,9 +921,6 @@ class MusicService : MediaLibraryService() {
921921
}
922922
}
923923
return hasWearHints
924-
// If hints identify a Wear/remote controller and it's not our app package,
925-
// reject to avoid the default Wear system media player hijacking the session.
926-
return true
927924
}
928925

929926
private fun createSleepTimerPendingIntent(): PendingIntent {
@@ -2332,7 +2329,9 @@ class MusicService : MediaLibraryService() {
23322329
}
23332330

23342331
private fun readBytesCapped(input: java.io.InputStream, maxBytes: Int): ByteArray? {
2335-
val output = ByteArrayOutputStream()
2332+
// Pre-size to 4× the read-buffer to reduce reallocation churn on typical album art
2333+
// (50–300 KB). Still far below the maxBytes cap enforced in the loop below.
2334+
val output = ByteArrayOutputStream(DEFAULT_STREAM_BUFFER_SIZE * 4)
23362335
val buffer = ByteArray(DEFAULT_STREAM_BUFFER_SIZE)
23372336
var totalRead = 0
23382337
while (true) {
@@ -2846,13 +2845,6 @@ class MusicService : MediaLibraryService() {
28462845
}
28472846
}
28482847

2849-
/**
2850-
* Bridges a suspend block into a [ListenableFuture] for Media3 callback methods.
2851-
*/
2852-
/**
2853-
* Bridges a suspend block into a [ListenableFuture] for Media3 callback methods.
2854-
*/
2855-
28562848
/**
28572849
* Bridges a suspend block into a [ListenableFuture] for Media3 callback methods.
28582850
*/

0 commit comments

Comments
 (0)