Skip to content

Commit

Permalink
Add Musixmatch API Integration (#88)
Browse files Browse the repository at this point in the history
* Add support for Musixmatch lyrics provider

* TODO: Handle Musixmatch provider selected

* Added `musixmatchID` to track Musixmatch song ID

* Add musixmatchID field to SongInfo model

* Add MusixmatchAPI class for searching and retrieving synced lyrics

* fix: correct data type for Musixmatch album_id in search response

* refactor: rename fields in Musixmatch response models for consistency

* fix: correct Musixmatch API usage in LyricsProviderService

* feat: add separate fields for synced and unsynced lyrics to Musixmatch API response
  • Loading branch information
kerollosy authored Oct 15, 2024
1 parent db39c88 commit c2c669b
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package pl.lambada.songsync.data.remote.lyrics_providers
import android.util.Log
import pl.lambada.songsync.data.remote.lyrics_providers.others.AppleAPI
import pl.lambada.songsync.data.remote.lyrics_providers.others.LRCLibAPI
import pl.lambada.songsync.data.remote.lyrics_providers.others.MusixmatchAPI
import pl.lambada.songsync.data.remote.lyrics_providers.others.NeteaseAPI
import pl.lambada.songsync.data.remote.lyrics_providers.spotify.SpotifyAPI
import pl.lambada.songsync.data.remote.lyrics_providers.spotify.SpotifyLyricsAPI
Expand All @@ -29,6 +30,9 @@ class LyricsProviderService {

// Apple Track ID
private var appleID = 0L

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

/**
Expand Down Expand Up @@ -63,6 +67,10 @@ class LyricsProviderService {
Providers.APPLE -> AppleAPI().getSongInfo(query).also {
appleID = it?.appleID ?: 0
} ?: throw NoTrackFoundException()

Providers.MUSIXMATCH -> MusixmatchAPI().getSongInfo(query).also {
musixmatchID = it?.musixmatchID ?: 0
} ?: throw NoTrackFoundException()
}
} catch (e: InternalErrorException) {
throw e
Expand Down Expand Up @@ -100,6 +108,7 @@ class LyricsProviderService {
appleID,
multiPersonWordByWord
)
Providers.MUSIXMATCH -> MusixmatchAPI().getSyncedLyrics(musixmatchID)
}
} catch (e: Exception) {
null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package pl.lambada.songsync.data.remote.lyrics_providers.others

import io.ktor.client.request.get
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
import pl.lambada.songsync.util.networking.Ktor.json
import java.net.URLEncoder

class MusixmatchAPI {
private val baseURL = "https://kerollosy.alwaysdata.net"

/**
* Searches for synced lyrics using the song name and artist name.
* @param query The SongInfo object with songName and artistName fields filled.
* @return Search result as a SongInfo object.
*/
suspend fun getSongInfo(query: SongInfo): SongInfo? {
val search = withContext(Dispatchers.IO) {
URLEncoder.encode(
"${query.songName} ${query.artistName}",
Charsets.UTF_8.toString()
)
}

if (search == " ")
throw EmptyQueryException()

val response = client.get(
"$baseURL/search?q=$search"
)
val responseBody = response.bodyAsText(Charsets.UTF_8)

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

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

val result = json[0]

return SongInfo(
songName = result.songName,
artistName = result.artistName,
songLink = result.url,
albumCoverLink = result.artwork,
musixmatchID = result.id,
)
}

/**
* 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.
*/
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ data class SongInfo(
var lrcLibID: Int? = null, // LRCLib-only
var neteaseID: Long? = null, // Netease-only
var appleID: Long? = null, // Apple-only
var musixmatchID: Long? = null, // Musixmatch-only
) : Parcelable
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package pl.lambada.songsync.domain.model.lyrics_providers.others

import kotlinx.serialization.Serializable

@Serializable
data class MusixmatchSearchResponse(
val id: Long,
val songName: String,
val artistName: String,
val albumName: String,
val artwork: String,
val releaseDate: String,
val duration: Int,
val url: String,
val albumId: Long,
val hasSyncedLyrics: Boolean,
val hasUnsyncedLyrics: Boolean
)

@Serializable
data class MusixmatchLyricsResponse(
val id: Long,
val duration: Int,
val language: String,
val updatedTime: String,
val lyrics: String,
)
3 changes: 2 additions & 1 deletion app/src/main/java/pl/lambada/songsync/util/LyricsUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ enum class Providers(val displayName: String) {
SPOTIFY("Spotify (via SpotifyLyricsAPI)"),
LRCLIB("LRCLib"),
NETEASE("Netease") { val inf = 0},
APPLE("Apple Music")
APPLE("Apple Music"),
MUSIXMATCH("Musixmatch")
}

// only for invoking the task and handling and reporting progress
Expand Down

0 comments on commit c2c669b

Please sign in to comment.