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