mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
refactor(network): central HttpClientFactory; drop Ktor CIO client
On iOS the CIO client engine competed with Darwin as the default engine and
has no TLS on Kotlin/Native, so HTTPS requests (e.g. plugin discovery) failed.
Remove ktor-client-cio and build every client through a Koin-provided
HttpClientFactory that owns one client on the platform default engine
(Darwin on iOS, OkHttp on Android/desktop). Consumers use the shared
default/json clients or derive their own via create { }, sharing the engine.
This commit is contained in:
parent
09df4b494c
commit
ff7a1714ff
@ -19,9 +19,13 @@
|
||||
|
||||
## Dependencies
|
||||
- `gradle/libs.versions.toml` is the single source of truth for all version pins and library declarations.
|
||||
- Kotlin: `2.3.0`, JVM target: `11` (compile/target compatibility in both `composeApp/build.gradle.kts` and `plugin_interfaces/build.gradle.kts`).
|
||||
- Kotlin: `2.4.21`, JVM target: `11` (compile/target compatibility in both `composeApp/build.gradle.kts` and `plugin_interfaces/build.gradle.kts`).
|
||||
- **compose-webview (desktop) dispatcher hijack:** `dev.nucleusframework:composewebview` transitively pulls `nucleus.decorated-window-tao`, which registers a `TaoMainDispatcherFactory` (`loadPriority = 100`) via `META-INF/services` that hijacks `Dispatchers.Main` away from the Swing EDT. This breaks lifecycle's `enforceMainThreadIfNeeded` (window fails with "Method addObserver must be called on the main thread"). The app overrides it with `SwingMainDispatcherFactory` (`composeApp/src/jvmMain/.../core/coroutines/`, `loadPriority = Int.MAX_VALUE`) which pins `Dispatchers.Main` back to the Swing EDT. Keep that factory + its `META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory` resource; the long-term fix is in the webview (make the Tao dispatcher opt-in or lower its priority).
|
||||
|
||||
## Networking
|
||||
- Never call `HttpClient(...)` directly. Inject `HttpClientFactory` (`commonMain/.../core/network/HttpClientFactory.kt`, a Koin single) and use `.default` / `.json` (shared, never close them) or `.create { ... }` for extra plugins (caller owns and may close it). It owns the one engine-backed client, so the platform default engine is used everywhere (Darwin on iOS, OkHttp on Android/desktop).
|
||||
- Do not add `ktor-client-cio`: on iOS it competes with Darwin as the default engine and has no TLS on Kotlin/Native, so every HTTPS request fails.
|
||||
|
||||
## GraalVM native image (desktop)
|
||||
- Enabled via `graalvm { }` inside `nucleus.application` in `composeApp/build.gradle.kts`. Nucleus downloads GraalVM CE itself (or uses `$GRAALVM_HOME`). Linux needs `gcc` + `patchelf`.
|
||||
- Build: `:composeApp:createGraalvmNativeDistributable` (output `composeApp/build/compose/binaries/main/graalvm-app/`), installers via `packageGraalvm{Deb,Rpm,AppImage,Dmg,Msi,Exe}`. A build takes ~10 min and uses all cores and ~15 GB RAM.
|
||||
|
||||
@ -199,7 +199,6 @@ kotlin {
|
||||
implementation(libs.ktor.client.logging)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.client.serialization.kotlinx.json)
|
||||
implementation(libs.ktor.client.cio)
|
||||
implementation(libs.ktor.client.websockets)
|
||||
implementation(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.cio)
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
|
||||
package org.spotube.spotube.core.di
|
||||
|
||||
import org.koin.core.module.dsl.onClose
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerQueueRepository
|
||||
@ -108,6 +110,7 @@ expect val platformModules: Module
|
||||
val sharedModules = module {
|
||||
includes(navigationModule)
|
||||
singleOf(::Database)
|
||||
single { HttpClientFactory() } withOptions { onClose { it?.close() } }
|
||||
|
||||
singleOf(::WebViewController)
|
||||
|
||||
@ -164,7 +167,7 @@ val sharedModules = module {
|
||||
viewModelOf(::WelcomeViewModel)
|
||||
|
||||
// Updates
|
||||
single { AppUpdateService(get()) } withOptions { createdAtStart() }
|
||||
single { AppUpdateService(get(), get()) } withOptions { createdAtStart() }
|
||||
single { PluginUpdateService(get(), get()) } withOptions { createdAtStart() }
|
||||
|
||||
// Downloads
|
||||
@ -273,7 +276,7 @@ val sharedModules = module {
|
||||
createdAtStart()
|
||||
}
|
||||
single { RemoteControlHandler(get(), get(), get(), get()) }
|
||||
single { RemoteControlClient() }
|
||||
single { RemoteControlClient(get()) }
|
||||
singleOf(::DeviceDiscoveryService)
|
||||
single { RemoteControlService(get(), get(), get()) } withOptions {
|
||||
createdAtStart()
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.spotube.spotube.core.network
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* The single place the app builds Ktor [HttpClient]s.
|
||||
*
|
||||
* It owns one client on the platform's default engine (Darwin on iOS, OkHttp on
|
||||
* Android/desktop), so every client shares that engine's connection pool and
|
||||
* no code path can accidentally pick an engine that doesn't work on a platform.
|
||||
*
|
||||
* - [default] and [json] are shared: use them directly and never close them.
|
||||
* - [create] derives a client with extra configuration (timeouts, WebSockets,
|
||||
* default request, …). It reuses the shared engine; the caller owns the
|
||||
* returned client and may close it without affecting anyone else.
|
||||
*/
|
||||
class HttpClientFactory : AutoCloseable {
|
||||
private val root = HttpClient()
|
||||
|
||||
/** Plain client with no plugins installed. */
|
||||
val default: HttpClient get() = root
|
||||
|
||||
/** Client that (de)serializes JSON bodies via [HttpJson]. */
|
||||
val json: HttpClient by lazy {
|
||||
create { install(ContentNegotiation) { json(HttpJson) } }
|
||||
}
|
||||
|
||||
fun create(block: HttpClientConfig<*>.() -> Unit): HttpClient = root.config(block)
|
||||
|
||||
override fun close() {
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** Lenient JSON used for HTTP bodies: remote APIs add fields over time. */
|
||||
val HttpJson = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
}
|
||||
@ -17,8 +17,8 @@
|
||||
|
||||
package org.spotube.spotube.core.remote
|
||||
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import co.touchlab.kermit.Logger
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.url
|
||||
@ -49,7 +49,7 @@ import spotube.composeapp.generated.resources.devices_error_send_failed
|
||||
* WebSocket client for controlling a remote Spotube instance.
|
||||
* Connects to the remote device's `/control` endpoint and sends commands.
|
||||
*/
|
||||
class RemoteControlClient {
|
||||
class RemoteControlClient(httpClientFactory: HttpClientFactory) {
|
||||
private val logger = Logger.withTag("RemoteControlClient")
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
@ -59,7 +59,7 @@ class RemoteControlClient {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
private val httpClient = HttpClient {
|
||||
private val httpClient = httpClientFactory.create {
|
||||
install(WebSockets)
|
||||
install(HttpTimeout) {
|
||||
connectTimeoutMillis = 10_000
|
||||
|
||||
@ -17,16 +17,15 @@
|
||||
|
||||
package org.spotube.spotube.core.server
|
||||
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import org.spotube.spotube.core.di.injectLogger
|
||||
import org.spotube.spotube.core.remote.RemoteControlHandler
|
||||
import org.spotube.spotube.modules.settings.SettingsViewModel
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.cio.CIO
|
||||
import io.ktor.server.engine.EmbeddedServer
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.response.respondText
|
||||
@ -60,11 +59,12 @@ class LocalServer(
|
||||
private val streamingUrlRepository: StreamingUrlRepository,
|
||||
private val audioPlayerQueue: AudioPlayerQueue,
|
||||
private val cacheManager: CacheManager,
|
||||
private val remoteControlHandler: RemoteControlHandler) : KoinComponent {
|
||||
private val remoteControlHandler: RemoteControlHandler,
|
||||
httpClientFactory: HttpClientFactory) : KoinComponent {
|
||||
|
||||
val logger by injectLogger<LocalServer>()
|
||||
private val httpClient = HttpClient {
|
||||
// A stalled upstream connection must not wedge the CIO dispatcher thread
|
||||
private val httpClient = httpClientFactory.create {
|
||||
// A stalled upstream connection must not wedge an engine thread
|
||||
// forever. Only the connect phase is bounded — the proxy streams long
|
||||
// audio bodies, so request/socket timeouts would cut playback short.
|
||||
install(HttpTimeout) {
|
||||
|
||||
@ -17,12 +17,13 @@
|
||||
|
||||
package org.spotube.spotube.core.tools.user_agents
|
||||
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.spotube.spotube.PlatformType
|
||||
import org.spotube.spotube.core.paths.Paths
|
||||
import org.spotube.spotube.getPlatform
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.isSuccess
|
||||
@ -53,7 +54,8 @@ import kotlin.time.Clock
|
||||
* [random] is intentionally synchronous and never blocks on the network: it serves
|
||||
* whatever is cached (even stale) and refreshes in the background when needed.
|
||||
*/
|
||||
object UserAgents {
|
||||
object UserAgents : KoinComponent {
|
||||
private val httpClientFactory: HttpClientFactory by inject()
|
||||
private const val SOURCE_URL =
|
||||
"https://cdn.jsdelivr.net/npm/top-user-agents@latest/src/index.json"
|
||||
private const val CACHE_FILE_NAME = "user_agents_cache.json"
|
||||
@ -111,14 +113,9 @@ object UserAgents {
|
||||
private fun isValidAgent(ua: String) = ua.startsWith("Mozilla/")
|
||||
|
||||
private suspend fun fetchRemote(): List<String> {
|
||||
val client = HttpClient(CIO)
|
||||
try {
|
||||
val res = client.get(SOURCE_URL)
|
||||
val res = httpClientFactory.default.get(SOURCE_URL)
|
||||
if (!res.status.isSuccess()) error("Unexpected status ${res.status}")
|
||||
return json.decodeFromString(ListSerializer(String.serializer()), res.bodyAsText())
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cachePath() = (Paths().getApplicationCacheDirPath() + "/" + CACHE_FILE_NAME).toPath()
|
||||
|
||||
@ -17,11 +17,12 @@
|
||||
|
||||
package org.spotube.spotube.core.zipline.host_apis
|
||||
|
||||
import org.koin.core.component.inject
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import org.spotube.plugin_interfaces.host_apis.HttpClientAPI
|
||||
import org.spotube.plugin_interfaces.host_apis.HttpMethod
|
||||
import org.spotube.plugin_interfaces.host_apis.HttpResponse
|
||||
import org.spotube.spotube.core.di.injectLogger
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.request
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
@ -30,7 +31,8 @@ import org.koin.core.component.KoinComponent
|
||||
|
||||
class RealHttpClientAPI : HttpClientAPI, KoinComponent {
|
||||
val logger by injectLogger<RealHttpClientAPI>()
|
||||
val httpClient = HttpClient()
|
||||
private val httpClientFactory: HttpClientFactory by inject()
|
||||
private val httpClient get() = httpClientFactory.default
|
||||
|
||||
override suspend fun request(
|
||||
method: HttpMethod,
|
||||
|
||||
@ -17,11 +17,14 @@
|
||||
|
||||
package org.spotube.spotube.core.zipline.plugin_apis.lrclib
|
||||
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.spotube.spotube.core.network.HttpJson
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import org.spotube.plugin_interfaces.plugin_apis.lyrics.LyricsAPI
|
||||
import org.spotube.plugin_interfaces.plugin_apis.lyrics.LyricsLine
|
||||
import org.spotube.plugin_interfaces.plugin_apis.lyrics.LyricsResponse
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.defaultRequest
|
||||
@ -36,7 +39,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
data class LRCLibResponse(
|
||||
@ -47,14 +49,16 @@ data class LRCLibResponse(
|
||||
val plainLyrics: String?,
|
||||
val syncedLyrics: String?)
|
||||
|
||||
class RealLRCLibLyricsAPI : LyricsAPI {
|
||||
private val httpClient = HttpClient {
|
||||
class RealLRCLibLyricsAPI : LyricsAPI, KoinComponent {
|
||||
private val httpClientFactory: HttpClientFactory by inject()
|
||||
private val httpClient by lazy {
|
||||
httpClientFactory.create {
|
||||
install(Logging) {
|
||||
level = LogLevel.ALL
|
||||
logger = Logger.SIMPLE
|
||||
}
|
||||
install(ContentNegotiation) {
|
||||
json(Json { ignoreUnknownKeys = true })
|
||||
json(HttpJson)
|
||||
}
|
||||
defaultRequest {
|
||||
url("https://lrclib.net/api/get")
|
||||
@ -62,6 +66,7 @@ class RealLRCLibLyricsAPI : LyricsAPI {
|
||||
userAgent("Spotube/v6.0.0 LRCLib Lyric Plugin")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getLRCLibLyrics(track: MetadataTrack): LRCLibResponse? =
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
@ -17,15 +17,15 @@
|
||||
|
||||
package org.spotube.spotube.core.zipline.plugin_apis.newpipe_yt
|
||||
|
||||
import org.koin.core.component.inject
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import org.spotube.spotube.core.tools.user_agents.UserAgents
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import org.spotube.spotube.core.di.injectLogger
|
||||
import org.koin.core.component.KoinComponent
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.request.post
|
||||
@ -54,15 +54,8 @@ class ISRCProviders : KoinComponent {
|
||||
.lowercase()
|
||||
}
|
||||
|
||||
val client = HttpClient() {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
prettyPrint = true
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
}
|
||||
}
|
||||
private val httpClientFactory: HttpClientFactory by inject()
|
||||
private val client get() = httpClientFactory.json
|
||||
|
||||
private fun MetadataTrack.describe() =
|
||||
"id=$id title='$title' artists=${artists.map { it.name }} album='${album?.title}' " +
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
package org.spotube.spotube.modules.downloads
|
||||
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import org.spotube.spotube.core.paths.Paths
|
||||
@ -26,7 +27,6 @@ import org.spotube.spotube.core.server.StreamingUrlRepository
|
||||
import org.spotube.spotube.core.server.urlSummary
|
||||
import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
||||
import org.spotube.spotube.modules.settings.SettingsRepository
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.HttpTimeout
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.head
|
||||
@ -88,7 +88,8 @@ sealed interface DownloadStatus {
|
||||
class DownloadManager(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val paths: Paths,
|
||||
private val streamingUrlRepository: StreamingUrlRepository
|
||||
private val streamingUrlRepository: StreamingUrlRepository,
|
||||
private val httpClientFactory: HttpClientFactory
|
||||
) {
|
||||
companion object {
|
||||
const val MAX_CONCURRENT_DOWNLOADS = 4
|
||||
@ -101,7 +102,7 @@ class DownloadManager(
|
||||
private val fileSystem = FileSystem.SYSTEM
|
||||
private val queueMutex = Mutex()
|
||||
|
||||
private val httpClient = HttpClient {
|
||||
private val httpClient = httpClientFactory.create {
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 300_000
|
||||
connectTimeoutMillis = 30_000
|
||||
|
||||
@ -17,9 +17,9 @@
|
||||
|
||||
package org.spotube.spotube.modules.plugin
|
||||
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.headers
|
||||
import io.ktor.client.request.parameter
|
||||
@ -27,7 +27,6 @@ import io.ktor.http.append
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
@Serializable
|
||||
data class GitHubRepoSearchResponse(
|
||||
@ -64,12 +63,8 @@ data class GitHubAsset(
|
||||
val name: String,
|
||||
@SerialName("browser_download_url") val browserDownloadUrl: String)
|
||||
|
||||
class GitHubPluginRepository {
|
||||
private val httpClient = HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json { ignoreUnknownKeys = true })
|
||||
}
|
||||
}
|
||||
/** [httpClient] must have JSON content negotiation installed (see [HttpClientFactory.json]). */
|
||||
class GitHubPluginRepository(private val httpClient: HttpClient) {
|
||||
|
||||
suspend fun searchSpotubePlugins(page: Int = 1, perPage: Int = 30): GitHubRepoSearchResponse {
|
||||
return httpClient.get("https://api.github.com/search/repositories") {
|
||||
@ -113,8 +108,4 @@ class GitHubPluginRepository {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
httpClient.close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
package org.spotube.spotube.modules.plugin
|
||||
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import org.spotube.spotube.core.db.Database
|
||||
import org.spotube.spotube.core.db.DatabaseKeys
|
||||
import org.spotube.spotube.core.di.injectLogger
|
||||
@ -24,7 +25,6 @@ import org.spotube.spotube.core.paths.Paths
|
||||
import org.spotube.spotube.core.zipline.BuiltInPluginService
|
||||
import org.spotube.spotube.core.zipline.PluginService
|
||||
import org.spotube.spotube.core.zipline.ZiplinePluginService
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.utils.io.readRemaining
|
||||
@ -113,7 +113,8 @@ interface ScrobblePluginSource {
|
||||
|
||||
class PluginManager(
|
||||
val database: Database,
|
||||
val paths: Paths) : KoinComponent, PluginProvider, AudioPluginSource, ScrobblePluginSource {
|
||||
val paths: Paths,
|
||||
private val httpClientFactory: HttpClientFactory) : KoinComponent, PluginProvider, AudioPluginSource, ScrobblePluginSource {
|
||||
private val logger by injectLogger<PluginManager>()
|
||||
private val pluginExceptionHandler = CoroutineExceptionHandler { _, exception ->
|
||||
logger.e(exception) { "Plugin runtime threw an unhandled exception. Intercepted safely." }
|
||||
@ -121,7 +122,7 @@ class PluginManager(
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate + pluginExceptionHandler)
|
||||
private val pluginsDir = "${paths.getApplicationDataDirPath()}/plugins".toPath()
|
||||
val pluginsDirPath: Path get() = pluginsDir
|
||||
private val httpClient = HttpClient()
|
||||
private val httpClient get() = httpClientFactory.default
|
||||
|
||||
enum class InstallPromptKind {
|
||||
INSTALL,
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
package org.spotube.spotube.modules.plugin
|
||||
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import org.spotube.spotube.core.di.injectLogger
|
||||
@ -141,10 +142,11 @@ private data class PluginScreenState(
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class PluginViewModel(
|
||||
private val pluginManager: PluginManager,
|
||||
private val webViewController: WebViewController) : ViewModel(), KoinComponent {
|
||||
private val webViewController: WebViewController,
|
||||
httpClientFactory: HttpClientFactory) : ViewModel(), KoinComponent {
|
||||
|
||||
private val logger by injectLogger<PluginViewModel>()
|
||||
private val gitHubRepo = GitHubPluginRepository()
|
||||
private val gitHubRepo = GitHubPluginRepository(httpClientFactory.json)
|
||||
|
||||
private val _screenState = MutableStateFlow(PluginScreenState())
|
||||
private val _allRepos = mutableListOf<GitHubRepo>()
|
||||
@ -658,8 +660,4 @@ class PluginViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
gitHubRepo.close()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,13 +17,11 @@
|
||||
|
||||
package org.spotube.spotube.modules.update
|
||||
|
||||
import org.spotube.spotube.core.network.HttpClientFactory
|
||||
import co.touchlab.kermit.Logger
|
||||
import org.spotube.spotube.AppBuildInfo
|
||||
import org.spotube.spotube.modules.settings.SettingsRepository
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.http.HttpHeaders
|
||||
@ -37,7 +35,6 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import net.swiftzer.semver.SemVer
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
@ -52,14 +49,11 @@ data class AvailableAppUpdate(
|
||||
* process, independent of any screen's lifecycle.
|
||||
*/
|
||||
class AppUpdateService(
|
||||
private val settingsRepository: SettingsRepository) : KoinComponent, AutoCloseable {
|
||||
private val settingsRepository: SettingsRepository,
|
||||
httpClientFactory: HttpClientFactory) : KoinComponent {
|
||||
private val logger = Logger.withTag("AppUpdateService")
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val httpClient = HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json(Json { ignoreUnknownKeys = true })
|
||||
}
|
||||
}
|
||||
private val httpClient = httpClientFactory.json
|
||||
|
||||
private val _availableUpdate = MutableStateFlow<AvailableAppUpdate?>(null)
|
||||
val availableUpdate: StateFlow<AvailableAppUpdate?> = _availableUpdate.asStateFlow()
|
||||
@ -117,10 +111,6 @@ class AppUpdateService(
|
||||
private fun parseVersion(version: String): SemVer? = runCatching {
|
||||
SemVer.parse(version)
|
||||
}.getOrNull()
|
||||
|
||||
override fun close() {
|
||||
httpClient.close()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
Loading…
Reference in New Issue
Block a user