diff --git a/src/en/qiscans/build.gradle b/src/en/qiscans/build.gradle new file mode 100644 index 000000000..3e48580eb --- /dev/null +++ b/src/en/qiscans/build.gradle @@ -0,0 +1,10 @@ +ext { + extName = 'Qi Scans' + extClass = '.QiScans' + themePkg = 'iken' + baseUrl = 'https://qiscans.org' + overrideVersionCode = 0 + isNsfw = false +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/qiscans/res/mipmap-hdpi/ic_launcher.png b/src/en/qiscans/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..22498e511 Binary files /dev/null and b/src/en/qiscans/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/qiscans/res/mipmap-mdpi/ic_launcher.png b/src/en/qiscans/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..d2c28090d Binary files /dev/null and b/src/en/qiscans/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/qiscans/res/mipmap-xhdpi/ic_launcher.png b/src/en/qiscans/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..f66fcb56b Binary files /dev/null and b/src/en/qiscans/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/qiscans/res/mipmap-xxhdpi/ic_launcher.png b/src/en/qiscans/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..6826e9298 Binary files /dev/null and b/src/en/qiscans/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/qiscans/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/qiscans/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..dbaaf67c0 Binary files /dev/null and b/src/en/qiscans/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/qiscans/src/eu/kanade/tachiyomi/extension/en/qiscans/GenreDto.kt b/src/en/qiscans/src/eu/kanade/tachiyomi/extension/en/qiscans/GenreDto.kt new file mode 100644 index 000000000..feea124d7 --- /dev/null +++ b/src/en/qiscans/src/eu/kanade/tachiyomi/extension/en/qiscans/GenreDto.kt @@ -0,0 +1,9 @@ +package eu.kanade.tachiyomi.extension.en.qiscans + +import kotlinx.serialization.Serializable + +@Serializable +class GenreDto(val id: Int, val name: String) + +@Serializable +class PageDto(val url: String, val order: Int) diff --git a/src/en/qiscans/src/eu/kanade/tachiyomi/extension/en/qiscans/QiScans.kt b/src/en/qiscans/src/eu/kanade/tachiyomi/extension/en/qiscans/QiScans.kt new file mode 100644 index 000000000..f4142d8cf --- /dev/null +++ b/src/en/qiscans/src/eu/kanade/tachiyomi/extension/en/qiscans/QiScans.kt @@ -0,0 +1,127 @@ +package eu.kanade.tachiyomi.extension.en.qiscans + +import eu.kanade.tachiyomi.multisrc.iken.GenreFilter +import eu.kanade.tachiyomi.multisrc.iken.Iken +import eu.kanade.tachiyomi.multisrc.iken.SelectFilter +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import keiyoushi.utils.parseAs +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import rx.schedulers.Schedulers +import java.util.concurrent.TimeUnit + +class QiScans : Iken( + "Qi Scans", + "en", + "https://qiscans.org", + "https://api.qiscans.org", +) { + + override val client = super.client.newBuilder() + .rateLimit(3, 1, TimeUnit.SECONDS) + .build() + + override fun popularMangaRequest(page: Int): Request { + val url = "$apiUrl/api/query".toHttpUrl().newBuilder().apply { + addQueryParameter("page", page.toString()) + addQueryParameter("perPage", "18") + addQueryParameter("orderBy", "totalViews") + }.build() + return GET(url, headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + return searchMangaParse(response) + } + + override fun latestUpdatesRequest(page: Int): Request { + val url = "$apiUrl/api/query".toHttpUrl().newBuilder().apply { // 'query' instead of 'posts' + addQueryParameter("page", page.toString()) + addQueryParameter("perPage", "18") + addQueryParameter("orderBy", "updatedAt") + }.build() + return GET(url, headers) + } + + override fun pageListRequest(chapter: SChapter): Request { + return GET(baseUrl + chapter.url, headersBuilder().add("rsc", "1").build()) + } + + override fun pageListParse(response: Response): List { + return response.body.string().lines() + .mapNotNull { line -> + val jsonStartIndex = line.indexOf('{').takeIf { it != -1 } ?: return@mapNotNull null + val jsonString = line.substring(jsonStartIndex) + try { + jsonString.parseAs().takeIf { it.url.isNotEmpty() } + } catch (e: Exception) { + null + } + } + .sortedBy { it.order } + .mapIndexed { i, p -> Page(i, imageUrl = p.url) } + } + + private var genresList: List> = emptyList() + private var fetchGenresAttempts = 0 + + private fun fetchGenres() { + try { + val response = client.newCall(GET("$apiUrl/api/genres", headers)).execute() + genresList = response.parseAs>() + .map { Pair(it.name, it.id.toString()) } + } catch (e: Throwable) { + } finally { + fetchGenresAttempts++ + } + } + + override fun getFilterList(): FilterList { + if (genresList.isEmpty() && fetchGenresAttempts < 3) { + Observable.fromCallable { fetchGenres() } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + val filters = mutableListOf>( + SortFilter(), + StatusFilter(), + ) + + if (genresList.isNotEmpty()) { + filters.add(GenreFilter(genresList)) + } else { + filters.add(Filter.Header("Press 'Reset' to attempt to load genres")) + } + return FilterList(filters) + } + + private class SortFilter : SelectFilter( + "Sort", + "orderBy", + listOf( + Pair("Popularity", "totalViews"), + Pair("Latest", "updatedAt"), + ), + ) + + private class StatusFilter : SelectFilter( + "Status", + "seriesStatus", + listOf( + Pair("All", ""), + Pair("Ongoing", "ONGOING"), + Pair("Hiatus", "HIATUS"), + Pair("Completed", "COMPLETED"), + Pair("Dropped", "DROPPED"), + ), + ) +}