diff --git a/src/en/kouhaiwork/AndroidManifest.xml b/src/en/kouhaiwork/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/en/kouhaiwork/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/en/kouhaiwork/build.gradle b/src/en/kouhaiwork/build.gradle new file mode 100644 index 000000000..7092fad4b --- /dev/null +++ b/src/en/kouhaiwork/build.gradle @@ -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" diff --git a/src/en/kouhaiwork/res/mipmap-hdpi/ic_launcher.png b/src/en/kouhaiwork/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..90295216c Binary files /dev/null and b/src/en/kouhaiwork/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/kouhaiwork/res/mipmap-mdpi/ic_launcher.png b/src/en/kouhaiwork/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..682fe5c69 Binary files /dev/null and b/src/en/kouhaiwork/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/kouhaiwork/res/mipmap-xhdpi/ic_launcher.png b/src/en/kouhaiwork/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..cedcf2010 Binary files /dev/null and b/src/en/kouhaiwork/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/kouhaiwork/res/mipmap-xxhdpi/ic_launcher.png b/src/en/kouhaiwork/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..72b5c4cc9 Binary files /dev/null and b/src/en/kouhaiwork/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/kouhaiwork/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/kouhaiwork/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..ef1ca9f06 Binary files /dev/null and b/src/en/kouhaiwork/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/kouhaiwork/res/web_hi_res_512.png b/src/en/kouhaiwork/res/web_hi_res_512.png new file mode 100644 index 000000000..c868c5942 Binary files /dev/null and b/src/en/kouhaiwork/res/web_hi_res_512.png differ diff --git a/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiFilters.kt b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiFilters.kt new file mode 100644 index 000000000..01f52eeeb --- /dev/null +++ b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiFilters.kt @@ -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 + 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 = genres) : + Filter.Group("Genres", values) + +class Theme(val id: Int, name: String) : Filter.CheckBox(name) + +private val themes: List + 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 = themes) : + Filter.Group("Themes", values) + +private val demographics: Array + get() = arrayOf("Any", "Shounen", "Shoujo", "Seinen") + +class DemographicsFilter(values: Array = demographics) : + Filter.Select("Demographic", values) + +private val statuses: Array + get() = arrayOf("Any", "Ongoing", "Finished", "Axed/Dropped") + +class StatusFilter(values: Array = statuses) : + Filter.Select("Status", values) diff --git a/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiModels.kt b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiModels.kt new file mode 100644 index 000000000..59fc77caf --- /dev/null +++ b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiModels.kt @@ -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, + val authors: List, + val alternative_titles: List, + val genres: List? = null, + val themes: List? = null, + val demographics: List? = null, + val chapters: List +) + +@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, + val themes: List, + val demographics: List, + val status: KouhaiTag? +) diff --git a/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiWork.kt b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiWork.kt new file mode 100644 index 000000000..18e4f628a --- /dev/null +++ b/src/en/kouhaiwork/src/eu/kanade/tachiyomi/extension/en/kouhaiwork/KouhaiWork.kt @@ -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() + + 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() + 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().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()?.state?.filter { it.state } + ?.map { KouhaiTag(it.id) } ?: emptyList(), + find()?.state?.filter { it.state } + ?.map { KouhaiTag(it.id) } ?: emptyList(), + find()?.state?.takeIf { it != 0 } + ?.let { listOf(KouhaiTag(it)) } ?: emptyList(), + find()?.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 Response.data() = + json.decodeFromJsonElement(parse()["data"]!!) + + private inline fun 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("#.##") + } +}