Skip to content

Commit

Permalink
Add Synced/Unsynced Lyrics Toggle and Refactor Musixmatch API, closes #…
Browse files Browse the repository at this point in the history
…91 (#95)

* Replaced the search endpoint with the full lyrics endpoint.

Updated the query to separate artist and song name into distinct URL parameters (`artist` and `track`) instead of combining them.

* Refactor and split MusixmatchLyricsResponse into Synced and Unsynced lyrics data classes

- Added new `SyncedLyricsResponse` and `UnsyncedLyricsResponse` data classes to handle synced and unsynced lyrics separately.
- Updated `MusixmatchSearchResponse` to include nullable `syncedLyrics` and `unsyncedLyrics` fields for more precise handling of lyric types.

* (api:update) Refactor SongInfo model to include Musixmatch synced and unsynced lyrics data

* Save the SongInfo object and use it to retreive data

* Add SyncedLyricsSwitch component #91

* Update string resources

* Update UserSettingsController and AboutScreen

- Add syncedMusixmatch property to UserSettingsController
- Update AboutScreen to include SyncedLyricsSwitch component

* Refactor MusixmatchAPI to separate synced and unsynced lyrics retrieval

* Refactor LyricsProviderService to add support for synced Musixmatch lyrics retrieval

* Refactor HomeViewModel and LyricsFetchViewModel to include support for synced and unsynced Musixmatch lyrics retrieval
  • Loading branch information
kerollosy authored Oct 24, 2024
1 parent b9f32eb commit 290dc89
Showing 10 changed files with 90 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -35,6 +35,9 @@ class UserSettingsController(private val dataStore: DataStore<Preferences>) {
var multiPersonWordByWord by mutableStateOf(dataStore.get(multiPersonWordByWordKey, false))
private set

var syncedMusixmatch by mutableStateOf(dataStore.get(syncedMusixmatchKey, true))
private set

var pureBlack by mutableStateOf(dataStore.get(pureBlackKey, false))
private set

@@ -74,6 +77,11 @@ class UserSettingsController(private val dataStore: DataStore<Preferences>) {
multiPersonWordByWord = to
}

fun updateSyncedMusixmatch(to: Boolean) {
dataStore.set(syncedMusixmatchKey, to)
syncedMusixmatch = to
}

fun updateDisableMarquee(to: Boolean) {
dataStore.set(disableMarqueeKey, to)
disableMarquee = to
@@ -96,6 +104,7 @@ private val blacklistedFoldersKey = stringPreferencesKey("blacklist")
private val hideLyricsKey = booleanPreferencesKey("hide_lyrics")
private val includeTranslationKey = booleanPreferencesKey("include_translation")
private val multiPersonWordByWordKey = booleanPreferencesKey("multi_person_word_by_word")
private val syncedMusixmatchKey = booleanPreferencesKey("synced_lyrics")
private val disableMarqueeKey = booleanPreferencesKey("marquee_disable")
private val pureBlackKey = booleanPreferencesKey("pure_black")
private val sdCardPathKey = stringPreferencesKey("sd_card_path")
Original file line number Diff line number Diff line change
@@ -31,8 +31,8 @@ class LyricsProviderService {
// Apple Track ID
private var appleID = 0L

// Musixmatch Track ID
private var musixmatchID = 0L
// Musixmatch Song Info
private var musixmatchSongInfo: SongInfo? = null
// TODO: Use values from SongInfo object returned by search instead of storing them here

/**
@@ -69,7 +69,7 @@ class LyricsProviderService {
} ?: throw NoTrackFoundException()

Providers.MUSIXMATCH -> MusixmatchAPI().getSongInfo(query).also {
musixmatchID = it?.musixmatchID ?: 0
musixmatchSongInfo = it
} ?: throw NoTrackFoundException()
}
} catch (e: InternalErrorException) {
@@ -94,7 +94,8 @@ class LyricsProviderService {
provider: Providers,
// TODO providers could be a sealed interface to include such parameters
includeTranslationNetEase: Boolean = false,
multiPersonWordByWord: Boolean = false
multiPersonWordByWord: Boolean = false,
syncedMusixmatch: Boolean = true
): String? {
return try {
when (provider) {
@@ -108,7 +109,10 @@ class LyricsProviderService {
appleID,
multiPersonWordByWord
)
Providers.MUSIXMATCH -> MusixmatchAPI().getSyncedLyrics(musixmatchID)
Providers.MUSIXMATCH -> MusixmatchAPI().getLyrics(
musixmatchSongInfo,
syncedMusixmatch
)
}
} catch (e: Exception) {
null
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@ import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import pl.lambada.songsync.domain.model.SongInfo
import pl.lambada.songsync.domain.model.lyrics_providers.others.MusixmatchLyricsResponse
import pl.lambada.songsync.domain.model.lyrics_providers.others.MusixmatchSearchResponse
import pl.lambada.songsync.util.EmptyQueryException
import pl.lambada.songsync.util.networking.Ktor.client
@@ -21,52 +20,53 @@ class MusixmatchAPI {
* @return Search result as a SongInfo object.
*/
suspend fun getSongInfo(query: SongInfo): SongInfo? {
val search = withContext(Dispatchers.IO) {
val artistName = withContext(Dispatchers.IO) {
URLEncoder.encode(
"${query.songName} ${query.artistName}",
"${query.artistName}",
Charsets.UTF_8.toString()
)
}

if (search == " ")
val songName = withContext(Dispatchers.IO) {
URLEncoder.encode(
"${query.songName}",
Charsets.UTF_8.toString()
)
}

if (artistName == "" || songName == "")
throw EmptyQueryException()

val response = client.get(
"$baseURL/search?q=$search"
"$baseURL/full?artist=$artistName&track=$songName"
)
val responseBody = response.bodyAsText(Charsets.UTF_8)

if (response.status.value !in 200..299 || responseBody == "[]")
if (response.status.value !in 200..299)
return null

val json = json.decodeFromString<List<MusixmatchSearchResponse>>(responseBody)

val result = json[0]

val result = json.decodeFromString<MusixmatchSearchResponse>(responseBody)

return SongInfo(
songName = result.songName,
artistName = result.artistName,
songLink = result.url,
albumCoverLink = result.artwork,
musixmatchID = result.id,
hasSyncedLyrics = result.hasSyncedLyrics,
hasUnsyncedLyrics = result.hasSyncedLyrics,
syncedLyrics = result.syncedLyrics?.lyrics,
unsyncedLyrics = result.unsyncedLyrics?.lyrics
)
}

/**
* Searches for synced lyrics using the song name and artist name.
* @param id The ID of the song from search results.
* @return The synced lyrics as a string.
* Returns the lyrics.
* @param songInfo The SongInfo of the song from search results.
* @return The lyrics as a string or null if the lyrics were not found.
*/
suspend fun getSyncedLyrics(id: Long): String? {
val response = client.get(
"$baseURL/lyrics?id=$id"
)
val responseBody = response.bodyAsText(Charsets.UTF_8)

if (response.status.value !in 200..299)
return null

val json = json.decodeFromString<MusixmatchLyricsResponse>(responseBody)
return json.lyrics
fun getLyrics(songInfo: SongInfo?, synced: Boolean = true): String? {
return if(synced) songInfo?.syncedLyrics
else songInfo?.unsyncedLyrics
}
}
Original file line number Diff line number Diff line change
@@ -23,4 +23,8 @@ data class SongInfo(
var neteaseID: Long? = null, // Netease-only
var appleID: Long? = null, // Apple-only
var musixmatchID: Long? = null, // Musixmatch-only
var hasSyncedLyrics: Boolean? = null, // Musixmatch-only
var hasUnsyncedLyrics: Boolean? = null, // Musixmatch-only
var syncedLyrics: String? = null, // Musixmatch-only
var unsyncedLyrics: String? = null, // Musixmatch-only
) : Parcelable
Original file line number Diff line number Diff line change
@@ -14,14 +14,24 @@ data class MusixmatchSearchResponse(
val url: String,
val albumId: Long,
val hasSyncedLyrics: Boolean,
val hasUnsyncedLyrics: Boolean
val hasUnsyncedLyrics: Boolean,
val syncedLyrics: SyncedLyricsResponse? = null,
val unsyncedLyrics: UnsyncedLyricsResponse? = null
)

@Serializable
data class MusixmatchLyricsResponse(
data class SyncedLyricsResponse(
val id: Long,
val duration: Int,
val language: String,
val updatedTime: String,
val lyrics: String
)

@Serializable
data class UnsyncedLyricsResponse(
val id: Long,
val language: String,
val updatedTime: String,
val lyrics: String,
)
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ import pl.lambada.songsync.ui.screens.about.components.MultiPersonSwitch
import pl.lambada.songsync.ui.screens.about.components.PureBlackThemeSwitch
import pl.lambada.songsync.ui.screens.about.components.SdCardPathSetting
import pl.lambada.songsync.ui.screens.about.components.SupportSection
import pl.lambada.songsync.ui.screens.about.components.SyncedLyricsSwitch
import pl.lambada.songsync.ui.screens.about.components.TranslationSwitch
import pl.lambada.songsync.ui.screens.about.components.UpdateAvailableDialog
import pl.lambada.songsync.util.ext.getVersion
@@ -89,6 +90,13 @@ fun AboutScreen(
)
}

item {
SyncedLyricsSwitch(
selected = userSettingsController.syncedMusixmatch,
onToggle = { userSettingsController.updateSyncedMusixmatch(it) }
)
}

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
item {
SdCardPathSetting(
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package pl.lambada.songsync.ui.screens.about.components

import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import pl.lambada.songsync.R
import pl.lambada.songsync.ui.components.AboutItem
import pl.lambada.songsync.ui.components.SwitchItem

@Composable
fun SyncedLyricsSwitch(selected: Boolean, onToggle: (Boolean) -> Unit) {
AboutItem(label = stringResource(id = R.string.synced_lyrics)) {
SwitchItem(
label = stringResource(id = R.string.synced_lyrics_summary),
selected = selected,
onClick = { onToggle(!selected) }
)
}
}
Original file line number Diff line number Diff line change
@@ -278,7 +278,8 @@ class HomeViewModel(
version,
provider = userSettingsController.selectedProvider,
includeTranslationNetEase = userSettingsController.includeTranslation,
multiPersonWordByWord = userSettingsController.multiPersonWordByWord
multiPersonWordByWord = userSettingsController.multiPersonWordByWord,
syncedMusixmatch = userSettingsController.syncedMusixmatch
)
}

Original file line number Diff line number Diff line change
@@ -48,7 +48,8 @@ class LyricsFetchViewModel(
version,
userSettingsController.selectedProvider,
userSettingsController.includeTranslation,
userSettingsController.multiPersonWordByWord
userSettingsController.multiPersonWordByWord,
userSettingsController.syncedMusixmatch
)

fun loadSongInfo(context: Context, tryingAgain: Boolean = false) {
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -125,4 +125,6 @@
<string name="embed_non_local_song_error">You tried to embed the lyrics to a non-local file. Aborting operation.</string>
<string name="multi_person_word_by_word">Multi-person word by word lyrics</string>
<string name="multi_person_word_by_word_summary">Use multi-person lyrics format when getting lyrics from Apple Music</string>
<string name="synced_lyrics">Get Synced Lyrics</string>
<string name="synced_lyrics_summary">Toggle to switch between synced and unsynced lyrics from Musixmatch.</string>
</resources>

0 comments on commit 290dc89

Please sign in to comment.