mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-17 16:49:43 +02:00
Grab extension repo detail from repo.json
and include in DB (#506)
* WIP Extension Repo DB Support * Wired in to extension screen, browse settings screen * Detekt changes * Ui tweaks and open in browser * Migrate ExtensionRepos on Update * Migration Cleanup * Slight cleanup / error handling * Update ExtensionRepo from Repo.json during extension search. Added Manual refresh in extension repos page. * Split repo fetching into separate API module, major refactor work * Removed development strings * Moved migration to #3 * Fixed rebase * Detekt changes * Added Replace Repository Dialog * Cleanup, removed platform specific code, PR comments * Removed extra function, reverted small change * Detekt cleanup * Apply suggestions from code review Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Fixed error introduced in cleanup * Tweak for multiline when * Moved getCount() to flow * changed getCount to non-suspend, used property delegation * Apply suggestions from code review Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Fixed formatting with updated comment string * Big wave of PR comments, renaming/other tweaks * onOpenWebsite changes * onOpenWebsite changes * trying to make single line * Renamed ExtensionRepoApi.kt to ExtensionRepoService.kt --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
package mihon.domain.extensionrepo.exception
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Exception to abstract over SQLiteException and SQLiteConstraintException for multiplatform.
|
||||
*
|
||||
* @param throwable the source throwable to include for tracing.
|
||||
*/
|
||||
class SaveExtensionRepoException(throwable: Throwable) : IOException("Error Saving Repository to Database", throwable)
|
@@ -0,0 +1,81 @@
|
||||
package mihon.domain.extensionrepo.interactor
|
||||
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import logcat.LogPriority
|
||||
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||
import okhttp3.OkHttpClient
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class CreateExtensionRepo(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository,
|
||||
) {
|
||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
||||
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
|
||||
private val client: OkHttpClient
|
||||
get() = networkService.client
|
||||
|
||||
private val extensionRepoService = ExtensionRepoService(client)
|
||||
|
||||
suspend fun await(repoUrl: String): Result {
|
||||
if (!repoUrl.matches(repoRegex)) {
|
||||
return Result.InvalidUrl
|
||||
}
|
||||
|
||||
val baseUrl = repoUrl.removeSuffix("/index.min.json")
|
||||
return extensionRepoService.fetchRepoDetails(baseUrl)?.let { insert(it) } ?: Result.InvalidUrl
|
||||
}
|
||||
|
||||
private suspend fun insert(repo: ExtensionRepo): Result {
|
||||
return try {
|
||||
extensionRepoRepository.insertRepository(
|
||||
repo.baseUrl,
|
||||
repo.name,
|
||||
repo.shortName,
|
||||
repo.website,
|
||||
repo.signingKeyFingerprint,
|
||||
)
|
||||
Result.Success
|
||||
} catch (e: SaveExtensionRepoException) {
|
||||
logcat(LogPriority.WARN, e) { "SQL Conflict attempting to add new repository ${repo.baseUrl}" }
|
||||
return handleInsertionError(repo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error Handler for insert when there are trying to create new repositories
|
||||
*
|
||||
* SaveExtensionRepoException doesn't provide constraint info in exceptions.
|
||||
* First check if the conflict was on primary key. if so return RepoAlreadyExists
|
||||
* Then check if the conflict was on fingerprint. if so Return DuplicateFingerprint
|
||||
* If neither are found, there was some other Error, and return Result.Error
|
||||
*
|
||||
* @param repo Extension Repo holder for passing to DB/Error Dialog
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
private suspend fun handleInsertionError(repo: ExtensionRepo): Result {
|
||||
val repoExists = extensionRepoRepository.getRepository(repo.baseUrl)
|
||||
if (repoExists != null) {
|
||||
return Result.RepoAlreadyExists
|
||||
}
|
||||
val matchingFingerprintRepo =
|
||||
extensionRepoRepository.getRepositoryBySigningKeyFingerprint(repo.signingKeyFingerprint)
|
||||
if (matchingFingerprintRepo != null) {
|
||||
return Result.DuplicateFingerprint(matchingFingerprintRepo, repo)
|
||||
}
|
||||
return Result.Error
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
data class DuplicateFingerprint(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : Result
|
||||
data object InvalidUrl : Result
|
||||
data object RepoAlreadyExists : Result
|
||||
data object Success : Result
|
||||
data object Error : Result
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package mihon.domain.extensionrepo.interactor
|
||||
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
|
||||
class DeleteExtensionRepo(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository,
|
||||
) {
|
||||
suspend fun await(baseUrl: String) {
|
||||
extensionRepoRepository.deleteRepository(baseUrl)
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package mihon.domain.extensionrepo.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
|
||||
class GetExtensionRepo(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository,
|
||||
) {
|
||||
fun subscribeAll(): Flow<List<ExtensionRepo>> = extensionRepoRepository.subscribeAll()
|
||||
|
||||
suspend fun getAll(): List<ExtensionRepo> = extensionRepoRepository.getAll()
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package mihon.domain.extensionrepo.interactor
|
||||
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
|
||||
class GetExtensionRepoCount(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository,
|
||||
) {
|
||||
fun subscribe() = extensionRepoRepository.getCount()
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package mihon.domain.extensionrepo.interactor
|
||||
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
|
||||
class ReplaceExtensionRepo(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository,
|
||||
) {
|
||||
suspend fun await(repo: ExtensionRepo) {
|
||||
extensionRepoRepository.replaceRepository(repo)
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package mihon.domain.extensionrepo.interactor
|
||||
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||
|
||||
class UpdateExtensionRepo(
|
||||
private val extensionRepoRepository: ExtensionRepoRepository,
|
||||
networkService: NetworkHelper,
|
||||
) {
|
||||
|
||||
private val extensionRepoService = ExtensionRepoService(networkService.client)
|
||||
|
||||
suspend fun awaitAll() = coroutineScope {
|
||||
extensionRepoRepository.getAll()
|
||||
.map { async { await(it) } }
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
suspend fun await(repo: ExtensionRepo) {
|
||||
val newRepo = extensionRepoService.fetchRepoDetails(repo.baseUrl) ?: return
|
||||
if (
|
||||
repo.signingKeyFingerprint.startsWith("NOFINGERPRINT") ||
|
||||
repo.signingKeyFingerprint == newRepo.signingKeyFingerprint
|
||||
) {
|
||||
extensionRepoRepository.upsertRepository(newRepo)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package mihon.domain.extensionrepo.model
|
||||
|
||||
data class ExtensionRepo(
|
||||
val baseUrl: String,
|
||||
val name: String,
|
||||
val shortName: String?,
|
||||
val website: String,
|
||||
val signingKeyFingerprint: String,
|
||||
)
|
@@ -0,0 +1,47 @@
|
||||
package mihon.domain.extensionrepo.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
|
||||
interface ExtensionRepoRepository {
|
||||
|
||||
fun subscribeAll(): Flow<List<ExtensionRepo>>
|
||||
|
||||
suspend fun getAll(): List<ExtensionRepo>
|
||||
|
||||
suspend fun getRepository(baseUrl: String): ExtensionRepo?
|
||||
|
||||
suspend fun getRepositoryBySigningKeyFingerprint(fingerprint: String): ExtensionRepo?
|
||||
|
||||
fun getCount(): Flow<Int>
|
||||
|
||||
suspend fun insertRepository(
|
||||
baseUrl: String,
|
||||
name: String,
|
||||
shortName: String?,
|
||||
website: String,
|
||||
signingKeyFingerprint: String,
|
||||
)
|
||||
|
||||
suspend fun upsertRepository(
|
||||
baseUrl: String,
|
||||
name: String,
|
||||
shortName: String?,
|
||||
website: String,
|
||||
signingKeyFingerprint: String,
|
||||
)
|
||||
|
||||
suspend fun upsertRepository(repo: ExtensionRepo) {
|
||||
upsertRepository(
|
||||
baseUrl = repo.baseUrl,
|
||||
name = repo.name,
|
||||
shortName = repo.shortName,
|
||||
website = repo.website,
|
||||
signingKeyFingerprint = repo.signingKeyFingerprint,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun replaceRepository(newRepo: ExtensionRepo)
|
||||
|
||||
suspend fun deleteRepository(baseUrl: String)
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package mihon.domain.extensionrepo.service
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.HttpException
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
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.jsonPrimitive
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import okhttp3.OkHttpClient
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ExtensionRepoService(
|
||||
private val client: OkHttpClient,
|
||||
) {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
suspend fun fetchRepoDetails(
|
||||
repo: String,
|
||||
): ExtensionRepo? {
|
||||
return withIOContext {
|
||||
val url = "$repo/repo.json".toUri()
|
||||
|
||||
try {
|
||||
val response = with(json) {
|
||||
client.newCall(GET(url.toString()))
|
||||
.awaitSuccess()
|
||||
.parseAs<JsonObject>()
|
||||
}
|
||||
response["meta"]
|
||||
?.jsonObject
|
||||
?.let { jsonToExtensionRepo(baseUrl = repo, it) }
|
||||
} catch (_: HttpException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToExtensionRepo(baseUrl: String, obj: JsonObject): ExtensionRepo? {
|
||||
return try {
|
||||
ExtensionRepo(
|
||||
baseUrl = baseUrl,
|
||||
name = obj["name"]!!.jsonPrimitive.content,
|
||||
shortName = obj["shortName"]?.jsonPrimitive?.content,
|
||||
website = obj["website"]!!.jsonPrimitive.content,
|
||||
signingKeyFingerprint = obj["signingKeyFingerprint"]!!.jsonPrimitive.content,
|
||||
)
|
||||
} catch (_: NullPointerException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user