Skip to content
Open
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
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.apple.AppleAPI
import pl.lambada.songsync.data.remote.lyrics_providers.others.LRCLibAPI
import pl.lambada.songsync.data.remote.lyrics_providers.others.VocaDBAPI
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.others.QQMusicAPI
Expand All @@ -29,6 +30,9 @@ class LyricsProviderService {
// LRCLib Track ID
private var lrcLibID = 0

// VocaDB Track ID
private var vocadbID = 0

// QQMusic request payload
private var qqPayload = ""

Expand Down Expand Up @@ -76,6 +80,10 @@ class LyricsProviderService {
lrcLibID = it?.lrcLibID ?: 0
} ?: throw NoTrackFoundException()

Providers.VOCADB -> VocaDBAPI().getSongInfo(query, offset).also {
vocadbID = it?.vocadbID ?: 0
} ?: throw NoTrackFoundException()

Providers.NETEASE -> NeteaseAPI().getSongInfo(query, offset).also {
neteaseID = it?.neteaseID ?: 0
} ?: throw NoTrackFoundException()
Expand Down Expand Up @@ -122,6 +130,12 @@ class LyricsProviderService {
neteaseID, includeTranslationNetEase, includeRomanizationNetEase
)

Providers.VOCADB -> VocaDBAPI().getSyncedLyrics(
vocadbID,
includeTranslation = includeTranslationNetEase,
includeRomanization = includeRomanizationNetEase
)

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

Providers.APPLE -> appleAPI.getSyncedLyrics(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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.VocaDBLyricsItem
import pl.lambada.songsync.domain.model.lyrics_providers.others.VocaDBSearchResponse
import pl.lambada.songsync.domain.model.lyrics_providers.others.VocaDBSongWithLyrics
import pl.lambada.songsync.domain.model.lyrics_providers.others.VocaDBArtistSearchResponse
import pl.lambada.songsync.util.networking.Ktor.client
import pl.lambada.songsync.util.networking.Ktor.json
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

class VocaDBAPI {
private val baseURL = "https://vocadb.net/api/"

suspend fun getSongInfo(query: SongInfo, offset: Int = 0): SongInfo? {
var offsetVar = offset
val artistIds = mutableListOf<Int>()

val artistQuery = withContext(Dispatchers.IO) {
URLEncoder.encode(query.artistName ?: "", StandardCharsets.UTF_8.toString())
}
if (artistQuery.isNotBlank()) {
val artistURL = baseURL + "artists?query=$artistQuery&allowBaseVoicebanks=true&maxResults=10&preferAccurateMatches=false"
val artistResponse = client.get(artistURL)
val artistBody = artistResponse.bodyAsText(Charsets.UTF_8)

if (artistResponse.status.value !in 200..299 || artistBody.isBlank()) {
return null
}

val artistResp = json.decodeFromString<VocaDBArtistSearchResponse>(artistBody)
artistResp.items.forEach { artistIds.add(it.id) }
}

val songsQuery = withContext(Dispatchers.IO) {
URLEncoder.encode("${query.songName}", StandardCharsets.UTF_8.toString())
}
for (id in artistIds) {
// i initially thought the artistID[] param was an or filter but instead it was an and filter
val songsURL = baseURL + "songs?query=$songsQuery&start=$offset&maxResults=10&artistId%5B%5D=$id"
val songsResponse = client.get(songsURL)
val songsBody = songsResponse.bodyAsText(Charsets.UTF_8)

if (songsResponse.status.value !in 200..299 || songsBody.isBlank()) {
return null
}

// this is an interesting way to implement offset value
// since we do more than one requests, we may not find it in our first request
// so we just lower it until we find it
// underflow shouldnt happen as if size is 0 offsetVar won't change and if size is
// smaller than offsetVar it will still stay positive
val songsResp = json.decodeFromString<VocaDBSearchResponse>(songsBody)
if (songsResp.items.size > 0 && songsResp.items.size > offsetVar) {
val item = songsResp.items[offsetVar]

return SongInfo(
songName = item.name,
artistName = item.artistString,
vocadbID = item.id
)
}
else {
offsetVar -= songsResp.items.size as Int
}
}

return null
}

suspend fun getSyncedLyrics(id: Int, includeTranslation: Boolean = false, includeRomanization: Boolean = false): String? {
if (id <= 0) return null
val lyricsURL = baseURL + "songs/$id?fields=Lyrics"
val lyricsResponse = client.get(lyricsURL)
val lyricsBody = lyricsResponse.bodyAsText(Charsets.UTF_8)

if (lyricsResponse.status.value !in 200..299 || lyricsBody.isBlank()) {
return null
}

val lyricsResp = json.decodeFromString<VocaDBSongWithLyrics>(lyricsBody)
if (lyricsResp.lyrics.isEmpty()) return null

val parts = mutableListOf<VocaDBLyricsItem>()

val original = lyricsResp.lyrics.firstOrNull { it.translationType?.equals("Original", true) == true }
original?.let { parts.add(it) }

if (includeRomanization) {
val romanization = lyricsResp.lyrics.firstOrNull { it.translationType?.contains("Romanized", true) == true }
romanization?.let { parts.add(it) }
}

if (includeTranslation) {
for (translation in lyricsResp.lyrics.filter { it.translationType?.contains("Translation", true) == true }) {
translation?.let { parts.add(it) }
}
}

if (parts.isEmpty()) {
return null
}

// i tried separating line by line to merge original and romanization but since they are all from different sources,
// the line endings are all over the place, which meant it wasn't very reliable, so i just scrapped that idea
return parts.mapNotNull { "=== ${it.translationType ?: "Lyrics"} (${it.cultureCodes.joinToString(", ")}) ===\n${it.value}" }.joinToString("\n\n\n")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ data class SongInfo(
var qqPayload: String? = null, // QQMusic-only
var neteaseID: Long? = null, // Netease-only
var appleID: Long? = null, // Apple-only
var vocadbID: Int? = null, // VocaDB-only
var musixmatchID: Long? = null, // Musixmatch-only
var hasSyncedLyrics: Boolean? = null, // Musixmatch-only
var hasUnsyncedLyrics: Boolean? = null, // Musixmatch-only
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package pl.lambada.songsync.domain.model.lyrics_providers.others

import kotlinx.serialization.Serializable

@Serializable
data class VocaDBSearchResponse(
val items: List<VocaDBSongItem> = emptyList(),
val term: String? = null,
val totalCount: Int = 0
)

@Serializable
data class VocaDBSongItem(
val id: Int,
val name: String,
val defaultName: String? = null,
val defaultNameLanguage: String? = null,
val artistString: String? = null,
val lengthSeconds: Int? = null
)

@Serializable
data class VocaDBSongWithLyrics(
val id: Int,
val name: String,
val artistString: String? = null,
val publishDate: String? = null,
val lyrics: List<VocaDBLyricsItem> = emptyList()
)

@Serializable
data class VocaDBLyricsItem(
val cultureCodes: List<String> = emptyList(),
val id: Int,
val source: String? = null,
val translationType: String? = null,
val url: String? = null,
val value: String? = null
)


@Serializable
data class VocaDBArtistSearchResponse(
val items: List<VocaDBArtistItem> = emptyList(),
val term: String? = null,
val totalCount: Int = 0
)

@Serializable
data class VocaDBArtistItem(
val id: Int,
val name: String,
val defaultName: String? = null,
val defaultNameLanguage: String? = null,
val artistType: String? = null
)
1 change: 1 addition & 0 deletions app/src/main/java/pl/lambada/songsync/util/LyricsUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ fun handleSecurityException(
enum class Providers(val displayName: String, val hasWordByWord: Boolean) {
APPLE("Apple Music", true),
LRCLIB("LRCLib", false),
VOCADB("VocaDB", false),
SPOTIFY("Spotify", false),
MUSIXMATCH("Musixmatch", false),
QQMUSIC("QQ Music", true),
Expand Down
Loading