Remove dependency injection from core module and data module from presentation-widget module

Includes side effects:
- No longer need to restart app for user agent string change to take effect
- parseAs extension function requires a Json instance in the calling context, which doesn't necessarily need to be the default one provided by Injekt
This commit is contained in:
arkon 2023-02-20 19:02:38 -05:00
parent 10d7349506
commit 93523ef50b
34 changed files with 576 additions and 433 deletions

View file

@ -283,6 +283,7 @@ tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> { withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-Xcontext-receivers",
"-opt-in=coil.annotation.ExperimentalCoilApi", "-opt-in=coil.annotation.ExperimentalCoilApi",
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi", "-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi", "-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",

View file

@ -222,7 +222,7 @@ object SettingsAdvancedScreen : SearchableSettings {
Preference.PreferenceItem.TextPreference( Preference.PreferenceItem.TextPreference(
title = stringResource(R.string.pref_clear_cookies), title = stringResource(R.string.pref_clear_cookies),
onClick = { onClick = {
networkHelper.cookieManager.removeAll() networkHelper.cookieJar.removeAll()
context.toast(R.string.cookies_cleared) context.toast(R.string.cookies_cleared)
}, },
), ),
@ -280,7 +280,6 @@ object SettingsAdvancedScreen : SearchableSettings {
context.toast(R.string.error_user_agent_string_invalid) context.toast(R.string.error_user_agent_string_invalid)
return@EditTextPreference false return@EditTextPreference false
} }
context.toast(R.string.requires_app_restart)
true true
}, },
), ),

View file

@ -118,7 +118,7 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { ChapterCache(app) } addSingletonFactory { ChapterCache(app) }
addSingletonFactory { CoverCache(app) } addSingletonFactory { CoverCache(app) }
addSingletonFactory { NetworkHelper(app) } addSingletonFactory { NetworkHelper(app, get()) }
addSingletonFactory { JavaScriptEngine(app) } addSingletonFactory { JavaScriptEngine(app) }
addSingletonFactory { SourceManager(app, get(), get()) } addSingletonFactory { SourceManager(app, get(), get()) }

View file

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit import eu.kanade.tachiyomi.network.interceptor.rateLimit
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
@ -24,11 +25,14 @@ import kotlinx.serialization.json.putJsonObject
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.util.Calendar import java.util.Calendar
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val json: Json by injectLazy()
private val authClient = client.newBuilder() private val authClient = client.newBuilder()
.addInterceptor(interceptor) .addInterceptor(interceptor)
.rateLimit(permits = 85, period = 1, unit = TimeUnit.MINUTES) .rateLimit(permits = 85, period = 1, unit = TimeUnit.MINUTES)
@ -53,6 +57,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("status", track.toAnilistStatus()) put("status", track.toAnilistStatus())
} }
} }
with(json) {
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, apiUrl,
@ -68,6 +73,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
} }
} }
}
suspend fun updateLibManga(track: Track): Track { suspend fun updateLibManga(track: Track): Track {
return withIOContext { return withIOContext {
@ -137,6 +143,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("query", search) put("query", search)
} }
} }
with(json) {
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, apiUrl,
@ -154,6 +161,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
} }
} }
}
suspend fun findLibManga(track: Track, userid: Int): Track? { suspend fun findLibManga(track: Track, userid: Int): Track? {
return withIOContext { return withIOContext {
@ -205,6 +213,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("manga_id", track.media_id) put("manga_id", track.media_id)
} }
} }
with(json) {
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, apiUrl,
@ -222,6 +231,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
} }
} }
}
suspend fun getLibManga(track: Track, userid: Int): Track { suspend fun getLibManga(track: Track, userid: Int): Track {
return findLibManga(track, userid) ?: throw Exception("Could not find manga") return findLibManga(track, userid) ?: throw Exception("Could not find manga")
@ -247,6 +257,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
val payload = buildJsonObject { val payload = buildJsonObject {
put("query", query) put("query", query)
} }
with(json) {
authClient.newCall( authClient.newCall(
POST( POST(
apiUrl, apiUrl,
@ -265,6 +276,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
} }
} }
} }
}
private fun jsonToALManga(struct: JsonObject): ALManga { private fun jsonToALManga(struct: JsonObject): ALManga {
return ALManga( return ALManga(

View file

@ -118,12 +118,14 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
suspend fun findLibManga(track: Track): Track? { suspend fun findLibManga(track: Track): Track? {
return withIOContext { return withIOContext {
with(json) {
authClient.newCall(GET("$apiUrl/subject/${track.media_id}")) authClient.newCall(GET("$apiUrl/subject/${track.media_id}"))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
.let { jsonToSearch(it) } .let { jsonToSearch(it) }
} }
} }
}
suspend fun statusLibManga(track: Track): Track? { suspend fun statusLibManga(track: Track): Track? {
return withIOContext { return withIOContext {
@ -155,11 +157,13 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
suspend fun accessToken(code: String): OAuth { suspend fun accessToken(code: String): OAuth {
return withIOContext { return withIOContext {
with(json) {
client.newCall(accessTokenRequest(code)) client.newCall(accessTokenRequest(code))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
} }
}
private fun accessTokenRequest(code: String) = POST( private fun accessTokenRequest(code: String) = POST(
oauthUrl, oauthUrl,
@ -181,7 +185,6 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
private const val loginUrl = "https://bgm.tv/oauth/authorize" private const val loginUrl = "https://bgm.tv/oauth/authorize"
private const val redirectUrl = "tachiyomi://bangumi-auth" private const val redirectUrl = "tachiyomi://bangumi-auth"
private const val baseMangaUrl = "$apiUrl/mangas"
fun authUrl(): Uri = fun authUrl(): Uri =
loginUrl.toUri().buildUpon() loginUrl.toUri().buildUpon()

View file

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import logcat.LogPriority import logcat.LogPriority
import okhttp3.Dns import okhttp3.Dns
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -13,11 +14,14 @@ import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor) { class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor) {
private val json: Json by injectLazy()
private val authClient = client.newBuilder() private val authClient = client.newBuilder()
.dns(Dns.SYSTEM) .dns(Dns.SYSTEM)
.addInterceptor(interceptor) .addInterceptor(interceptor)
@ -39,6 +43,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()), body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()),
) )
try { try {
with(json) {
client.newCall(request).execute().use { client.newCall(request).execute().use {
when (it.code) { when (it.code) {
200 -> return it.parseAs<AuthenticationDto>().token 200 -> return it.parseAs<AuthenticationDto>().token
@ -53,6 +58,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
else -> {} else -> {}
} }
} }
}
// Not sure which one to catch // Not sure which one to catch
} catch (e: SocketTimeoutException) { } catch (e: SocketTimeoutException) {
logcat(LogPriority.WARN) { logcat(LogPriority.WARN) {
@ -86,9 +92,11 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
private fun getTotalChapters(url: String): Int { private fun getTotalChapters(url: String): Int {
val requestUrl = getApiVolumesUrl(url) val requestUrl = getApiVolumesUrl(url)
try { try {
val listVolumeDto = authClient.newCall(GET(requestUrl)) val listVolumeDto = with(json) {
authClient.newCall(GET(requestUrl))
.execute() .execute()
.parseAs<List<VolumeDto>>() .parseAs<List<VolumeDto>>()
}
var volumeNumber = 0 var volumeNumber = 0
var maxChapterNumber = 0 var maxChapterNumber = 0
for (volume in listVolumeDto) { for (volume in listVolumeDto) {
@ -110,6 +118,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
val serieId = getIdFromUrl(url) val serieId = getIdFromUrl(url)
val requestUrl = "${getApiFromUrl(url)}/Tachiyomi/latest-chapter?seriesId=$serieId" val requestUrl = "${getApiFromUrl(url)}/Tachiyomi/latest-chapter?seriesId=$serieId"
try { try {
with(json) {
authClient.newCall(GET(requestUrl)).execute().use { authClient.newCall(GET(requestUrl)).execute().use {
if (it.code == 200) { if (it.code == 200) {
return it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat() return it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat()
@ -118,6 +127,7 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
return 0F return 0F
} }
} }
}
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Exception getting latest chapter read. Could not get itemRequest: $requestUrl" } logcat(LogPriority.WARN, e) { "Exception getting latest chapter read. Could not get itemRequest: $requestUrl" }
throw e throw e
@ -127,9 +137,11 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
suspend fun getTrackSearch(url: String): TrackSearch = withIOContext { suspend fun getTrackSearch(url: String): TrackSearch = withIOContext {
try { try {
val serieDto: SeriesDto = authClient.newCall(GET(url)) val serieDto: SeriesDto = with(json) {
authClient.newCall(GET(url))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
}
val track = serieDto.toTrack() val track = serieDto.toTrack()
track.apply { track.apply {

View file

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonArray
@ -24,11 +25,14 @@ import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) { class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
private val json: Json by injectLazy()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun addLibManga(track: Track, userId: String): Track { suspend fun addLibManga(track: Track, userId: String): Track {
@ -57,6 +61,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
with(json) {
authClient.newCall( authClient.newCall(
POST( POST(
"${baseUrl}library-entries", "${baseUrl}library-entries",
@ -64,7 +69,8 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
"Content-Type", "Content-Type",
"application/vnd.api+json", "application/vnd.api+json",
), ),
body = data.toString().toRequestBody("application/vnd.api+json".toMediaType()), body = data.toString()
.toRequestBody("application/vnd.api+json".toMediaType()),
), ),
) )
.awaitSuccess() .awaitSuccess()
@ -75,6 +81,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
} }
}
suspend fun updateLibManga(track: Track): Track { suspend fun updateLibManga(track: Track): Track {
return withIOContext { return withIOContext {
@ -92,6 +99,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
with(json) {
authClient.newCall( authClient.newCall(
Request.Builder() Request.Builder()
.url("${baseUrl}library-entries/${track.media_id}") .url("${baseUrl}library-entries/${track.media_id}")
@ -101,7 +109,9 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
"application/vnd.api+json", "application/vnd.api+json",
), ),
) )
.patch(data.toString().toRequestBody("application/vnd.api+json".toMediaType())) .patch(
data.toString().toRequestBody("application/vnd.api+json".toMediaType()),
)
.build(), .build(),
) )
.awaitSuccess() .awaitSuccess()
@ -111,9 +121,11 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
} }
}
suspend fun search(query: String): List<TrackSearch> { suspend fun search(query: String): List<TrackSearch> {
return withIOContext { return withIOContext {
with(json) {
authClient.newCall(GET(algoliaKeyUrl)) authClient.newCall(GET(algoliaKeyUrl))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
@ -123,6 +135,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
} }
}
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> { private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
return withIOContext { return withIOContext {
@ -130,6 +143,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$algoliaFilter") put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$algoliaFilter")
} }
with(json) {
client.newCall( client.newCall(
POST( POST(
algoliaUrl, algoliaUrl,
@ -152,6 +166,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
} }
}
suspend fun findLibManga(track: Track, userId: String): Track? { suspend fun findLibManga(track: Track, userId: String): Track? {
return withIOContext { return withIOContext {
@ -159,6 +174,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId") .encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
.appendQueryParameter("include", "manga") .appendQueryParameter("include", "manga")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
@ -173,6 +189,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
} }
}
suspend fun getLibManga(track: Track): Track { suspend fun getLibManga(track: Track): Track {
return withIOContext { return withIOContext {
@ -180,6 +197,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.encodedQuery("filter[id]=${track.media_id}") .encodedQuery("filter[id]=${track.media_id}")
.appendQueryParameter("include", "manga") .appendQueryParameter("include", "manga")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
@ -194,6 +212,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
} }
}
suspend fun login(username: String, password: String): OAuth { suspend fun login(username: String, password: String): OAuth {
return withIOContext { return withIOContext {
@ -204,17 +223,20 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
.add("client_id", clientId) .add("client_id", clientId)
.add("client_secret", clientSecret) .add("client_secret", clientSecret)
.build() .build()
with(json) {
client.newCall(POST(loginUrl, body = formBody)) client.newCall(POST(loginUrl, body = formBody))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
} }
}
suspend fun getCurrentUser(): String { suspend fun getCurrentUser(): String {
return withIOContext { return withIOContext {
val url = "${baseUrl}users".toUri().buildUpon() val url = "${baseUrl}users".toUri().buildUpon()
.encodedQuery("filter[self]=true") .encodedQuery("filter[self]=true")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
@ -223,6 +245,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
} }
} }
} }
}
companion object { companion object {
private const val clientId = private const val clientId =

View file

@ -26,7 +26,8 @@ class KomgaApi(private val client: OkHttpClient) {
suspend fun getTrackSearch(url: String): TrackSearch = suspend fun getTrackSearch(url: String): TrackSearch =
withIOContext { withIOContext {
try { try {
val track = if (url.contains(READLIST_API)) { val track = with(json) {
if (url.contains(READLIST_API)) {
client.newCall(GET(url)) client.newCall(GET(url))
.awaitSuccess() .awaitSuccess()
.parseAs<ReadListDto>() .parseAs<ReadListDto>()
@ -37,16 +38,19 @@ class KomgaApi(private val client: OkHttpClient) {
.parseAs<SeriesDto>() .parseAs<SeriesDto>()
.toTrack() .toTrack()
} }
}
val progress = client val progress = client
.newCall(GET("${url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi")) .newCall(GET("${url.replace("/api/v1/series/", "/api/v2/series/")}/read-progress/tachiyomi"))
.awaitSuccess().let { .awaitSuccess().let {
with(json) {
if (url.contains("/api/v1/series/")) { if (url.contains("/api/v1/series/")) {
it.parseAs<ReadProgressV2Dto>() it.parseAs<ReadProgressV2Dto>()
} else { } else {
it.parseAs<ReadProgressDto>().toV2() it.parseAs<ReadProgressDto>().toV2()
} }
} }
}
track.apply { track.apply {
cover_url = "$url/thumbnail" cover_url = "$url/thumbnail"

View file

@ -47,7 +47,7 @@ class MangaUpdatesApi(
} }
suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> { suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> {
val listItem = val listItem = with(json) {
authClient.newCall( authClient.newCall(
GET( GET(
url = "$baseUrl/v1/lists/series/${track.media_id}", url = "$baseUrl/v1/lists/series/${track.media_id}",
@ -55,6 +55,7 @@ class MangaUpdatesApi(
) )
.awaitSuccess() .awaitSuccess()
.parseAs<ListItem>() .parseAs<ListItem>()
}
val rating = getSeriesRating(track) val rating = getSeriesRating(track)
@ -111,6 +112,7 @@ class MangaUpdatesApi(
suspend fun getSeriesRating(track: Track): Rating? { suspend fun getSeriesRating(track: Track): Rating? {
return try { return try {
with(json) {
authClient.newCall( authClient.newCall(
GET( GET(
url = "$baseUrl/v1/series/${track.media_id}/rating", url = "$baseUrl/v1/series/${track.media_id}/rating",
@ -118,6 +120,7 @@ class MangaUpdatesApi(
) )
.awaitSuccess() .awaitSuccess()
.parseAs<Rating>() .parseAs<Rating>()
}
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@ -156,7 +159,8 @@ class MangaUpdatesApi(
}, },
) )
} }
return client.newCall( return with(json) {
client.newCall(
POST( POST(
url = "$baseUrl/v1/series/search", url = "$baseUrl/v1/series/search",
body = body.toString().toRequestBody(contentType), body = body.toString().toRequestBody(contentType),
@ -171,13 +175,15 @@ class MangaUpdatesApi(
} }
.orEmpty() .orEmpty()
} }
}
suspend fun authenticate(username: String, password: String): Context? { suspend fun authenticate(username: String, password: String): Context? {
val body = buildJsonObject { val body = buildJsonObject {
put("username", username) put("username", username)
put("password", password) put("password", password)
} }
return client.newCall( return with(json) {
client.newCall(
PUT( PUT(
url = "$baseUrl/v1/account/login", url = "$baseUrl/v1/account/login",
body = body.toString().toRequestBody(contentType), body = body.toString().toRequestBody(contentType),
@ -194,4 +200,5 @@ class MangaUpdatesApi(
} }
} }
} }
}
} }

View file

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.PkceUtil import eu.kanade.tachiyomi.util.PkceUtil
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.contentOrNull
@ -27,11 +28,14 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) { class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
private val json: Json by injectLazy()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun getAccessToken(authCode: String): OAuth { suspend fun getAccessToken(authCode: String): OAuth {
@ -42,11 +46,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.add("code_verifier", codeVerifier) .add("code_verifier", codeVerifier)
.add("grant_type", "authorization_code") .add("grant_type", "authorization_code")
.build() .build()
with(json) {
client.newCall(POST("$baseOAuthUrl/token", body = formBody)) client.newCall(POST("$baseOAuthUrl/token", body = formBody))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
} }
}
suspend fun getCurrentUser(): String { suspend fun getCurrentUser(): String {
return withIOContext { return withIOContext {
@ -54,12 +60,14 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url("$baseApiUrl/users/@me") .url("$baseApiUrl/users/@me")
.get() .get()
.build() .build()
with(json) {
authClient.newCall(request) authClient.newCall(request)
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
.let { it["name"]!!.jsonPrimitive.content } .let { it["name"]!!.jsonPrimitive.content }
} }
} }
}
suspend fun search(query: String): List<TrackSearch> { suspend fun search(query: String): List<TrackSearch> {
return withIOContext { return withIOContext {
@ -68,6 +76,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendQueryParameter("q", query.take(64)) .appendQueryParameter("q", query.take(64))
.appendQueryParameter("nsfw", "true") .appendQueryParameter("nsfw", "true")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
@ -83,6 +92,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
} }
} }
} }
}
suspend fun getMangaDetails(id: Int): TrackSearch { suspend fun getMangaDetails(id: Int): TrackSearch {
return withIOContext { return withIOContext {
@ -90,6 +100,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath(id.toString()) .appendPath(id.toString())
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date") .appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
@ -100,10 +111,14 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
title = obj["title"]!!.jsonPrimitive.content title = obj["title"]!!.jsonPrimitive.content
summary = obj["synopsis"]?.jsonPrimitive?.content ?: "" summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
cover_url = obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content ?: "" cover_url =
obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content
?: ""
tracking_url = "https://myanimelist.net/manga/$media_id" tracking_url = "https://myanimelist.net/manga/$media_id"
publishing_status = obj["status"]!!.jsonPrimitive.content.replace("_", " ") publishing_status =
publishing_type = obj["media_type"]!!.jsonPrimitive.content.replace("_", " ") obj["status"]!!.jsonPrimitive.content.replace("_", " ")
publishing_type =
obj["media_type"]!!.jsonPrimitive.content.replace("_", " ")
start_date = try { start_date = try {
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
outputDf.format(obj["start_date"]!!) outputDf.format(obj["start_date"]!!)
@ -114,6 +129,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
} }
} }
} }
}
suspend fun updateItem(track: Track): Track { suspend fun updateItem(track: Track): Track {
return withIOContext { return withIOContext {
@ -133,12 +149,14 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url(mangaUrl(track.media_id).toString()) .url(mangaUrl(track.media_id).toString())
.put(formBodyBuilder.build()) .put(formBodyBuilder.build())
.build() .build()
with(json) {
authClient.newCall(request) authClient.newCall(request)
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
.let { parseMangaItem(it, track) } .let { parseMangaItem(it, track) }
} }
} }
}
suspend fun findListItem(track: Track): Track? { suspend fun findListItem(track: Track): Track? {
return withIOContext { return withIOContext {
@ -146,6 +164,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}") .appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
.build() .build()
with(json) {
authClient.newCall(GET(uri.toString())) authClient.newCall(GET(uri.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
@ -157,6 +176,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
} }
} }
} }
}
suspend fun findListItems(query: String, offset: Int = 0): List<TrackSearch> { suspend fun findListItems(query: String, offset: Int = 0): List<TrackSearch> {
return withIOContext { return withIOContext {
@ -198,11 +218,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.url(urlBuilder.build().toString()) .url(urlBuilder.build().toString())
.get() .get()
.build() .build()
with(json) {
authClient.newCall(request) authClient.newCall(request)
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
} }
}
private fun parseMangaItem(response: JsonObject, track: Track): Track { private fun parseMangaItem(response: JsonObject, track: Track): Track {
val obj = response.jsonObject val obj = response.jsonObject

View file

@ -1,12 +1,16 @@
package eu.kanade.tachiyomi.data.track.myanimelist package eu.kanade.tachiyomi.data.track.myanimelist
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor { class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor {
private val json: Json by injectLazy()
private var oauth: OAuth? = null private var oauth: OAuth? = null
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
@ -69,7 +73,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!)) val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
if (oauthResponse.isSuccessful) { if (oauthResponse.isSuccessful) {
oauthResponse.parseAs<OAuth>() with(json) { oauthResponse.parseAs<OAuth>() }
} else { } else {
oauthResponse.close() oauthResponse.close()
null null

View file

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
@ -24,9 +25,12 @@ import okhttp3.FormBody
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import uy.kohesive.injekt.injectLazy
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) { class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
private val json: Json by injectLazy()
private val authClient = client.newBuilder().addInterceptor(interceptor).build() private val authClient = client.newBuilder().addInterceptor(interceptor).build()
suspend fun addLibManga(track: Track, user_id: String): Track { suspend fun addLibManga(track: Track, user_id: String): Track {
@ -60,6 +64,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
.appendQueryParameter("search", search) .appendQueryParameter("search", search)
.appendQueryParameter("limit", "20") .appendQueryParameter("limit", "20")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonArray>() .parseAs<JsonArray>()
@ -70,6 +75,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
} }
} }
}
private fun jsonToSearch(obj: JsonObject): TrackSearch { private fun jsonToSearch(obj: JsonObject): TrackSearch {
return TrackSearch.create(TrackManager.SHIKIMORI).apply { return TrackSearch.create(TrackManager.SHIKIMORI).apply {
@ -81,7 +87,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content tracking_url = baseUrl + obj["url"]!!.jsonPrimitive.content
publishing_status = obj["status"]!!.jsonPrimitive.content publishing_status = obj["status"]!!.jsonPrimitive.content
publishing_type = obj["kind"]!!.jsonPrimitive.content publishing_type = obj["kind"]!!.jsonPrimitive.content
start_date = obj.get("aired_on")!!.jsonPrimitive.contentOrNull ?: "" start_date = obj["aired_on"]!!.jsonPrimitive.contentOrNull ?: ""
} }
} }
@ -102,15 +108,18 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
val urlMangas = "$apiUrl/mangas".toUri().buildUpon() val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
.build() .build()
val mangas = authClient.newCall(GET(urlMangas.toString())) val mangas = with(json) {
authClient.newCall(GET(urlMangas.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
}
val url = "$apiUrl/v2/user_rates".toUri().buildUpon() val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id) .appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString()) .appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga") .appendQueryParameter("target_type", "Manga")
.build() .build()
with(json) {
authClient.newCall(GET(url.toString())) authClient.newCall(GET(url.toString()))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonArray>() .parseAs<JsonArray>()
@ -125,23 +134,28 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
} }
} }
}
suspend fun getCurrentUser(): Int { suspend fun getCurrentUser(): Int {
return authClient.newCall(GET("$apiUrl/users/whoami")) return with(json) {
authClient.newCall(GET("$apiUrl/users/whoami"))
.awaitSuccess() .awaitSuccess()
.parseAs<JsonObject>() .parseAs<JsonObject>()
.let { .let {
it["id"]!!.jsonPrimitive.int it["id"]!!.jsonPrimitive.int
} }
} }
}
suspend fun accessToken(code: String): OAuth { suspend fun accessToken(code: String): OAuth {
return withIOContext { return withIOContext {
with(json) {
client.newCall(accessTokenRequest(code)) client.newCall(accessTokenRequest(code))
.awaitSuccess() .awaitSuccess()
.parseAs() .parseAs()
} }
} }
}
private fun accessTokenRequest(code: String) = POST( private fun accessTokenRequest(code: String) = POST(
oauthUrl, oauthUrl,
@ -164,14 +178,8 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
private const val loginUrl = "$baseUrl/oauth/authorize" private const val loginUrl = "$baseUrl/oauth/authorize"
private const val redirectUrl = "tachiyomi://shikimori-auth" private const val redirectUrl = "tachiyomi://shikimori-auth"
private const val baseMangaUrl = "$apiUrl/mangas"
fun mangaUrl(remoteId: Int): String { fun authUrl() = loginUrl.toUri().buildUpon()
return "$baseMangaUrl/$remoteId"
}
fun authUrl() =
loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", redirectUrl) .appendQueryParameter("redirect_uri", redirectUrl)
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")

View file

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.PUT
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json
import okhttp3.Credentials import okhttp3.Credentials
import okhttp3.Dns import okhttp3.Dns
import okhttp3.FormBody import okhttp3.FormBody
@ -23,11 +24,15 @@ import java.nio.charset.Charset
import java.security.MessageDigest import java.security.MessageDigest
class TachideskApi { class TachideskApi {
private val network by injectLazy<NetworkHelper>()
private val network: NetworkHelper by injectLazy()
private val json: Json by injectLazy()
val client: OkHttpClient = val client: OkHttpClient =
network.client.newBuilder() network.client.newBuilder()
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing .dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
.build() .build()
fun headersBuilder(): Headers.Builder = Headers.Builder().apply { fun headersBuilder(): Headers.Builder = Headers.Builder().apply {
if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) { if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) {
val credentials = Credentials.basic(baseLogin, basePassword) val credentials = Credentials.basic(baseLogin, basePassword)
@ -50,7 +55,11 @@ class TachideskApi {
trackUrl trackUrl
} }
val manga = client.newCall(GET("$url/full", headers)).awaitSuccess().parseAs<MangaDataClass>() val manga = with(json) {
client.newCall(GET("$url/full", headers))
.awaitSuccess()
.parseAs<MangaDataClass>()
}
TrackSearch.create(TrackManager.SUWAYOMI).apply { TrackSearch.create(TrackManager.SUWAYOMI).apply {
title = manga.title title = manga.title
@ -70,7 +79,11 @@ class TachideskApi {
suspend fun updateProgress(track: Track): Track { suspend fun updateProgress(track: Track): Track {
val url = track.tracking_url val url = track.tracking_url
val chapters = client.newCall(GET("$url/chapters", headers)).awaitSuccess().parseAs<List<ChapterDataClass>>() val chapters = with(json) {
client.newCall(GET("$url/chapters", headers))
.awaitSuccess()
.parseAs<List<ChapterDataClass>>()
}
val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index val lastChapterIndex = chapters.first { it.chapterNumber == track.last_chapter_read }.index
client.newCall( client.newCall(

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
import kotlinx.serialization.json.Json
import tachiyomi.core.preference.Preference import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
@ -18,6 +19,8 @@ class AppUpdateChecker {
private val networkService: NetworkHelper by injectLazy() private val networkService: NetworkHelper by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy() private val preferenceStore: PreferenceStore by injectLazy()
private val json: Json by injectLazy()
private val lastAppCheck: Preference<Long> by lazy { private val lastAppCheck: Preference<Long> by lazy {
preferenceStore.getLong("last_app_check", 0) preferenceStore.getLong("last_app_check", 0)
} }
@ -29,7 +32,8 @@ class AppUpdateChecker {
} }
return withIOContext { return withIOContext {
val result = networkService.client val result = with(json) {
networkService.client
.newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest")) .newCall(GET("https://api.github.com/repos/$GITHUB_REPO/releases/latest"))
.awaitSuccess() .awaitSuccess()
.parseAs<GithubRelease>() .parseAs<GithubRelease>()
@ -47,6 +51,7 @@ class AppUpdateChecker {
AppUpdateResult.NoNewUpdate AppUpdateResult.NoNewUpdate
} }
} }
}
when (result) { when (result) {
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release) is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)

View file

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.preference.Preference import tachiyomi.core.preference.Preference
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
@ -24,10 +25,12 @@ internal class ExtensionGithubApi {
private val networkService: NetworkHelper by injectLazy() private val networkService: NetworkHelper by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy() private val preferenceStore: PreferenceStore by injectLazy()
private val extensionManager: ExtensionManager by injectLazy()
private val json: Json by injectLazy()
private val lastExtCheck: Preference<Long> by lazy { private val lastExtCheck: Preference<Long> by lazy {
preferenceStore.getLong("last_ext_check", 0) preferenceStore.getLong("last_ext_check", 0)
} }
private val extensionManager: ExtensionManager by injectLazy()
private var requiresFallbackSource = false private var requiresFallbackSource = false
@ -53,9 +56,11 @@ internal class ExtensionGithubApi {
.awaitSuccess() .awaitSuccess()
} }
val extensions = response val extensions = with(json) {
response
.parseAs<List<ExtensionJsonObject>>() .parseAs<List<ExtensionJsonObject>>()
.toExtensions() .toExtensions()
}
// Sanity check - a small number of extensions probably means something broke // Sanity check - a small number of extensions probably means something broke
// with the repo generator // with the repo generator

View file

@ -120,7 +120,7 @@ class ExtensionDetailsScreenModel(
val cleared = urls.sumOf { val cleared = urls.sumOf {
try { try {
network.cookieManager.remove(it.toHttpUrl()) network.cookieJar.remove(it.toHttpUrl())
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to clear cookies for $it" } logcat(LogPriority.ERROR, e) { "Failed to clear cookies for $it" }
0 0

View file

@ -92,7 +92,7 @@ class WebViewActivity : BaseActivity() {
} }
private fun clearCookies(url: String) { private fun clearCookies(url: String) {
val cleared = network.cookieManager.remove(url.toHttpUrl()) val cleared = network.cookieJar.remove(url.toHttpUrl())
logcat { "Cleared $cleared cookies for: $url" } logcat { "Cleared $cleared cookies for: $url" }
} }

View file

@ -47,7 +47,7 @@ class WebViewScreenModel(
} }
fun clearCookies(url: String) { fun clearCookies(url: String) {
val cleared = network.cookieManager.remove(url.toHttpUrl()) val cleared = network.cookieJar.remove(url.toHttpUrl())
logcat { "Cleared $cleared cookies for: $url" } logcat { "Cleared $cleared cookies for: $url" }
} }
} }

View file

@ -6,6 +6,13 @@ plugins {
android { android {
namespace = "eu.kanade.tachiyomi.core" namespace = "eu.kanade.tachiyomi.core"
kotlinOptions {
freeCompilerArgs += listOf(
"-Xcontext-receivers",
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
}
} }
dependencies { dependencies {
@ -24,12 +31,8 @@ dependencies {
api(kotlinx.serialization.json) api(kotlinx.serialization.json)
api(kotlinx.serialization.json.okio) api(kotlinx.serialization.json.okio)
api(libs.injekt.core)
api(libs.preferencektx) api(libs.preferencektx)
implementation(androidx.corektx)
// JavaScript engine // JavaScript engine
implementation(libs.bundles.js.engine) implementation(libs.bundles.js.engine)
} }

View file

@ -6,26 +6,30 @@ import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkHelper(context: Context) { class NetworkHelper(
context: Context,
private val preferences: NetworkPreferences by injectLazy() private val preferences: NetworkPreferences,
) {
private val cacheDir = File(context.cacheDir, "network_cache") private val cacheDir = File(context.cacheDir, "network_cache")
private val cacheSize = 5L * 1024 * 1024 // 5 MiB private val cacheSize = 5L * 1024 * 1024 // 5 MiB
val cookieManager = AndroidCookieJar() val cookieJar = AndroidCookieJar()
private val userAgentInterceptor by lazy { UserAgentInterceptor() } private val userAgentInterceptor by lazy {
private val cloudflareInterceptor by lazy { CloudflareInterceptor(context) } UserAgentInterceptor(::defaultUserAgentProvider)
}
private val cloudflareInterceptor by lazy {
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider)
}
private val baseClientBuilder: OkHttpClient.Builder private val baseClientBuilder: OkHttpClient.Builder
get() { get() {
val builder = OkHttpClient.Builder() val builder = OkHttpClient.Builder()
.cookieJar(cookieManager) .cookieJar(cookieJar)
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES) .callTimeout(2, TimeUnit.MINUTES)
@ -65,7 +69,5 @@ class NetworkHelper(context: Context) {
.build() .build()
} }
val defaultUserAgent by lazy { fun defaultUserAgentProvider() = preferences.defaultUserAgent().get().trim()
preferences.defaultUserAgent().get().trim()
}
} }

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.network
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.DeserializationStrategy import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.okio.decodeFromBufferedSource import kotlinx.serialization.json.okio.decodeFromBufferedSource
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
@ -16,8 +15,6 @@ import okhttp3.Response
import rx.Observable import rx.Observable
import rx.Producer import rx.Producer
import rx.Subscription import rx.Subscription
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@ -131,14 +128,18 @@ fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: Progre
return progressClient.newCall(request) return progressClient.newCall(request)
} }
context(Json)
inline fun <reified T> Response.parseAs(): T { inline fun <reified T> Response.parseAs(): T {
return decodeFromJsonResponse(serializer(), this) return decodeFromJsonResponse(serializer(), this)
} }
@OptIn(ExperimentalSerializationApi::class) context(Json)
fun <T> decodeFromJsonResponse(deserializer: DeserializationStrategy<T>, response: Response): T { fun <T> decodeFromJsonResponse(
deserializer: DeserializationStrategy<T>,
response: Response,
): T {
return response.body.source().use { return response.body.source().use {
Injekt.get<Json>().decodeFromBufferedSource(deserializer, it) decodeFromBufferedSource(deserializer, it)
} }
} }

View file

@ -6,7 +6,7 @@ import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.core.R import eu.kanade.tachiyomi.core.R
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.AndroidCookieJar
import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.isOutdated import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -15,16 +15,17 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(context) { class CloudflareInterceptor(
private val context: Context,
private val cookieManager: AndroidCookieJar,
defaultUserAgentProvider: () -> String,
) : WebViewInterceptor(context, defaultUserAgentProvider) {
private val executor = ContextCompat.getMainExecutor(context) private val executor = ContextCompat.getMainExecutor(context)
private val networkHelper: NetworkHelper by injectLazy()
override fun shouldIntercept(response: Response): Boolean { override fun shouldIntercept(response: Response): Boolean {
// Check if Cloudflare anti-bot is on // Check if Cloudflare anti-bot is on
return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
@ -33,8 +34,8 @@ class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(c
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response { override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
try { try {
response.close() response.close()
networkHelper.cookieManager.remove(request.url, COOKIE_NAMES, 0) cookieManager.remove(request.url, COOKIE_NAMES, 0)
val oldCookie = networkHelper.cookieManager.get(request.url) val oldCookie = cookieManager.get(request.url)
.firstOrNull { it.name == "cf_clearance" } .firstOrNull { it.name == "cf_clearance" }
resolveWithWebView(request, oldCookie) resolveWithWebView(request, oldCookie)
@ -70,7 +71,7 @@ class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(c
webview?.webViewClient = object : WebViewClientCompat() { webview?.webViewClient = object : WebViewClientCompat() {
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
fun isCloudFlareBypassed(): Boolean { fun isCloudFlareBypassed(): Boolean {
return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl()) return cookieManager.get(origRequestUrl.toHttpUrl())
.firstOrNull { it.name == "cf_clearance" } .firstOrNull { it.name == "cf_clearance" }
.let { it != null && it != oldCookie } .let { it != null && it != oldCookie }
} }

View file

@ -1,13 +1,11 @@
package eu.kanade.tachiyomi.network.interceptor package eu.kanade.tachiyomi.network.interceptor
import eu.kanade.tachiyomi.network.NetworkHelper
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class UserAgentInterceptor : Interceptor { class UserAgentInterceptor(
private val defaultUserAgentProvider: () -> String,
private val networkHelper: NetworkHelper by injectLazy() ) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
@ -16,7 +14,7 @@ class UserAgentInterceptor : Interceptor {
val newRequest = originalRequest val newRequest = originalRequest
.newBuilder() .newBuilder()
.removeHeader("User-Agent") .removeHeader("User-Agent")
.addHeader("User-Agent", networkHelper.defaultUserAgent) .addHeader("User-Agent", defaultUserAgentProvider())
.build() .build()
chain.proceed(newRequest) chain.proceed(newRequest)
} else { } else {

View file

@ -6,7 +6,6 @@ import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import eu.kanade.tachiyomi.core.R import eu.kanade.tachiyomi.core.R
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
@ -16,14 +15,14 @@ import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import tachiyomi.core.util.lang.launchUI import tachiyomi.core.util.lang.launchUI
import uy.kohesive.injekt.injectLazy
import java.util.Locale import java.util.Locale
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
abstract class WebViewInterceptor(private val context: Context) : Interceptor { abstract class WebViewInterceptor(
private val context: Context,
private val networkHelper: NetworkHelper by injectLazy() private val defaultUserAgentProvider: () -> String,
) : Interceptor {
/** /**
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid * When this is called, it initializes the WebView if it wasn't already. We use this to avoid
@ -85,7 +84,7 @@ abstract class WebViewInterceptor(private val context: Context) : Interceptor {
return WebView(context).apply { return WebView(context).apply {
setDefaultSettings() setDefaultSettings()
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
settings.userAgentString = request.header("User-Agent") ?: networkHelper.defaultUserAgent settings.userAgentString = request.header("User-Agent") ?: defaultUserAgentProvider()
} }
} }
} }

View file

@ -23,6 +23,7 @@ dependencies {
implementation(project(":source-api")) implementation(project(":source-api"))
implementation(project(":domain")) implementation(project(":domain"))
implementation(project(":core")) implementation(project(":core"))
api(libs.sqldelight.android.driver) api(libs.sqldelight.android.driver)
api(libs.sqldelight.coroutines) api(libs.sqldelight.coroutines)
api(libs.sqldelight.android.paging) api(libs.sqldelight.android.paging)

View file

@ -6,12 +6,32 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.domain.updates.repository.UpdatesRepository import tachiyomi.domain.updates.repository.UpdatesRepository
class UpdatesRepositoryImpl( class UpdatesRepositoryImpl(
val databaseHandler: DatabaseHandler, private val databaseHandler: DatabaseHandler,
) : UpdatesRepository { ) : UpdatesRepository {
override suspend fun awaitWithRead(read: Boolean, after: Long): List<UpdatesWithRelations> {
return databaseHandler.awaitList {
updatesViewQueries.getUpdatesByReadStatus(
read = read,
after = after,
mapper = updateWithRelationMapper,
)
}
}
override fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>> { override fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>> {
return databaseHandler.subscribeToList { return databaseHandler.subscribeToList {
updatesViewQueries.updates(after, updateWithRelationMapper) updatesViewQueries.updates(after, updateWithRelationMapper)
} }
} }
override fun subscribeWithRead(read: Boolean, after: Long): Flow<List<UpdatesWithRelations>> {
return databaseHandler.subscribeToList {
updatesViewQueries.getUpdatesByReadStatus(
read = read,
after = after,
mapper = updateWithRelationMapper,
)
}
}
} }

View file

@ -13,11 +13,11 @@ android {
} }
dependencies { dependencies {
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
implementation(project(":source-api")) implementation(project(":source-api"))
implementation(project(":core")) implementation(project(":core"))
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
testImplementation(libs.junit) testImplementation(libs.junit)
} }

View file

@ -9,9 +9,17 @@ class GetUpdates(
private val repository: UpdatesRepository, private val repository: UpdatesRepository,
) { ) {
suspend fun await(read: Boolean, after: Long): List<UpdatesWithRelations> {
return repository.awaitWithRead(read, after)
}
fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> = subscribe(calendar.time.time) fun subscribe(calendar: Calendar): Flow<List<UpdatesWithRelations>> = subscribe(calendar.time.time)
fun subscribe(after: Long): Flow<List<UpdatesWithRelations>> { fun subscribe(after: Long): Flow<List<UpdatesWithRelations>> {
return repository.subscribeAll(after) return repository.subscribeAll(after)
} }
fun subscribe(read: Boolean, after: Long): Flow<List<UpdatesWithRelations>> {
return repository.subscribeWithRead(read, after)
}
} }

View file

@ -5,5 +5,9 @@ import tachiyomi.domain.updates.model.UpdatesWithRelations
interface UpdatesRepository { interface UpdatesRepository {
suspend fun awaitWithRead(read: Boolean, after: Long): List<UpdatesWithRelations>
fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>> fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>>
fun subscribeWithRead(read: Boolean, after: Long): Flow<List<UpdatesWithRelations>>
} }

View file

@ -22,11 +22,11 @@ android {
dependencies { dependencies {
implementation(project(":core")) implementation(project(":core"))
implementation(project(":data"))
implementation(project(":domain")) implementation(project(":domain"))
implementation(project(":presentation-core")) implementation(project(":presentation-core"))
implementation(androidx.glance) implementation(androidx.glance)
implementation(libs.coil.core) implementation(libs.coil.core)
api(libs.injekt.core)
} }

View file

@ -7,21 +7,17 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import tachiyomi.data.DatabaseHandler import tachiyomi.domain.updates.interactor.GetUpdates
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class TachiyomiWidgetManager( class TachiyomiWidgetManager(
private val database: DatabaseHandler = Injekt.get(), private val getUpdates: GetUpdates,
) { ) {
fun Context.init(scope: LifecycleCoroutineScope) { fun Context.init(scope: LifecycleCoroutineScope) {
database.subscribeToList { getUpdates.subscribe(
updatesViewQueries.getUpdatesByReadStatus(
read = false, read = false,
after = UpdatesGridGlanceWidget.DateLimit.timeInMillis, after = UpdatesGridGlanceWidget.DateLimit.timeInMillis,
) )
}
.drop(1) .drop(1)
.distinctUntilChanged() .distinctUntilChanged()
.onEach { .onEach {

View file

@ -25,13 +25,13 @@ import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.dpToPx
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import tachiyomi.core.util.lang.launchIO import tachiyomi.core.util.lang.launchIO
import tachiyomi.data.DatabaseHandler
import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.updates.interactor.GetUpdates
import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.presentation.widget.components.CoverHeight import tachiyomi.presentation.widget.components.CoverHeight
import tachiyomi.presentation.widget.components.CoverWidth import tachiyomi.presentation.widget.components.CoverWidth
import tachiyomi.presentation.widget.components.LockedWidget import tachiyomi.presentation.widget.components.LockedWidget
import tachiyomi.presentation.widget.components.UpdatesWidget import tachiyomi.presentation.widget.components.UpdatesWidget
import tachiyomi.view.UpdatesView
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -39,6 +39,7 @@ import java.util.Calendar
import java.util.Date import java.util.Date
class UpdatesGridGlanceWidget : GlanceAppWidget() { class UpdatesGridGlanceWidget : GlanceAppWidget() {
private val app: Application by injectLazy() private val app: Application by injectLazy()
private val preferences: SecurityPreferences by injectLazy() private val preferences: SecurityPreferences by injectLazy()
@ -58,7 +59,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
UpdatesWidget(data) UpdatesWidget(data)
} }
fun loadData(list: List<UpdatesView>? = null) { fun loadData(list: List<UpdatesWithRelations>? = null) {
coroutineScope.launchIO { coroutineScope.launchIO {
// Don't show anything when lock is active // Don't show anything when lock is active
if (preferences.useAuthenticator().get()) { if (preferences.useAuthenticator().get()) {
@ -71,13 +72,10 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
if (ids.isEmpty()) return@launchIO if (ids.isEmpty()) return@launchIO
val processList = list val processList = list
?: Injekt.get<DatabaseHandler>() ?: Injekt.get<GetUpdates>().await(
.awaitList {
updatesViewQueries.getUpdatesByReadStatus(
read = false, read = false,
after = DateLimit.timeInMillis, after = DateLimit.timeInMillis,
) )
}
val (rowCount, columnCount) = ids val (rowCount, columnCount) = ids
.flatMap { manager.getAppWidgetSizes(it) } .flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value } .maxBy { it.height.value * it.width.value }
@ -88,7 +86,7 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
} }
} }
private fun prepareList(processList: List<UpdatesView>, take: Int): List<Pair<Long, Bitmap?>> { private fun prepareList(processList: List<UpdatesWithRelations>, take: Int): List<Pair<Long, Bitmap?>> {
// Resize to cover size // Resize to cover size
val widthPx = CoverWidth.value.toInt().dpToPx val widthPx = CoverWidth.value.toInt().dpToPx
val heightPx = CoverHeight.value.toInt().dpToPx val heightPx = CoverHeight.value.toInt().dpToPx
@ -101,10 +99,10 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
.data( .data(
MangaCover( MangaCover(
mangaId = updatesView.mangaId, mangaId = updatesView.mangaId,
sourceId = updatesView.source, sourceId = updatesView.sourceId,
isMangaFavorite = updatesView.favorite, isMangaFavorite = true,
url = updatesView.thumbnailUrl, url = updatesView.coverData.url,
lastModified = updatesView.coverLastModified, lastModified = updatesView.coverData.lastModified,
), ),
) )
.memoryCachePolicy(CachePolicy.DISABLED) .memoryCachePolicy(CachePolicy.DISABLED)

View file

@ -10,20 +10,14 @@ android {
defaultConfig { defaultConfig {
consumerProguardFile("consumer-proguard.pro") consumerProguardFile("consumer-proguard.pro")
} }
} }
dependencies { dependencies {
implementation(project(":core")) implementation(project(":core"))
api(kotlinx.serialization.json) api(kotlinx.serialization.json)
api(libs.injekt.core)
api(libs.rxjava) api(libs.rxjava)
api(libs.preferencektx) api(libs.preferencektx)
api(libs.jsoup) api(libs.jsoup)
implementation(androidx.corektx)
} }

View file

@ -68,7 +68,7 @@ abstract class HttpSource : CatalogueSource {
* Headers builder for requests. Implementations can override this method for custom headers. * Headers builder for requests. Implementations can override this method for custom headers.
*/ */
protected open fun headersBuilder() = Headers.Builder().apply { protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", network.defaultUserAgent) add("User-Agent", network.defaultUserAgentProvider())
} }
/** /**