Added Mangakawaii, based on MMRCMS (#907)
Added Mangakawaii, based on MMRCMS
This commit is contained in:
parent
0b7431cae9
commit
3b82504fc0
|
@ -0,0 +1,17 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
appName = 'Tachiyomi: Mangakawaii'
|
||||
pkgNameSuffix = 'fr.mangakawaii'
|
||||
extClass = '.MangaKawaiiSource'
|
||||
extVersionCode = 1
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'com.google.code.gson:gson:2.8.2'
|
||||
compileOnly 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
|
@ -0,0 +1,324 @@
|
|||
package eu.kanade.tachiyomi.extension.fr.mangakawaii
|
||||
|
||||
import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
class MangaKawaiiSource : HttpSource() {
|
||||
|
||||
override val lang = "fr"
|
||||
override val name = "Mangakawaii"
|
||||
override val baseUrl="https://www.mangakawaii.to"
|
||||
override val supportsLatest = false
|
||||
private val itemUrl= "https://www.mangakawaii.to/manga/"
|
||||
|
||||
private val jsonParser = JsonParser()
|
||||
private val itemUrlPath = Uri.parse(itemUrl).pathSegments.first()
|
||||
private val parsedBaseUrl = Uri.parse(baseUrl)
|
||||
|
||||
private val categoriesJson = """{"categories":[{"id":"1","name":"Action"},{"id":"2","name":"Aventure"},{"id":"3","name":"Comédie"},{"id":"5","name":"Drame"},{"id":"7","name":"Fantastique"},{"id":"8","name":"Gender Bender"},{"id":"9","name":"Harem"},{"id":"10","name":"Historique"},{"id":"11","name":"Horreur"},{"id":"12","name":"Josei"},{"id":"13","name":"Arts Martiaux"},{"id":"14","name":"Mature"},{"id":"15","name":"Mecha"},{"id":"16","name":"Mystère"},{"id":"17","name":"One Shot"},{"id":"18","name":"Psychologique"},{"id":"19","name":"Romance"},{"id":"20","name":"Vie Scolaire"},{"id":"21","name":"Sci-fi"},{"id":"22","name":"Seinen"},{"id":"23","name":"Shojo"},{"id":"24","name":"Shojo Ai"},{"id":"25","name":"Shonen"},{"id":"26","name":"Shonen Ai"},{"id":"27","name":"Tranche de vie"},{"id":"28","name":"Sports"},{"id":"29","name":"Surnaturel"},{"id":"30","name":"Adulte"},{"id":"31","name":"Yaoi"},{"id":"32","name":"Yuri"},{"id":"33","name":"Webtoon"},{"id":"35","name":"Ecchi"},{"id":"36","name":"Doujin"}]}"""
|
||||
private val jsonCategories = jsonParser.parse(categoriesJson) as JsonObject
|
||||
private val categoryMappings = mapToPairs(jsonCategories["categories"].array)
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/filterLists?page=$page&sortBy=views&asc=false")
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
//Query overrides everything
|
||||
val url: Uri.Builder
|
||||
if (query.isNotBlank()) {
|
||||
url = Uri.parse("$baseUrl/recherche")!!.buildUpon()
|
||||
url.appendQueryParameter("query", query)
|
||||
} else {
|
||||
url = Uri.parse("$baseUrl/filterLists?page=$page")!!.buildUpon()
|
||||
filters.filterIsInstance<UriFilter>()
|
||||
.forEach { it.addToUri(url) }
|
||||
}
|
||||
return GET(url.toString())
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/filterLists?page=$page&sortBy=last_release&asc=false")
|
||||
|
||||
override fun popularMangaParse(response: Response) = internalMangaParse(response)
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
return if (response.request().url().queryParameter("query")?.isNotBlank() == true) {
|
||||
//If a search query was specified, use search instead!
|
||||
MangasPage(jsonParser
|
||||
.parse(response.body()!!.string())["suggestions"].array
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
val segment = it["data"].string
|
||||
url = getUrlWithoutBaseUrl(itemUrl + segment)
|
||||
title = it["value"].string
|
||||
|
||||
// Guess thumbnails
|
||||
// thumbnail_url = "$baseUrl/uploads/manga/$segment/cover/cover_250x350.jpg"
|
||||
}
|
||||
}, false)
|
||||
} else {
|
||||
internalMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = internalMangaParse(response)
|
||||
|
||||
private fun internalMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return MangasPage(document.select("div[class^=col-sm]").map {
|
||||
SManga.create().apply {
|
||||
val urlElement = it.getElementsByClass("infobubble")
|
||||
url = getUrlWithoutBaseUrl(urlElement.attr("href"))
|
||||
title = it.select("p.infotitle").text().trim()
|
||||
|
||||
|
||||
val cover = it.select(".media-left img").attr("data-src")
|
||||
thumbnail_url =
|
||||
if (cover.isEmpty()) {
|
||||
coverGuess(it.select("img").attr("data-src"), url)
|
||||
} else {
|
||||
coverGuess(cover, url)
|
||||
}
|
||||
}
|
||||
}, document.select(".pagination a[rel=next]").isNotEmpty())
|
||||
}
|
||||
|
||||
// Guess thumbnails on broken websites
|
||||
|
||||
private fun coverGuess(url: String?, mangaUrl: String): String {
|
||||
// Guess thumbnails on broken websites
|
||||
if (url != null && url.isNotBlank()) {
|
||||
if (url.startsWith("//")) {
|
||||
return "$baseUrl/uploads/manga/${url.substringBeforeLast("/cover/").substringAfter("/manga/")}/cover/cover_250x350.jpg"
|
||||
}
|
||||
if (url.endsWith("no-image.png")) {
|
||||
return "$baseUrl/uploads/manga/${mangaUrl?.substringAfterLast('/')}/cover/cover_250x350.jpg"
|
||||
}
|
||||
return url
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun getUrlWithoutBaseUrl(newUrl: String): String {
|
||||
val parsedNewUrl = Uri.parse(newUrl)
|
||||
val newPathSegments = parsedNewUrl.pathSegments.toMutableList()
|
||||
|
||||
for (i in parsedBaseUrl.pathSegments) {
|
||||
if (i.trim().equals(newPathSegments.first(), true)) {
|
||||
newPathSegments.removeAt(0)
|
||||
} else break
|
||||
}
|
||||
|
||||
val builtUrl = parsedNewUrl.buildUpon().path("/")
|
||||
newPathSegments.forEach { builtUrl.appendPath(it) }
|
||||
|
||||
var out = builtUrl.build().encodedPath
|
||||
if (parsedNewUrl.encodedQuery != null)
|
||||
out += "?" + parsedNewUrl.encodedQuery
|
||||
if (parsedNewUrl.encodedFragment != null)
|
||||
out += "#" + parsedNewUrl.encodedFragment
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val document = response.asJsoup()
|
||||
title = document.select(".info-desc__content").text().trim()
|
||||
thumbnail_url = coverGuess(document.select(".manga__image .manga__cover").attr("src"), document.location())
|
||||
description = document.select(".info-desc__content").text().trim()
|
||||
|
||||
var cur: String? = null
|
||||
for (element in document.select(".manga-info .info-list__row").select("strong,a,span")) {
|
||||
when (element.tagName()) {
|
||||
"strong" -> cur = element.text().trim().toLowerCase()
|
||||
"a","span" -> when (cur) {
|
||||
"auteur(s)" -> author = element.text()
|
||||
"artiste(s)" -> artist = element.text()
|
||||
"categories" -> genre = element.getElementsByTag("a").joinToString {
|
||||
it.text().trim()
|
||||
}
|
||||
|
||||
"statut"-> status = when (element.text().trim().toLowerCase()) {
|
||||
"terminé" -> SManga.COMPLETED
|
||||
"en cours" -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the response from the site and returns a list of chapters.
|
||||
*
|
||||
* Overriden to allow for null chapters
|
||||
*
|
||||
* @param response the response from the site.
|
||||
*/
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return document.select(chapterListSelector()).mapNotNull { nullableChapterFromElement(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
|
||||
*/
|
||||
fun chapterListSelector() = ".chapters-list > .chapter-item:not(.btn)"
|
||||
|
||||
/**
|
||||
* Returns a chapter from the given element.
|
||||
*
|
||||
* @param element an element obtained from [chapterListSelector].
|
||||
*/
|
||||
private fun nullableChapterFromElement(element: Element): SChapter? {
|
||||
val titleWrapper = element.getElementsByClass("list-item__title").first()
|
||||
val url = titleWrapper.getElementsByTag("a").attr("href")
|
||||
|
||||
// Ensure chapter actually links to a manga
|
||||
// Some websites use the chapters box to link to post announcements
|
||||
if (!Uri.parse(url).pathSegments.firstOrNull().equals(itemUrlPath, true)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val chapter = SChapter.create()
|
||||
|
||||
chapter.url = getUrlWithoutBaseUrl(url)
|
||||
chapter.name = titleWrapper.text()
|
||||
|
||||
// Parse date
|
||||
val dateText = element.getElementsByClass("chapter-item__date").text().trim()
|
||||
val formattedDate = try {
|
||||
DATE_FORMAT.parse(dateText).time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
chapter.date_upload = formattedDate
|
||||
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response) = response.asJsoup().select("#all > .img-responsive")
|
||||
.mapIndexed { i, e ->
|
||||
var url = e.attr("data-src")
|
||||
|
||||
if (url.isBlank()) {
|
||||
url = e.attr("src")
|
||||
}
|
||||
|
||||
url = url.trim()
|
||||
|
||||
Page(i, url, url)
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Unused method called!")
|
||||
|
||||
private fun getInitialFilterList() = listOf<Filter<*>>(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
AuthorFilter(),
|
||||
UriSelectFilter("Category",
|
||||
"cat",
|
||||
arrayOf("" to "Any",
|
||||
*categoryMappings.toTypedArray()
|
||||
)
|
||||
),
|
||||
UriSelectFilter("Begins with",
|
||||
"alpha",
|
||||
arrayOf("" to "Any",
|
||||
*"#ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray().map {
|
||||
Pair(it.toString(), it.toString())
|
||||
}.toTypedArray()
|
||||
)
|
||||
),
|
||||
SortFilter()
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
override fun getFilterList() = FilterList(
|
||||
getInitialFilterList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Class that creates a select filter. Each entry in the dropdown has a name and a display name.
|
||||
* If an entry is selected it is appended as a query parameter onto the end of the URI.
|
||||
* If `firstIsUnspecified` is set to true, if the first entry is selected, nothing will be appended on the the URI.
|
||||
*/
|
||||
//vals: <name, display>
|
||||
open class UriSelectFilter(displayName: String, val uriParam: String, val vals: Array<Pair<String, String>>,
|
||||
val firstIsUnspecified: Boolean = true,
|
||||
defaultValue: Int = 0) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
if (state != 0 || !firstIsUnspecified)
|
||||
uri.appendQueryParameter(uriParam, vals[state].first)
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorFilter : Filter.Text("Author"), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
uri.appendQueryParameter("author", state)
|
||||
}
|
||||
}
|
||||
|
||||
class SortFilter : Filter.Sort("Sort",
|
||||
sortables.map { it.second }.toTypedArray(),
|
||||
Filter.Sort.Selection(0, true)), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder) {
|
||||
uri.appendQueryParameter("sortBy", sortables[state!!.index].first)
|
||||
uri.appendQueryParameter("asc", state!!.ascending.toString())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val sortables = arrayOf(
|
||||
"name" to "Name",
|
||||
"views" to "Popularity",
|
||||
"last_release" to "Last update"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a filter that is able to modify a URI.
|
||||
*/
|
||||
interface UriFilter {
|
||||
fun addToUri(uri: Uri.Builder)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMAT = SimpleDateFormat("DD.MM.yyyy", Locale.FRANCE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an array of JSON objects to pairs. Each JSON object must have
|
||||
* the following properties:
|
||||
*
|
||||
* id: first item in pair
|
||||
* name: second item in pair
|
||||
*
|
||||
* @param array The array to process
|
||||
* @return The new list of pairs
|
||||
*/
|
||||
private fun mapToPairs(array: JsonArray): List<Pair<String, String>> = array.map {
|
||||
it as JsonObject
|
||||
|
||||
it["id"].string to it["name"].string
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue