This commit is contained in:
2024-02-27 22:09:30 +03:00
parent bfa3231823
commit 38a3141d43
479 changed files with 36348 additions and 10142 deletions

1
providers/kugou/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,25 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.android.lint)
}
dependencies {
implementation(projects.providers.common)
implementation(libs.kotlin.coroutines)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.client.encoding)
implementation(libs.ktor.client.serialization)
implementation(libs.ktor.serialization.json)
detektPlugins(libs.detekt.compose)
detektPlugins(libs.detekt.formatting)
}
kotlin {
jvmToolchain(libs.versions.jvm.get().toInt())
}

View File

@@ -0,0 +1,184 @@
package it.hamy.kugou
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.plugins.BrowserUserAgent
import io.ktor.client.plugins.compression.ContentEncoding
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.http.ContentType
import io.ktor.http.encodeURLParameter
import io.ktor.serialization.kotlinx.json.json
import io.ktor.util.decodeBase64String
import it.hamy.extensions.runCatchingCancellable
import it.hamy.kugou.models.DownloadLyricsResponse
import it.hamy.kugou.models.SearchLyricsResponse
import it.hamy.kugou.models.SearchSongResponse
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
object KuGou {
@OptIn(ExperimentalSerializationApi::class)
private val client by lazy {
HttpClient(OkHttp) {
BrowserUserAgent()
expectSuccess = true
install(ContentNegotiation) {
val feature = Json {
ignoreUnknownKeys = true
explicitNulls = false
encodeDefaults = true
}
json(feature)
json(feature, ContentType.Text.Html)
json(feature, ContentType.Text.Plain)
}
install(ContentEncoding) {
gzip()
deflate()
}
defaultRequest {
url("https://krcs.kugou.com")
}
}
}
suspend fun lyrics(artist: String, title: String, duration: Long) = runCatchingCancellable {
val keyword = keyword(artist, title)
val infoByKeyword = searchSong(keyword)
if (infoByKeyword.isNotEmpty()) {
var tolerance = 0
while (tolerance <= 5) {
for (info in infoByKeyword) {
if (info.duration >= duration - tolerance && info.duration <= duration + tolerance) {
searchLyricsByHash(info.hash).firstOrNull()?.let { candidate ->
return@runCatchingCancellable downloadLyrics(
candidate.id,
candidate.accessKey
).normalize()
}
}
}
tolerance++
}
}
searchLyricsByKeyword(keyword).firstOrNull()?.let { candidate ->
return@runCatchingCancellable downloadLyrics(
candidate.id,
candidate.accessKey
).normalize()
}
null
}
private suspend fun downloadLyrics(id: Long, accessKey: String) = client.get("/download") {
parameter("ver", 1)
parameter("man", "yes")
parameter("client", "pc")
parameter("fmt", "lrc")
parameter("id", id)
parameter("accesskey", accessKey)
}.body<DownloadLyricsResponse>().content.decodeBase64String().let(::Lyrics)
private suspend fun searchLyricsByHash(hash: String) = client.get("/search") {
parameter("ver", 1)
parameter("man", "yes")
parameter("client", "mobi")
parameter("hash", hash)
}.body<SearchLyricsResponse>().candidates
private suspend fun searchLyricsByKeyword(keyword: String) = client.get("/search") {
parameter("ver", 1)
parameter("man", "yes")
parameter("client", "mobi")
url.encodedParameters.append("keyword", keyword.encodeURLParameter(spaceToPlus = false))
}.body<SearchLyricsResponse>().candidates
private suspend fun searchSong(keyword: String) =
client.get("https://mobileservice.kugou.com/api/v3/search/song") {
parameter("version", 9108)
parameter("plat", 0)
parameter("pagesize", 8)
parameter("showtype", 0)
url.encodedParameters.append("keyword", keyword.encodeURLParameter(spaceToPlus = false))
}.body<SearchSongResponse>().data.info
private fun keyword(artist: String, title: String): String {
val (newTitle, featuring) = title.extract(" (feat. ", ')')
val newArtist = (if (featuring.isEmpty()) artist else "$artist, $featuring")
.replace(", ", "")
.replace(" & ", "")
.replace(".", "")
return "$newArtist - $newTitle"
}
@Suppress("ReturnCount")
private fun String.extract(startDelimiter: String, endDelimiter: Char): Pair<String, String> {
val startIndex = indexOf(startDelimiter).takeIf { it != -1 } ?: return this to ""
val endIndex = indexOf(endDelimiter, startIndex).takeIf { it != -1 } ?: return this to ""
return removeRange(startIndex, endIndex + 1) to substring(startIndex + startDelimiter.length, endIndex)
}
@JvmInline
value class Lyrics(val value: String) {
@Suppress("CyclomaticComplexMethod")
fun normalize(): Lyrics {
var toDrop = 0
var maybeToDrop = 0
val text = value.replace("\r\n", "\n").trim()
for (line in text.lineSequence()) when {
line.startsWith("[ti:") ||
line.startsWith("[ar:") ||
line.startsWith("[al:") ||
line.startsWith("[by:") ||
line.startsWith("[hash:") ||
line.startsWith("[sign:") ||
line.startsWith("[qq:") ||
line.startsWith("[total:") ||
line.startsWith("[offset:") ||
line.startsWith("[id:") ||
line.containsAt("]Written by", 9) ||
line.containsAt("]Lyrics by", 9) ||
line.containsAt("]Composed by", 9) ||
line.containsAt("]Producer", 9) ||
line.containsAt("]作曲 : ", 9) ||
line.containsAt("]作词 : ", 9) -> {
toDrop += line.length + 1 + maybeToDrop
maybeToDrop = 0
}
maybeToDrop == 0 -> maybeToDrop = line.length + 1
else -> {
maybeToDrop = 0
break
}
}
return Lyrics(text.drop(toDrop + maybeToDrop).removeHtmlEntities())
}
private fun String.containsAt(charSequence: CharSequence, startIndex: Int) =
regionMatches(startIndex, charSequence, 0, charSequence.length)
private fun String.removeHtmlEntities() = replace("&apos;", "'")
}
}

View File

@@ -0,0 +1,8 @@
package it.hamy.kugou.models
import kotlinx.serialization.Serializable
@Serializable
internal class DownloadLyricsResponse(
val content: String
)

View File

@@ -0,0 +1,16 @@
package it.hamy.kugou.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
internal class SearchLyricsResponse(
val candidates: List<Candidate>
) {
@Serializable
internal class Candidate(
val id: Long,
@SerialName("accesskey") val accessKey: String,
val duration: Long
)
}

View File

@@ -0,0 +1,19 @@
package it.hamy.kugou.models
import kotlinx.serialization.Serializable
@Serializable
internal data class SearchSongResponse(
val data: Data
) {
@Serializable
internal data class Data(
val info: List<Info>
) {
@Serializable
internal data class Info(
val duration: Long,
val hash: String
)
}
}