mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01: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