parent
12ea764882
commit
75b19b7006
8
src/en/hyakuro/build.gradle
Normal file
8
src/en/hyakuro/build.gradle
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'Hyakuro Translations'
|
||||||
|
extClass = '.Hyakuro'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
BIN
src/en/hyakuro/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/hyakuro/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
BIN
src/en/hyakuro/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/hyakuro/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/en/hyakuro/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/hyakuro/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
src/en/hyakuro/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/hyakuro/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
src/en/hyakuro/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/hyakuro/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@ -0,0 +1,132 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.hyakuro
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PaginatedResponse(
|
||||||
|
val data: List<MangaResponse>,
|
||||||
|
val meta: Meta,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MangaResponse(
|
||||||
|
val attributes: MangaAttributes,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MangaAttributes(
|
||||||
|
@SerialName("Title") private val title: String,
|
||||||
|
private val slug: String,
|
||||||
|
@SerialName("Synopsis") private val synopsis: String?,
|
||||||
|
@SerialName("Artist") private val artist: String?,
|
||||||
|
@SerialName("Author") private val author: String?,
|
||||||
|
@SerialName("Status") private val status: String?,
|
||||||
|
@SerialName("Cover") private val cover: CoverObject?,
|
||||||
|
@SerialName("Chapters") val chapters: List<ChapterInListDto>?,
|
||||||
|
@SerialName("Categories") private val categories: List<String>?,
|
||||||
|
@SerialName("Longstrip") private val longstrip: Boolean?,
|
||||||
|
@SerialName("Oneshot") val oneshot: Boolean?,
|
||||||
|
val publishedAt: String?,
|
||||||
|
) {
|
||||||
|
fun toSManga(baseUrl: String): SManga = SManga.create().apply {
|
||||||
|
title = this@MangaAttributes.title
|
||||||
|
url = "/manga/$slug"
|
||||||
|
thumbnail_url = cover?.data?.attributes?.url?.let { "$baseUrl/backend$it" }
|
||||||
|
author = this@MangaAttributes.author
|
||||||
|
artist = this@MangaAttributes.artist
|
||||||
|
status = when (this@MangaAttributes.status) {
|
||||||
|
"Ongoing" -> SManga.ONGOING
|
||||||
|
"Completed" -> SManga.COMPLETED
|
||||||
|
"Dropped" -> SManga.CANCELLED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
description = synopsis
|
||||||
|
genre = (
|
||||||
|
categories?.plus(
|
||||||
|
listOfNotNull(
|
||||||
|
"Longstrip".takeIf { longstrip == true },
|
||||||
|
"Oneshot".takeIf { oneshot == true },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)?.joinToString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class CoverObject(
|
||||||
|
val data: CoverData,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class CoverData(
|
||||||
|
val attributes: CoverAttributes,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class CoverAttributes(
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterInListDto(
|
||||||
|
val id: Int,
|
||||||
|
@SerialName("Chapter") val chapter: Float,
|
||||||
|
@SerialName("Title") private val title: String?,
|
||||||
|
@SerialName("TranslatedOn") private val translatedOn: String?,
|
||||||
|
@SerialName("Pages") val pages: PageListDto?,
|
||||||
|
) {
|
||||||
|
fun toSChapter(mangaSlug: String, parent: MangaAttributes): SChapter = SChapter.create().apply {
|
||||||
|
url = "$mangaSlug#$chapter#$id"
|
||||||
|
val chapterStr = if (chapter % 1 == 0f) {
|
||||||
|
chapter.toInt().toString()
|
||||||
|
} else {
|
||||||
|
chapter.toString()
|
||||||
|
}
|
||||||
|
name = when {
|
||||||
|
title == null && parent.oneshot == true -> "Oneshot"
|
||||||
|
title == null && parent.oneshot == false -> "Chapter $chapterStr"
|
||||||
|
title != null && parent.oneshot == true -> "Oneshot - $title"
|
||||||
|
title != null && parent.oneshot == false -> "Chapter $chapterStr - $title"
|
||||||
|
else -> "Chapter $chapterStr"
|
||||||
|
}
|
||||||
|
val date = translatedOn ?: parent.publishedAt
|
||||||
|
date_upload = when {
|
||||||
|
date!!.contains("T") -> SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).tryParse(date)
|
||||||
|
else -> SimpleDateFormat("yyyy-MM-dd", Locale.US).tryParse(date)
|
||||||
|
}
|
||||||
|
chapter_number = chapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PageListDto(
|
||||||
|
val data: List<PageData>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PageData(
|
||||||
|
val attributes: PageAttributes,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PageAttributes(
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Meta(
|
||||||
|
val pagination: Pagination,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Pagination(
|
||||||
|
val page: Int,
|
||||||
|
val pageCount: Int,
|
||||||
|
)
|
||||||
@ -0,0 +1,210 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.hyakuro
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
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 eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import keiyoushi.utils.parseAs
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class Hyakuro : HttpSource() {
|
||||||
|
override val name = "Hyakuro Translations"
|
||||||
|
override val baseUrl = "https://hyakuro.net"
|
||||||
|
override val lang = "en"
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val apiUrl = "$baseUrl/backend/api"
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
// Popular/A-Z
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
val url = "$apiUrl/mangas".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("populate", "Cover,Chapters")
|
||||||
|
.addQueryParameter("sort", "Title:asc")
|
||||||
|
.addQueryParameter("pagination[page]", page.toString())
|
||||||
|
.build()
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val result = response.parseAs<PaginatedResponse>()
|
||||||
|
val mangas = result.data.map { it.attributes.toSManga(baseUrl) }
|
||||||
|
val hasNextPage = result.meta.pagination.page < result.meta.pagination.pageCount
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latest
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
val url = "$apiUrl/mangas".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("populate", "Cover,Chapters")
|
||||||
|
.addQueryParameter("sort", "updatedAt:desc")
|
||||||
|
.addQueryParameter("pagination[page]", page.toString())
|
||||||
|
.build()
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
|
||||||
|
|
||||||
|
// Search
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = "$apiUrl/mangas".toHttpUrl().newBuilder()
|
||||||
|
url.addQueryParameter("pagination[page]", page.toString())
|
||||||
|
url.addQueryParameter("populate", "Cover,Chapters")
|
||||||
|
url.addQueryParameter("sort", "updatedAt:desc")
|
||||||
|
|
||||||
|
if (query.isNotBlank()) {
|
||||||
|
url.addQueryParameter("filters[Title][\$containsi]", query)
|
||||||
|
}
|
||||||
|
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is StatusFilter -> {
|
||||||
|
if (filter.state != 0) {
|
||||||
|
val status = filter.values[filter.state]
|
||||||
|
if (status == "Oneshot") {
|
||||||
|
url.addQueryParameter("filters[Oneshot][\$eq]", "true")
|
||||||
|
} else {
|
||||||
|
url.addQueryParameter("filters[Status][\$eq]", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CategoryFilter -> {
|
||||||
|
filter.state.filter { it.state }.forEachIndexed { index, checkbox ->
|
||||||
|
url.addQueryParameter("filters[\$and][${index + 1}][Categories][\$containsi]", checkbox.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GET(url.build(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
||||||
|
|
||||||
|
// Details
|
||||||
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
|
return baseUrl + manga.url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
val slug = manga.url.substringAfter("/manga/")
|
||||||
|
val url = "$apiUrl/mangas".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("filters[slug][\$eq]", slug)
|
||||||
|
.addQueryParameter("populate", "Cover,Chapters")
|
||||||
|
.build()
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val result = response.parseAs<PaginatedResponse>()
|
||||||
|
return result.data.first().attributes.toSManga(baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chapter
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
return mangaDetailsRequest(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
val mangaSlug = response.request.url.queryParameter("filters[slug][\$eq]")!!
|
||||||
|
val parent = response.parseAs<PaginatedResponse>().data.first().attributes
|
||||||
|
return parent.chapters!!
|
||||||
|
.sortedByDescending { it.chapter }
|
||||||
|
.map { it.toSChapter(mangaSlug, parent) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChapterUrl(chapter: SChapter): String {
|
||||||
|
val parts = chapter.url.split("#")
|
||||||
|
val slug = parts[0]
|
||||||
|
val chapterNumber = parts[1]
|
||||||
|
return "$baseUrl/manga/$slug/read/$chapterNumber/1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
val parts = chapter.url.split("#")
|
||||||
|
val slug = parts[0]
|
||||||
|
val chapterId = parts[2]
|
||||||
|
val url = "$apiUrl/mangas".toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("filters[slug][\$eq]", slug)
|
||||||
|
.addQueryParameter("populate[Chapters][populate]", "*")
|
||||||
|
.fragment(chapterId)
|
||||||
|
.build()
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val chapterId = response.request.url.fragment!!.toInt()
|
||||||
|
val parent = response.parseAs<PaginatedResponse>().data.first().attributes
|
||||||
|
val chapter = parent.chapters!!.find { it.id == chapterId }!!
|
||||||
|
|
||||||
|
return chapter.pages!!.data
|
||||||
|
.sortedBy { it.attributes.url }
|
||||||
|
.mapIndexed { index, pageData ->
|
||||||
|
Page(index, imageUrl = "$baseUrl/backend${pageData.attributes.url}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
private class StatusFilter : Filter.Select<String>("Status", arrayOf("All", "Ongoing", "Completed", "Dropped", "Oneshot"))
|
||||||
|
private class Category(name: String) : Filter.CheckBox(name)
|
||||||
|
private class CategoryFilter(categories: List<Category>) : Filter.Group<Category>("Categories", categories)
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
Filter.Header("NOTE: Search query will be applied to filters")
|
||||||
|
return FilterList(
|
||||||
|
StatusFilter(),
|
||||||
|
CategoryFilter(getCategoryList()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCategoryList() = listOf(
|
||||||
|
Category("Action"),
|
||||||
|
Category("Adult"),
|
||||||
|
Category("Adventure"),
|
||||||
|
Category("Comedy"),
|
||||||
|
Category("Doujinshi"),
|
||||||
|
Category("Drama"),
|
||||||
|
Category("Ecchi"),
|
||||||
|
Category("Fantasy"),
|
||||||
|
Category("Gender Bender"),
|
||||||
|
Category("Harem"),
|
||||||
|
Category("Hentai"),
|
||||||
|
Category("Historical"),
|
||||||
|
Category("Horror"),
|
||||||
|
Category("Josei"),
|
||||||
|
Category("Lolicon"),
|
||||||
|
Category("Martial Arts"),
|
||||||
|
Category("Mature"),
|
||||||
|
Category("Mecha"),
|
||||||
|
Category("Mystery"),
|
||||||
|
Category("Psychological"),
|
||||||
|
Category("Romance"),
|
||||||
|
Category("School Life"),
|
||||||
|
Category("Sci-fi"),
|
||||||
|
Category("Seinen"),
|
||||||
|
Category("Shotacon"),
|
||||||
|
Category("Shoujo"),
|
||||||
|
Category("Shoujo Ai"),
|
||||||
|
Category("Shounen"),
|
||||||
|
Category("Shounen Ai"),
|
||||||
|
Category("Slice of Life"),
|
||||||
|
Category("Smut"),
|
||||||
|
Category("Sports"),
|
||||||
|
Category("Supernatural"),
|
||||||
|
Category("Tragedy"),
|
||||||
|
Category("Webtoon"),
|
||||||
|
Category("Yaoi"),
|
||||||
|
Category("Yuri"),
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user