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
@@ -1,7 +1,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.apple.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
Expand Down Expand Up @@ -34,7 +34,10 @@ class LyricsProviderService {

// Netease Track ID and stuff
private var neteaseID = 0L


// Apple API
private val appleAPI = AppleAPI()

// Apple Track ID
private var appleID = 0L

Expand Down Expand Up @@ -81,7 +84,7 @@ class LyricsProviderService {
qqPayload = it?.qqPayload ?: ""
} ?: throw NoTrackFoundException()

Providers.APPLE -> AppleAPI().getSongInfo(query, offset).also {
Providers.APPLE -> appleAPI.getSongInfo(query, offset).also {
appleID = it?.appleID ?: 0
} ?: throw NoTrackFoundException()

Expand Down Expand Up @@ -121,7 +124,7 @@ class LyricsProviderService {

Providers.QQMUSIC -> QQMusicAPI().getSyncedLyrics(qqPayload, multiPersonWordByWord)

Providers.APPLE -> AppleAPI().getSyncedLyrics(
Providers.APPLE -> appleAPI.getSyncedLyrics(
appleID, multiPersonWordByWord
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package pl.lambada.songsync.data.remote.lyrics_providers.apple

import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import pl.lambada.songsync.data.remote.PaxMusicHelper
import pl.lambada.songsync.domain.model.SongInfo
import pl.lambada.songsync.domain.model.lyrics_providers.others.AppleMusicSearchResponse
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 AppleAPI {
private val lyricsBaseURL = "https://lyrics.paxsenix.org/"
private val apiBaseURL = "https://amp-api.music.apple.com/v1/catalog/us"
private val tokenManager = AppleTokenManager()

/**
* Searches for song information using the song name and artist name.
* @param query The SongInfo object with songName and artistName fields filled.
* @param offset The offset used for trying to find a better match or searching again.
* @return Search result as a SongInfo object.
*/
suspend fun getSongInfo(query: SongInfo, offset: Int = 0): SongInfo? {
val search = withContext(Dispatchers.IO) {
URLEncoder.encode(
"${query.songName} ${query.artistName}",
Charsets.UTF_8.toString()
)
}

if (search.isBlank())
throw EmptyQueryException()

return try {
val token = tokenManager.getToken()

val response = client.get(
"$apiBaseURL/search?" +
"term=$search&" +
"types=songs&" +
"limit=25&" +
"l=en-US&" +
"platform=web&" +
"format[resources]=map&" +
"include[songs]=artists&" +
"extend=artistUrl"
) {
header("Authorization", "Bearer $token")
header("Origin", "https://music.apple.com")
header("Referer", "https://music.apple.com/")
header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0")
header("Accept", "application/json")
header("Accept-Language", "en-US,en;q=0.5")
header("x-apple-renewal", "true")
}

val responseBody = response.bodyAsText(Charsets.UTF_8)

if (response.status.value !in 200..299) {
// Token might be expired, clear it and retry once
if (response.status.value == 401) {
tokenManager.clearToken()
}
return null
}

val searchResponse = try {
json.decodeFromString<AppleMusicSearchResponse>(responseBody)
} catch (e: Exception) {
return null
}

val songs = searchResponse.results.songs?.data ?: return null

if (offset >= songs.size)
return null

val songId = songs[offset].id
val songDetail = searchResponse.resources?.songs?.get(songId) ?: return null
val attributes = songDetail.attributes

val artworkUrl = attributes.artwork.url
.replace("{w}", "100")
.replace("{h}", "100")
.replace("{f}", "png")

SongInfo(
songName = attributes.name,
artistName = attributes.artistName,
songLink = attributes.url,
albumCoverLink = artworkUrl,
appleID = songId.toLongOrNull() ?: return null
)
} catch (e: Exception) {
e.printStackTrace()
null
}
}

/**
* Gets synced lyrics using the song ID and returns them as a string formatted as an LRC file.
* @param id The ID of the song from search results.
* @param multiPersonWordByWord Flag to format lyrics for multiple persons word by word.
* @return The synced lyrics as a string.
*/
suspend fun getSyncedLyrics(id: Long, multiPersonWordByWord: Boolean): String? {
val response = client.get(
lyricsBaseURL + "apple-music/lyrics?id=$id"
)
val responseBody = response.bodyAsText(Charsets.UTF_8)

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

return PaxMusicHelper().formatWordByWordLyrics(responseBody, multiPersonWordByWord)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package pl.lambada.songsync.data.remote.lyrics_providers.apple

import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import pl.lambada.songsync.util.networking.Ktor.client

class AppleTokenManager {
private var cachedToken: String? = null
private val mutex = Mutex()

suspend fun getToken(): String {
mutex.withLock {
cachedToken?.let { return it }

try {
val mainPageResponse = client.get("https://beta.music.apple.com")
val mainPageBody = mainPageResponse.bodyAsText()

val indexJsRegex = Regex("""/assets/index~[^/]+\.js""")
val indexJsMatch = indexJsRegex.find(mainPageBody)
?: throw Exception("Could not find index-legacy script URL")

val indexJsUri = indexJsMatch.value

val indexJsResponse = client.get("https://beta.music.apple.com$indexJsUri")
val indexJsBody = indexJsResponse.bodyAsText()

val tokenRegex = Regex("""eyJh([^"]*)""")
val tokenMatch = tokenRegex.find(indexJsBody)
?: throw Exception("Could not find token")

val token = tokenMatch.value
cachedToken = token
return token
} catch (e: Exception) {
throw Exception("Error fetching Apple Music token: ${e.message}", e)
}
}
}

fun clearToken() {
cachedToken = null
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,90 @@
package pl.lambada.songsync.domain.model.lyrics_providers.others

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class AppleSearchResponse(
val id: Long,
val songName: String,
data class AppleMusicSearchResponse(
val results: AppleMusicResults,
val resources: AppleMusicResources? = null
)

@Serializable
data class AppleMusicResults(
val songs: AppleMusicSongsResult? = null
)

@Serializable
data class AppleMusicSongsResult(
val data: List<AppleMusicSongData>
)

@Serializable
data class AppleMusicSongData(
val id: String,
val type: String,
val href: String
)

@Serializable
data class AppleMusicResources(
val songs: Map<String, AppleMusicSongDetail>? = null,
val artists: Map<String, AppleMusicArtistDetail>? = null
)

@Serializable
data class AppleMusicSongDetail(
val id: String,
val type: String,
val attributes: AppleMusicSongAttributes,
val relationships: AppleMusicRelationships? = null
)

@Serializable
data class AppleMusicSongAttributes(
val name: String,
val artistName: String,
val albumName: String,
val artwork: String,
val releaseDate: String?,
val duration: Int,
val isrc: String,
val artwork: AppleMusicArtwork,
val url: String,
val contentRating: String?,
val albumId: String
val isrc: String? = null,
val releaseDate: String? = null,
val durationInMillis: Long? = null,
val hasTimeSyncedLyrics: Boolean? = null,
val contentRating: String? = null
)

@Serializable
data class AppleMusicArtwork(
val url: String,
val width: Int? = null,
val height: Int? = null
)

@Serializable
data class AppleMusicRelationships(
val artists: AppleMusicArtistsRelation? = null
)

@Serializable
data class AppleMusicArtistsRelation(
val data: List<AppleMusicArtistData>
)

@Serializable
data class AppleMusicArtistData(
val id: String,
val type: String
)

@Serializable
data class AppleMusicArtistDetail(
val id: String,
val attributes: AppleMusicArtistAttributes
)

@Serializable
data class AppleMusicArtistAttributes(
val name: String,
val url: String? = null
)