Change extension repo to extension store and add support for newer extension index format (#3349)
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
package eu.kanade.tachiyomi.extension.model
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
import tachiyomi.domain.source.model.StubSource
|
||||
|
||||
sealed class Extension {
|
||||
|
||||
abstract val name: String
|
||||
abstract val pkgName: String
|
||||
abstract val versionName: String
|
||||
abstract val versionCode: Long
|
||||
abstract val libVersion: Double
|
||||
abstract val lang: String?
|
||||
abstract val isNsfw: Boolean
|
||||
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
override val libVersion: Double,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val pkgFactory: String?,
|
||||
val sources: List<Source>,
|
||||
val icon: Drawable?,
|
||||
val hasUpdate: Boolean = false,
|
||||
val isObsolete: Boolean = false,
|
||||
val isShared: Boolean,
|
||||
val store: ExtensionStore? = null,
|
||||
) : Extension()
|
||||
|
||||
data class Available(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
override val libVersion: Double,
|
||||
override val lang: String,
|
||||
override val isNsfw: Boolean,
|
||||
val sources: List<Source>,
|
||||
val apkUrl: String,
|
||||
val iconUrl: String,
|
||||
val store: ExtensionStore,
|
||||
) : Extension() {
|
||||
|
||||
data class Source(
|
||||
val id: Long,
|
||||
val lang: String,
|
||||
val name: String,
|
||||
val baseUrl: String,
|
||||
) {
|
||||
fun toStubSource(): StubSource {
|
||||
return StubSource(
|
||||
id = this.id,
|
||||
lang = this.lang,
|
||||
name = this.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Untrusted(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
override val libVersion: Double,
|
||||
val signatureHash: String,
|
||||
override val lang: String? = null,
|
||||
override val isNsfw: Boolean = false,
|
||||
) : Extension()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package mihon.domain.extension.interactor
|
||||
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
|
||||
class AddExtensionStore(
|
||||
private val repository: ExtensionStoreRepository,
|
||||
) {
|
||||
suspend operator fun invoke(indexUrl: String): Result<Unit> {
|
||||
return repository.insert(indexUrl)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package mihon.domain.extension.interactor
|
||||
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
|
||||
class GetExtensionStoreCountAsFlow(
|
||||
private val repository: ExtensionStoreRepository,
|
||||
) {
|
||||
operator fun invoke() = repository.getCountAsFlow()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package mihon.domain.extension.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
|
||||
class GetExtensionStores(
|
||||
private val repository: ExtensionStoreRepository,
|
||||
) {
|
||||
suspend fun get(): List<ExtensionStore> = repository.getAll()
|
||||
|
||||
fun subscribe(): Flow<List<ExtensionStore>> = repository.getAllAsFlow()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package mihon.domain.extension.interactor
|
||||
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
|
||||
class RemoveExtensionStore(
|
||||
private val repository: ExtensionStoreRepository,
|
||||
) {
|
||||
suspend operator fun invoke(indexUrl: String) {
|
||||
repository.remove(indexUrl)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package mihon.domain.extension.interactor
|
||||
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
|
||||
class UpdateExtensionStores(
|
||||
private val repository: ExtensionStoreRepository,
|
||||
) {
|
||||
suspend operator fun invoke() {
|
||||
repository.refreshAll()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package mihon.domain.extension.model
|
||||
|
||||
data class ExtensionStore(
|
||||
val indexUrl: String,
|
||||
val name: String,
|
||||
val badgeLabel: String,
|
||||
val signingKey: String,
|
||||
val contact: Contact,
|
||||
val isLegacy: Boolean,
|
||||
) {
|
||||
data class Contact(
|
||||
val website: String,
|
||||
val discord: String?,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package mihon.domain.extension.repository
|
||||
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
|
||||
interface ExtensionStoreRepository {
|
||||
suspend fun insert(indexUrl: String): Result<Unit>
|
||||
|
||||
suspend fun insertFromPreference(indexUrl: String, name: String)
|
||||
|
||||
suspend fun refreshAll()
|
||||
|
||||
suspend fun fetchExtensions(): List<Extension.Available>
|
||||
|
||||
suspend fun getAll(): List<ExtensionStore>
|
||||
|
||||
fun getAllAsFlow(): Flow<List<ExtensionStore>>
|
||||
|
||||
fun getCountAsFlow(): Flow<Long>
|
||||
|
||||
suspend fun remove(indexUrl: String)
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
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)
|
||||
@@ -1,72 +0,0 @@
|
||||
package mihon.domain.extensionrepo.interactor
|
||||
|
||||
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.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
|
||||
class CreateExtensionRepo(
|
||||
private val repository: ExtensionRepoRepository,
|
||||
private val service: ExtensionRepoService,
|
||||
) {
|
||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
||||
|
||||
suspend fun await(indexUrl: String): Result {
|
||||
val formattedIndexUrl = indexUrl.toHttpUrlOrNull()
|
||||
?.toString()
|
||||
?.takeIf { it.matches(repoRegex) }
|
||||
?: return Result.InvalidUrl
|
||||
|
||||
val baseUrl = formattedIndexUrl.removeSuffix("/index.min.json")
|
||||
return service.fetchRepoDetails(baseUrl)?.let { insert(it) } ?: Result.InvalidUrl
|
||||
}
|
||||
|
||||
private suspend fun insert(repo: ExtensionRepo): Result {
|
||||
return try {
|
||||
repository.insertRepo(
|
||||
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
|
||||
*/
|
||||
private suspend fun handleInsertionError(repo: ExtensionRepo): Result {
|
||||
val repoExists = repository.getRepo(repo.baseUrl)
|
||||
if (repoExists != null) {
|
||||
return Result.RepoAlreadyExists
|
||||
}
|
||||
val matchingFingerprintRepo = repository.getRepoBySigningKeyFingerprint(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
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package mihon.domain.extensionrepo.interactor
|
||||
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
|
||||
class DeleteExtensionRepo(
|
||||
private val repository: ExtensionRepoRepository,
|
||||
) {
|
||||
suspend fun await(baseUrl: String) {
|
||||
repository.deleteRepo(baseUrl)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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 repository: ExtensionRepoRepository,
|
||||
) {
|
||||
fun subscribeAll(): Flow<List<ExtensionRepo>> = repository.subscribeAll()
|
||||
|
||||
suspend fun getAll(): List<ExtensionRepo> = repository.getAll()
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package mihon.domain.extensionrepo.interactor
|
||||
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
|
||||
class GetExtensionRepoCount(
|
||||
private val repository: ExtensionRepoRepository,
|
||||
) {
|
||||
fun subscribe() = repository.getCount()
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package mihon.domain.extensionrepo.interactor
|
||||
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
|
||||
class ReplaceExtensionRepo(
|
||||
private val repository: ExtensionRepoRepository,
|
||||
) {
|
||||
suspend fun await(repo: ExtensionRepo) {
|
||||
repository.replaceRepo(repo)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package mihon.domain.extensionrepo.interactor
|
||||
|
||||
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 repository: ExtensionRepoRepository,
|
||||
private val service: ExtensionRepoService,
|
||||
) {
|
||||
|
||||
suspend fun awaitAll() = coroutineScope {
|
||||
repository.getAll()
|
||||
.map { async { await(it) } }
|
||||
.awaitAll()
|
||||
}
|
||||
|
||||
suspend fun await(repo: ExtensionRepo) {
|
||||
val newRepo = service.fetchRepoDetails(repo.baseUrl) ?: return
|
||||
if (
|
||||
repo.signingKeyFingerprint.startsWith("NOFINGERPRINT") ||
|
||||
repo.signingKeyFingerprint == newRepo.signingKeyFingerprint
|
||||
) {
|
||||
repository.upsertRepo(newRepo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package mihon.domain.extensionrepo.model
|
||||
|
||||
data class ExtensionRepo(
|
||||
val baseUrl: String,
|
||||
val name: String,
|
||||
val shortName: String?,
|
||||
val website: String,
|
||||
val signingKeyFingerprint: String,
|
||||
)
|
||||
@@ -1,47 +0,0 @@
|
||||
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 getRepo(baseUrl: String): ExtensionRepo?
|
||||
|
||||
suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo?
|
||||
|
||||
fun getCount(): Flow<Int>
|
||||
|
||||
suspend fun insertRepo(
|
||||
baseUrl: String,
|
||||
name: String,
|
||||
shortName: String?,
|
||||
website: String,
|
||||
signingKeyFingerprint: String,
|
||||
)
|
||||
|
||||
suspend fun upsertRepo(
|
||||
baseUrl: String,
|
||||
name: String,
|
||||
shortName: String?,
|
||||
website: String,
|
||||
signingKeyFingerprint: String,
|
||||
)
|
||||
|
||||
suspend fun upsertRepo(repo: ExtensionRepo) {
|
||||
upsertRepo(
|
||||
baseUrl = repo.baseUrl,
|
||||
name = repo.name,
|
||||
shortName = repo.shortName,
|
||||
website = repo.website,
|
||||
signingKeyFingerprint = repo.signingKeyFingerprint,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun replaceRepo(newRepo: ExtensionRepo)
|
||||
|
||||
suspend fun deleteRepo(baseUrl: String)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package mihon.domain.extensionrepo.service
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
|
||||
@Serializable
|
||||
data class ExtensionRepoMetaDto(
|
||||
val meta: ExtensionRepoDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ExtensionRepoDto(
|
||||
val name: String,
|
||||
val shortName: String?,
|
||||
val website: String,
|
||||
val signingKeyFingerprint: String,
|
||||
)
|
||||
|
||||
fun ExtensionRepoMetaDto.toExtensionRepo(baseUrl: String): ExtensionRepo {
|
||||
return ExtensionRepo(
|
||||
baseUrl = baseUrl,
|
||||
name = meta.name,
|
||||
shortName = meta.shortName,
|
||||
website = meta.website,
|
||||
signingKeyFingerprint = meta.signingKeyFingerprint,
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package mihon.domain.extensionrepo.service
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import logcat.LogPriority
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
|
||||
class ExtensionRepoService(
|
||||
networkHelper: NetworkHelper,
|
||||
private val json: Json,
|
||||
) {
|
||||
val client = networkHelper.client
|
||||
|
||||
suspend fun fetchRepoDetails(
|
||||
repo: String,
|
||||
): ExtensionRepo? {
|
||||
return withIOContext {
|
||||
try {
|
||||
with(json) {
|
||||
client.newCall(GET("$repo/repo.json"))
|
||||
.awaitSuccess()
|
||||
.parseAs<ExtensionRepoMetaDto>()
|
||||
.toExtensionRepo(baseUrl = repo)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to fetch repo details" }
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user