Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

自动尝试所有 ani 服务器, close #1471 #1624

Merged
merged 2 commits into from
Feb 1, 2025
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
3 changes: 2 additions & 1 deletion app/shared/app-data/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 OpenAni and contributors.
* Copyright (C) 2024-2025 OpenAni and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license, which can be found at the following link.
Expand Down Expand Up @@ -67,6 +67,7 @@ kotlin {
}
sourceSets.commonTest.dependencies {
implementation(projects.utils.uiTesting)
implementation(libs.ktor.client.mock)
implementation(libs.turbine)
}
sourceSets.getByName("jvmTest").dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package me.him188.ani.app.data.network
import io.ktor.client.call.body
import io.ktor.client.request.get
import me.him188.ani.app.data.network.protocol.DanmakuGetResponse
import me.him188.ani.app.domain.foundation.ServerListFeatureConfig
import me.him188.ani.danmaku.api.AbstractDanmakuProvider
import me.him188.ani.danmaku.api.DanmakuFetchResult
import me.him188.ani.danmaku.api.DanmakuMatchInfo
Expand All @@ -22,7 +23,6 @@ import me.him188.ani.danmaku.api.DanmakuProviderFactory
import me.him188.ani.danmaku.api.DanmakuSearchRequest
import me.him188.ani.utils.ktor.ScopedHttpClient
import me.him188.ani.utils.logging.info
import kotlin.coroutines.CoroutineContext
import me.him188.ani.app.data.network.protocol.DanmakuLocation as ProtocolDanmakuLocation
import me.him188.ani.danmaku.api.Danmaku as ApiDanmaku
import me.him188.ani.danmaku.api.DanmakuLocation as ApiDanmakuLocation
Expand All @@ -40,9 +40,7 @@ class AniDanmakuProvider(
config: DanmakuProviderConfig,
private val client: ScopedHttpClient,
) : AbstractDanmakuProvider() {
// don't keep reference to `config` which will leak memory
private val sessionCoroutineContext: CoroutineContext = config.coroutineContext
private val baseUrl = AniBangumiSeverBaseUrls.getBaseUrl(config.useGlobal)
private val baseUrl = ServerListFeatureConfig.MAGIC_ANI_SERVER

companion object {
const val ID = "ani"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import me.him188.ani.app.data.network.protocol.BangumiLoginRequest
import me.him188.ani.app.data.network.protocol.BangumiLoginResponse
import me.him188.ani.app.data.network.protocol.DanmakuInfo
import me.him188.ani.app.data.network.protocol.DanmakuPostRequest
import me.him188.ani.app.domain.foundation.ServerListFeatureConfig.Companion.MAGIC_ANI_SERVER
import me.him188.ani.app.platform.currentAniBuildConfig
import me.him188.ani.app.ui.foundation.BackgroundScope
import me.him188.ani.app.ui.foundation.HasBackgroundScope
Expand Down Expand Up @@ -74,7 +75,7 @@ class AniDanmakuSenderImpl(
private val bangumiToken: Flow<String?>,
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
) : AniDanmakuSender, HasBackgroundScope by BackgroundScope(parentCoroutineContext) {
private fun getBaseUrl() = AniBangumiSeverBaseUrls.getBaseUrl(config.useGlobal)
private fun getBaseUrl() = MAGIC_ANI_SERVER

companion object {
private val logger = logger<AniDanmakuSenderImpl>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import me.him188.ani.app.data.network.protocol.DanmakuInfo
import me.him188.ani.app.data.repository.RepositoryException
import me.him188.ani.app.data.repository.user.SettingsRepository
import me.him188.ani.app.domain.foundation.HttpClientProvider
import me.him188.ani.app.domain.foundation.ScopedHttpClientUserAgent
import me.him188.ani.app.domain.foundation.get
import me.him188.ani.app.domain.session.OpaqueSession
import me.him188.ani.app.domain.session.SessionManager
Expand Down Expand Up @@ -117,7 +118,9 @@ class DanmakuManagerImpl(
@OptIn(OpaqueSession::class)
private val sender: Flow<AniDanmakuSender> = config.mapAutoClose { config ->
AniDanmakuSenderImpl(
httpClientProvider.get(),
httpClientProvider.get(
userAgent = ScopedHttpClientUserAgent.ANI,
),
config,
sessionManager.verifiedAccessToken, // TODO: Handle danmaku sender errors
backgroundScope.coroutineContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,12 @@ sealed class HttpClientProvider {
fun HttpClientProvider.get(
userAgent: ScopedHttpClientUserAgent = ScopedHttpClientUserAgent.ANI,
useBangumiToken: Boolean = false,
serverListConfig: ServerListFeatureConfig = ServerListFeatureConfig.Default,
): ScopedHttpClient = get(
setOf(
UserAgentFeature.withValue(userAgent),
UseBangumiTokenFeature.withValue(useBangumiToken),
ServerListFeature.withValue(serverListConfig),
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,26 @@ package me.him188.ani.app.domain.foundation

import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.call.HttpClientCall
import io.ktor.client.plugins.BrowserUserAgent
import io.ktor.client.plugins.HttpSend
import io.ktor.client.plugins.ResponseException
import io.ktor.client.plugins.Sender
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.plugins.auth.providers.bearer
import io.ktor.client.plugins.plugin
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.http.Url
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.io.IOException
import me.him188.ani.app.platform.getAniUserAgent
import me.him188.ani.utils.coroutines.Symbol
import me.him188.ani.utils.ktor.userAgent
import me.him188.ani.utils.logging.SilentLogger
import me.him188.ani.utils.logging.debug
import kotlin.coroutines.cancellation.CancellationException
import kotlin.jvm.JvmField


Expand Down Expand Up @@ -156,3 +165,106 @@ class UseBangumiTokenFeatureHandler(
// endregion


// region ServerListFeature
/**
* 自动替换请求中的 host, 使用多个 URL 重试.
*/
val ServerListFeature = ScopedHttpClientFeatureKey<ServerListFeatureConfig>("ServerList")

data class ServerListFeatureConfig(
val aniServerRules: AniServerRule?,
) {
data class AniServerRule(
/**
* 如果请求的 host 为其中任何一个, 则替换为 ani 服务器地址, 并依次重试.
*/
val hostMatches: Set<String>,
) {
init {
require(hostMatches.isNotEmpty()) { "hostMatches must not be empty" }
}
}

companion object {
const val MAGIC_ANI_SERVER_HOST = "MAGIC_ANI_SERVER"
const val MAGIC_ANI_SERVER = "https://$MAGIC_ANI_SERVER_HOST/"

val Default = ServerListFeatureConfig(
aniServerRules = AniServerRule(
hostMatches = setOf(MAGIC_ANI_SERVER_HOST),
),
)
}
}

data class ServerListFeatureHandler(
private val aniServerUrls: Flow<List<Url>>,
) : ScopedHttpClientFeatureHandler<ServerListFeatureConfig>(ServerListFeature) {
override fun applyToClient(client: HttpClient, value: ServerListFeatureConfig) {
client.plugin(HttpSend).intercept { request ->
value.aniServerRules?.let { rule ->
handleAniRule(rule, request)
}?.let {
return@intercept it
}

execute(request)
}
}

/**
* @return non-null if this rule is applied, which means further processing is NOT needed.
* Returns `null` if the request does not match this rule, so further handling is needed.
*/
private suspend fun Sender.handleAniRule(
rule: ServerListFeatureConfig.AniServerRule,
request: HttpRequestBuilder
): HttpClientCall? {
if (rule.hostMatches.isEmpty() || rule.hostMatches.none { request.url.host.startsWith(it) }) {
return null
}

val urls = aniServerUrls.first()
if (urls.isEmpty()) {
error("No server URL to try for ani server request")
}

var lastCall: HttpClientCall? = null
for (serverUrl in urls) {
// Apply server URL to request
request.url.protocol = serverUrl.protocol
request.url.host = serverUrl.host
request.url.port = serverUrl.port
request.url.encodedUser = serverUrl.encodedUser
request.url.encodedPassword = serverUrl.encodedPassword

logger.debug { "Trying server $serverUrl for request ${request.url}" }
val thisCall = try {
execute(request) // if `expectSuccess` is true, this will throw on failure, otherwise return the call
} catch (e: CancellationException) {
throw e // don't prevent cancellation
} catch (e: ResponseException) {
continue // try next server
} catch (e: IOException) {
continue // try next server
}
lastCall = thisCall

if (thisCall.response.status.value in 100..399) {
// success
return thisCall
} else {
// failed
continue // try next server
}
}

// all servers failed, return the last failure (for exception and logging)
return lastCall ?: throw IOException(
"All servers failed for request ${request.url}. Tried: " +
"\n${urls.joinToString("\n")}",
)
}

private val logger = SilentLogger//logger<ServerListFeatureHandler>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

package me.him188.ani.app.domain.session

import me.him188.ani.app.platform.currentAniBuildConfig
import me.him188.ani.app.domain.foundation.ServerListFeatureConfig
import me.him188.ani.client.apis.BangumiOAuthAniApi
import me.him188.ani.client.apis.ScheduleAniApi
import me.him188.ani.client.apis.SubjectRelationsAniApi
Expand All @@ -26,6 +26,5 @@ class AniApiProvider(
val oauthApi = ApiInvoker(client) { BangumiOAuthAniApi(baseurl, it) }
val subjectRelationsApi = ApiInvoker(client) { SubjectRelationsAniApi(baseurl, it) }

@PublishedApi
internal val baseurl = currentAniBuildConfig.aniAuthServerUrl
private inline val baseurl get() = ServerListFeatureConfig.MAGIC_ANI_SERVER
}
Loading
Loading