From c2c669b04500ae4d7084a7ca813267d03d5a730e Mon Sep 17 00:00:00 2001 From: Kerollos Emad Date: Tue, 15 Oct 2024 12:35:57 +0300 Subject: [PATCH] Add Musixmatch API Integration (#88) * 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 --- .../lyrics_providers/LyricsProviderService.kt | 9 +++ .../lyrics_providers/others/MusixmatchAPI.kt | 72 +++++++++++++++++++ .../lambada/songsync/domain/model/SongInfo.kt | 1 + .../lyrics_providers/others/Musixmatch.kt | 27 +++++++ .../pl/lambada/songsync/util/LyricsUtils.kt | 3 +- 5 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/MusixmatchAPI.kt create mode 100644 app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/Musixmatch.kt diff --git a/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/LyricsProviderService.kt b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/LyricsProviderService.kt index bd4857f..b915d81 100644 --- a/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/LyricsProviderService.kt +++ b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/LyricsProviderService.kt @@ -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 @@ -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 /** @@ -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 @@ -100,6 +108,7 @@ class LyricsProviderService { appleID, multiPersonWordByWord ) + Providers.MUSIXMATCH -> MusixmatchAPI().getSyncedLyrics(musixmatchID) } } catch (e: Exception) { null diff --git a/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/MusixmatchAPI.kt b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/MusixmatchAPI.kt new file mode 100644 index 0000000..c1382fb --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/data/remote/lyrics_providers/others/MusixmatchAPI.kt @@ -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>(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(responseBody) + return json.lyrics + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/domain/model/SongInfo.kt b/app/src/main/java/pl/lambada/songsync/domain/model/SongInfo.kt index d85c5ea..598c8fc 100644 --- a/app/src/main/java/pl/lambada/songsync/domain/model/SongInfo.kt +++ b/app/src/main/java/pl/lambada/songsync/domain/model/SongInfo.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/Musixmatch.kt b/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/Musixmatch.kt new file mode 100644 index 0000000..223ce36 --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/domain/model/lyrics_providers/others/Musixmatch.kt @@ -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, +) diff --git a/app/src/main/java/pl/lambada/songsync/util/LyricsUtils.kt b/app/src/main/java/pl/lambada/songsync/util/LyricsUtils.kt index 1092ab1..e0356f1 100644 --- a/app/src/main/java/pl/lambada/songsync/util/LyricsUtils.kt +++ b/app/src/main/java/pl/lambada/songsync/util/LyricsUtils.kt @@ -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