Add Kouhai Scanlations (#9241)

* Add Kouhai Scanlations

* Fix filters

* Add chapter names
This commit is contained in:
ObserverOfTime 2021-09-27 17:12:32 +03:00 committed by GitHub
parent 17c02bfec4
commit b6fb13ec97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 260 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -0,0 +1,12 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Kouhai Scanlations'
pkgNameSuffix = 'en.kouhaiwork'
extClass = '.KouhaiWork'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -0,0 +1,56 @@
package eu.kanade.tachiyomi.extension.en.kouhaiwork
import eu.kanade.tachiyomi.source.model.Filter
class Genre(val id: Int, name: String) : Filter.CheckBox(name)
private val genres: List<Genre>
get() = listOf(
Genre(1, "Romance"),
Genre(2, "Comedy"),
Genre(3, "Slice of Life"),
Genre(4, "Fantasy"),
Genre(5, "Sci-Fi"),
Genre(6, "Psychological"),
Genre(7, "Horror"),
Genre(8, "Mystery"),
Genre(9, "Girls' Love"),
Genre(10, "Drama"),
Genre(11, "Action"),
Genre(12, "Ecchi"),
Genre(13, "Adventure"),
)
class GenresFilter(values: List<Genre> = genres) :
Filter.Group<Genre>("Genres", values)
class Theme(val id: Int, name: String) : Filter.CheckBox(name)
private val themes: List<Theme>
get() = listOf(
Theme(1, "Office Workers"),
Theme(2, "Family"),
Theme(3, "Supernatural"),
Theme(4, "Demons"),
Theme(5, "Magic"),
Theme(6, "Aliens"),
Theme(7, "Suggestive"),
Theme(8, "Doujinshi"),
Theme(9, "School Life"),
Theme(10, "Police"),
)
class ThemesFilter(values: List<Theme> = themes) :
Filter.Group<Theme>("Themes", values)
private val demographics: Array<String>
get() = arrayOf("Any", "Shounen", "Shoujo", "Seinen")
class DemographicsFilter(values: Array<String> = demographics) :
Filter.Select<String>("Demographic", values)
private val statuses: Array<String>
get() = arrayOf("Any", "Ongoing", "Finished", "Axed/Dropped")
class StatusFilter(values: Array<String> = statuses) :
Filter.Select<String>("Status", values)

View File

@ -0,0 +1,39 @@
package eu.kanade.tachiyomi.extension.en.kouhaiwork
import kotlinx.serialization.Serializable
@Serializable
data class KouhaiSeries(
val id: Int,
val title: String,
val cover: String,
val synopsis: String,
val status: String,
val artists: List<String>,
val authors: List<String>,
val alternative_titles: List<String>,
val genres: List<String>? = null,
val themes: List<String>? = null,
val demographics: List<String>? = null,
val chapters: List<KouhaiChapter>
)
@Serializable
data class KouhaiChapter(
val id: Int,
val group: String,
val number: Float,
val updated_at: String,
val name: String? = null
)
@Serializable
data class KouhaiTag(val id: Int)
@Serializable
data class KouhaiTagList(
val genres: List<KouhaiTag>,
val themes: List<KouhaiTag>,
val demographics: List<KouhaiTag>,
val status: KouhaiTag?
)

View File

@ -0,0 +1,151 @@
package eu.kanade.tachiyomi.extension.en.kouhaiwork
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
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 eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat
import java.text.SimpleDateFormat
import java.util.Locale
class KouhaiWork : HttpSource() {
override val name = "Kouhai Scanlations"
override val baseUrl = "https://kouhai.work"
override val lang = "en"
override val supportsLatest = true
private val json by injectLazy<Json>()
override fun latestUpdatesRequest(page: Int) =
GET("$API_URL/manga/week", headers)
override fun latestUpdatesParse(response: Response) =
response.parse()["data"]?.jsonArray?.map {
val arr = it.jsonArray
SManga.create().apply {
url = arr[0].jsonPrimitive.content
title = arr[1].jsonPrimitive.content
thumbnail_url = arr.last().jsonPrimitive.content
}
}.let { MangasPage(it ?: emptyList(), false) }
override fun popularMangaRequest(page: Int) =
GET("$API_URL/manga/all", headers)
override fun popularMangaParse(response: Response) =
latestUpdatesParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
FormBody.Builder().add("search", query).add("tags", filters.json())
.let { POST("$API_URL/manga/search", headers, it.build()) }
override fun searchMangaParse(response: Response) =
latestUpdatesParse(response)
// Request the actual manga URL for the webview
override fun mangaDetailsRequest(manga: SManga) =
GET("$baseUrl/series/${manga.url}", headers)
override fun fetchMangaDetails(manga: SManga) =
client.newCall(chapterListRequest(manga)).asObservableSuccess().map {
val series = it.data<KouhaiSeries>()
manga.description = series.synopsis
manga.author = series.authors.joinToString()
manga.artist = series.artists.joinToString()
manga.genre = series.genres.orEmpty()
.plus(series.themes.orEmpty())
.plus(series.demographics.orEmpty())
.joinToString()
manga.status = when (series.status) {
"ongoing" -> SManga.ONGOING
"finished" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
manga.initialized = true
return@map manga
}!!
override fun chapterListRequest(manga: SManga) =
GET("$API_URL/mangas/${manga.url}", headers)
override fun chapterListParse(response: Response) =
response.data<KouhaiSeries>().chapters.map {
SChapter.create().apply {
url = it.id.toString()
scanlator = it.group
chapter_number = it.number
name = "Chapter ${decimalFormat.format(it.number)}" +
if (it.name == null) "" else " - ${it.name}"
date_upload = dateFormat.parse(it.updated_at)?.time ?: 0L
}
}
override fun pageListRequest(chapter: SChapter) =
GET("$API_URL/chapters/${chapter.url}", headers)
override fun pageListParse(response: Response) =
response.parse()["chapter"]!!.jsonObject["pages"]!!
.jsonArray.mapIndexed { idx, obj ->
Page(idx, "", obj.jsonObject["media"]!!.jsonPrimitive.content)
}
override fun getFilterList() =
FilterList(GenresFilter(), ThemesFilter(), DemographicsFilter(), StatusFilter())
override fun mangaDetailsParse(response: Response) =
throw UnsupportedOperationException("Not used")
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException("Not used")
@Suppress("NOTHING_TO_INLINE")
private inline fun FilterList.json() =
json.encodeToJsonElement(
KouhaiTagList(
find<GenresFilter>()?.state?.filter { it.state }
?.map { KouhaiTag(it.id) } ?: emptyList(),
find<ThemesFilter>()?.state?.filter { it.state }
?.map { KouhaiTag(it.id) } ?: emptyList(),
find<DemographicsFilter>()?.state?.takeIf { it != 0 }
?.let { listOf(KouhaiTag(it)) } ?: emptyList(),
find<StatusFilter>()?.state?.takeIf { it != 0 }
?.let { KouhaiTag(it - 1) }
)
).toString()
@Suppress("NOTHING_TO_INLINE")
private inline fun Response.parse() =
json.parseToJsonElement(body!!.string()).jsonObject
private inline fun <reified T> Response.data() =
json.decodeFromJsonElement<T>(parse()["data"]!!)
private inline fun <reified T> FilterList.find() = find { it is T } as? T
companion object {
private const val API_URL = "https://api.kouhai.work/v2"
private const val ISO_DATE = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
private val dateFormat = SimpleDateFormat(ISO_DATE, Locale.ROOT)
private val decimalFormat = DecimalFormat("#.##")
}
}