Manhwa latino (#9692)

* First commit Mahnwa-Latino Extension.

* manhwa-latino: Implementing find function

TODO: only works with text, filter not implemented yet

* manhwa-latino Add Parser to finde the information of Manhwa-Latino

The parser 'ManhwaLatinoSiteParser.kt' make the whole magic to find the
information of the Website.

Mudularize The code and adding documentation

* manhwa-latino: Adding Logos der Extension

TODO: I am not to happy, i will check it later.

* manhwa-latino: Adding Tags to Genre Combobox

* manhwa-latino: Adding Headers to prevent error 404

The headers are necesary to prevent error 403 by downloading images.

* manhwa-latino: Tags addded into Manga Description Page

Status from Manga readed from Tags

* manhwa-latino: Modularize Code

* manhwa-latino: Adding Uploaddate for Chapters

* manhwa-latino: Bug to get Chapter Number fixed

* manhwa-latino: Logo 0.2

* manhwa-latino: Versionb 1.2.10

Adding Comments to ManhwaLatinoSiteParser

* manhwa-latino: Remove logo_model directory

* manhwa-latino: Show Seconds after Release a new Chapter

Co-authored-by: Luis Beroiza <luisalberto.beroizaosses@intern.osp-dd.de>
This commit is contained in:
Luis Alberto 2021-11-03 17:05:12 +01:00 committed by GitHub
parent c5262ebb59
commit 5695e7e470
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 673 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'
ext {
extName = 'Manhwa-Latino'
pkgNameSuffix = 'es.manhwalatino'
extClass = '.ManhwaLatino'
extVersionCode = 11
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

View File

@ -0,0 +1,279 @@
package eu.kanade.tachiyomi.extension.es.manhwalatino
import eu.kanade.tachiyomi.extension.es.manhwalatino.filters.GenreTagFilter
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class ManhwaLatino : ParsedHttpSource() {
/**
* Name of the source.
*/
override val name = "Manhwa-Latino"
/**
* Base url of the website without the trailing slash, like: http://mysite.com
*/
override val baseUrl = "https://manhwa-latino.com"
/**
* Parser for Mainsite or Genre Site
*/
val manhwaLatinoSiteParser = ManhwaLatinoSiteParser(baseUrl)
/**
* An ISO 639-1 compliant language code (two letters in lower case).
*/
override val lang = "es"
/**
* Whether the source has support for latest updates.
*/
override val supportsLatest = true
/**
* User Agent for this wWebsite
*/
private val userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"
override fun headersBuilder(): Headers.Builder {
return Headers.Builder()
.add("User-Agent", userAgent)
.add("Referer", "$baseUrl/")
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
override fun popularMangaSelector(): String {
return manhwaLatinoSiteParser.popularMangaSelector
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
override fun latestUpdatesSelector(): String {
return manhwaLatinoSiteParser.latestUpdatesSelector
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
override fun searchMangaSelector(): String {
return manhwaLatinoSiteParser.searchMangaSelector
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
*/
override fun chapterListSelector() =
throw Exception("Not Used")
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
override fun popularMangaNextPageSelector(): String {
return manhwaLatinoSiteParser.popularMangaNextPageSelector
}
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
override fun latestUpdatesNextPageSelector(): String {
return manhwaLatinoSiteParser.latestUpdatesNextPageSelector
}
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
override fun searchMangaNextPageSelector(): String {
return manhwaLatinoSiteParser.searchMangaNextPageSelector
}
/**
* Returns the request for the popular manga given the page.
*
* @param page the page number to retrieve.
*/
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/page/$page/", headers)
}
/**
* Returns the request for latest manga given the page.
*
* @param page the page number to retrieve.
*/
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/page/$page/", headers)
}
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map { latestUpdatesFromElement(it) }
return MangasPage(mangas, manhwaLatinoSiteParser.latestUpdatesHasNextPages())
}
/**
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [latestUpdatesSelector].
*/
override fun latestUpdatesFromElement(element: Element): SManga {
return manhwaLatinoSiteParser.getMangaFromLastTranslatedSlide(element)
}
/**
* Returns the request for the search manga given the page.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val uri = manhwaLatinoSiteParser.searchMangaRequest(page, query, filters)
return GET(uri.toString(), headers)
}
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun searchMangaParse(response: Response): MangasPage {
return manhwaLatinoSiteParser.searchMangaParse(response)
}
// /**
// * Returns the request for the details of a manga. Override only if it's needed to change the
// * url, send different headers or request method like POST.
// *
// * @param manga the manga to be updated.
// */
// override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, headers)
/**
* Returns the request for updating the chapter list. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param manga the manga to look for chapters.
*/
override fun chapterListRequest(manga: SManga): Request {
return GET(baseUrl + manga.url, headers)
}
/**
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [popularMangaSelector].
*/
override fun popularMangaFromElement(element: Element) = mangaFromElement(element)
/**
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [searchMangaSelector].
*/
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
/**
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [searchMangaSelector].
*/
private fun mangaFromElement(element: Element): SManga {
return manhwaLatinoSiteParser.getMangaFromList(element)
}
/**
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
*/
override fun chapterListParse(response: Response): List<SChapter> {
return manhwaLatinoSiteParser.getChapterListParse(response)
}
/**
* Returns a chapter from the given element.
*
* @param element an element obtained from [chapterListSelector].
*/
override fun chapterFromElement(element: Element) = throw Exception("Not used")
/**
* Returns the details of the manga from the given [document].
*
* @param document the parsed document.
*/
override fun mangaDetailsParse(document: Document): SManga {
return manhwaLatinoSiteParser.getMangaDetails(document)
}
/**
* Returns the request for getting the page list. Override only if it's needed to override the
* url, send different headers or request method like POST.
* (Request to Webseite with comic)
*
* @param chapter the chapter whose page list has to be fetched.
*/
override fun pageListRequest(chapter: SChapter): Request {
return GET(baseUrl + chapter.url, headers)
}
/**
* Parses the response from the site and returns the page list.
* (Parse the comic pages from the website with the chapter)
*
* @param response the response from the site.
*/
override fun pageListParse(response: Response): List<Page> {
return manhwaLatinoSiteParser.getPageListParse(response)
}
/**
* Returns a page list from the given document.
*
* @param document the parsed document.
*/
override fun pageListParse(document: Document) = throw Exception("Not Used")
override fun imageUrlParse(document: Document) = throw Exception("Not Used")
/**
* Returns the list of filters for the source.
*/
override fun getFilterList() = FilterList(
Filter.Header("NOTA: ¡La búsqueda de títulos no funciona!"), // "Title search not working"
Filter.Separator(),
GenreTagFilter(),
// LetterFilter(),
// StatusFilter(),
// SortFilter()
)
}

View File

@ -0,0 +1,294 @@
package eu.kanade.tachiyomi.extension.es.manhwalatino
import android.net.Uri
import eu.kanade.tachiyomi.extension.es.manhwalatino.filters.UriFilter
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.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class ManhwaLatinoSiteParser(private val baseUrl: String) {
/**
* TODO: ADD SEARCH_TAG
*/
enum class SearchType {
SEARCH_FREE, SEARCH_FILTER
}
/**
* Type of search ( FREE, FILTER)
*/
var searchType = SearchType.SEARCH_FREE
private val urlHTMLSelector: String = "a"
private val titleHTMLSelector: String = "h3"
private val thumbnailUrlMangaListHTMLSelector: String = "div.item-thumb.c-image-hover img"
private val authorHTMLSelector: String = "div.author-content"
private val artistHTMLSelector: String = "div.artist-content"
private val descriptionHTMLSelector: String = "div.summary__content.show-more p"
private val genreHTMLSelector: String = "div.genres-content a"
private val statusHTMLSelector: String =
"div.summary_content div.post-status div.post-content_item div.summary-content"
private val thumbnailUrlMangaDetailsHTMLSelector: String = "div.summary_image img"
private val tagsHTMLSelector: String = "div.tags-content a"
private val searchSiteMangasHTMLSelector = "div.c-tabs-item__content"
private val genreSiteMangasHTMLSelector = "div.page-item-detail.manga"
private val latestUpdatesSelectorUrl = "div.slider__thumb_item a"
private val latestUpdatesSelectorThumbnailUrl = "div.slider__thumb_item a img"
private val latestUpdatesSelectorTitle = "div.slider__content h4"
private val chapterListParseSelector = "li.wp-manga-chapter"
private val chapterLinkParser = "a"
private val chapterReleaseDateLinkParser = "span.chapter-release-date a"
private val chapterReleaseDateIParser = "span.chapter-release-date i"
private val pageListParseSelector = "div.page-break.no-gaps img"
val searchMangaNextPageSelector = "link[rel=next]"
val latestUpdatesSelector = "div.slider__item"
val popularMangaSelector = "div.page-item-detail.manga"
val searchMangaSelector = "div.page-item-detail.manga"
val popularMangaNextPageSelector = "a.nextpostslink"
val latestUpdatesNextPageSelector = "div[role=navigation] a.last"
/**
* The Latest Updates are in a Slider, this Methods get a Manga from the slide
*/
fun getMangaFromLastTranslatedSlide(element: Element): SManga {
val manga = SManga.create()
manga.url =
getUrlWithoutDomain(element.select(latestUpdatesSelectorUrl).first().attr("abs:href"))
manga.title = element.select(latestUpdatesSelectorTitle).text().trim()
manga.thumbnail_url = element.select(latestUpdatesSelectorThumbnailUrl).attr("abs:data-src")
return manga
}
/**
* The Latest Updates has only one site
*/
fun latestUpdatesHasNextPages() = false
/**
* Get eine Liste mit Mangas from Search Site
*/
fun getMangasFromSearchSite(document: Document): List<SManga> {
return document.select(searchSiteMangasHTMLSelector).map { getMangaFromList(it) }
}
/**
* Get eine Liste mit Mangas from Genre Site
*/
fun getMangasFromGenreSite(document: Document): List<SManga> {
return document.select(genreSiteMangasHTMLSelector).map { getMangaFromList(it) }
}
/**
* Parse The Information from Mangas From Search or Genre Site
* Title, Address and thumbnail_url
*/
fun getMangaFromList(element: Element): SManga {
val manga = SManga.create()
manga.url = getUrlWithoutDomain(element.select(urlHTMLSelector).first().attr("abs:href"))
manga.title = element.select(titleHTMLSelector).text().trim()
manga.thumbnail_url = element.select(thumbnailUrlMangaListHTMLSelector).attr("abs:data-src")
return manga
}
/**
* Get The Details of a Manga Main Website
* Description, genre, tags, picture (thumbnail_url)
* status...
*/
fun getMangaDetails(document: Document): SManga {
val manga = SManga.create()
val descriptionList = document.select(descriptionHTMLSelector).map { it.text() }
val author = document.select(authorHTMLSelector).text()
val artist = document.select(artistHTMLSelector).text()
val genrelist = document.select(genreHTMLSelector).map { it.text() }
val tagList = document.select(tagsHTMLSelector).map { it.text() }
val genreTagList = genrelist + tagList
manga.thumbnail_url =
document.select(thumbnailUrlMangaDetailsHTMLSelector).attr("abs:data-src")
manga.description = descriptionList.joinToString("\n")
manga.author = if (author.isBlank()) "Autor Desconocido" else author
manga.artist = artist
manga.genre = genreTagList.joinToString(", ")
manga.status = findMangaStatus(tagList, document)
return manga
}
private fun findMangaStatus(tagList: List<String>, document: Document): Int {
return if (tagList.contains("Fin")) {
SManga.COMPLETED
} else {
when (document.select(statusHTMLSelector)?.first()?.text()?.trim()) {
"Publicandose" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
}
}
/**
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
*/
fun getChapterListParse(response: Response): List<SChapter> {
return response.asJsoup().select(chapterListParseSelector).map { element ->
// Link to the Chapter with the info (address and chapter title)
val chapterInfo = element.select(chapterLinkParser)
// Chaptername
val chapterName = chapterInfo.text().trim()
// release date came as text with format dd/mm/yyyy from a link or <i>dd/mm/yyyy</i>
val chapterReleaseDate = getChapterReleaseDate(element)
SChapter.create().apply {
name = chapterName
chapter_number = getChapterNumber(chapterName)
url = getUrlWithoutDomain(chapterInfo.attr("abs:href"))
date_upload = parseChapterReleaseDate(chapterReleaseDate)
}
}
}
/**
* Get the number of Chapter from Chaptername
*/
private fun getChapterNumber(chapterName: String): Float =
Regex("""\d+""").find(chapterName)?.value.toString().trim().toFloat()
/**
* Get The String with the information about the Release date of the Chapter
*/
private fun getChapterReleaseDate(element: Element): String {
val chapterReleaseDateLink = element.select(chapterReleaseDateLinkParser).attr("title")
val chapterReleaseDateI = element.select(chapterReleaseDateIParser).text()
return when {
chapterReleaseDateLink.isNotEmpty() -> chapterReleaseDateLink
chapterReleaseDateI.isNotEmpty() -> chapterReleaseDateI
else -> ""
}
}
/**
* Transform String with the Date of Release into Long format
*/
private fun parseChapterReleaseDate(releaseDateStr: String): Long {
val regExSecs = Regex("""hace\s+(\d+)\s+segundos?""")
val regExMins = Regex("""hace\s+(\d+)\s+mins?""")
val regExHours = Regex("""hace\s+(\d+)\s+horas?""")
val regExDays = Regex("""hace\s+(\d+)\s+días?""")
val regExDate = Regex("""\d+/\d+/\d+""")
return when {
regExSecs.containsMatchIn(releaseDateStr) ->
getReleaseTime(releaseDateStr, Calendar.SECOND)
regExMins.containsMatchIn(releaseDateStr) ->
getReleaseTime(releaseDateStr, Calendar.MINUTE)
regExHours.containsMatchIn(releaseDateStr) ->
getReleaseTime(releaseDateStr, Calendar.HOUR)
regExDays.containsMatchIn(releaseDateStr) ->
getReleaseTime(releaseDateStr, Calendar.DAY_OF_YEAR)
regExDate.containsMatchIn(releaseDateStr) ->
SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).parse(releaseDateStr).time
else -> 0
}
}
/**
* Extract the Release time from a Text String
* Format of the String "hace\s+\d+\s(segundo|minuto|hora|dia)s?"
*/
private fun getReleaseTime(releaseDateStr: String, timeType: Int): Long {
val releaseTimeAgo = Regex("""\d+""").find(releaseDateStr)?.value.toString().toInt()
val calendar = Calendar.getInstance()
calendar.add(timeType, -releaseTimeAgo)
return calendar.timeInMillis
}
/**
* Parses the response from the site and returns the page list.
* (Parse the comic pages from the website with the chapter)
*
* @param response the response from the site.
*/
fun getPageListParse(response: Response): List<Page> {
val list =
response.asJsoup().select(pageListParseSelector).mapIndexed { index, imgElement ->
Page(index, "", imgElement.attr("abs:data-src"))
}
return list
}
/**
* Returns the request for the search manga given the page.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
fun searchMangaRequest(page: Int, query: String, filters: FilterList): Uri.Builder {
val uri = Uri.parse(baseUrl).buildUpon()
if (query.isNotBlank()) {
searchType = SearchType.SEARCH_FREE
uri.appendQueryParameter("s", query)
.appendQueryParameter("post_type", "wp-manga")
} else {
searchType = SearchType.SEARCH_FILTER
// Append uri filters
filters.forEach {
if (it is UriFilter)
it.addToUri(uri)
}
uri.appendPath("page").appendPath(page.toString())
}
return uri
}
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val hasNextPages = hasNextPages(document)
val mangas: List<SManga>
when (searchType) {
SearchType.SEARCH_FREE ->
mangas = getMangasFromSearchSite(document)
SearchType.SEARCH_FILTER ->
mangas = getMangasFromGenreSite(document)
}
return MangasPage(mangas, hasNextPages)
}
/**
* Check if there ir another page to show
*/
fun hasNextPages(document: Document): Boolean {
return !document.select(searchMangaNextPageSelector).isEmpty()
}
/**
* Create a Address url without the base url.
*/
protected fun getUrlWithoutDomain(url: String) = url.substringAfter(baseUrl)
}

View File

@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.extension.es.manhwalatino.filters
class GenreTagFilter : UriPartFilter(
"Género o Tag",
"filtro",
arrayOf(
Pair("manga-genre/manga", "Todas"),
Pair("manga-genre/accion", "Acción"),
Pair("manga-genre/adulto", "Adulto"),
Pair("manga-genre/aventura", "Aventura"),
Pair("manga-genre/bondage", "Bondage"),
Pair("manga-genre/cambio-de-pareja", "Cambio de pareja"),
Pair("manga-genre/chantaje", "Chantaje"),
Pair("manga-genre/ciencia-ficcion", "Ciencia Ficción"),
Pair("manga-genre/comedia", "Comedia"),
Pair("manga-genre/doujinshi", "Doujinshi"),
Pair("manga-genre/drama", "Drama"),
Pair("manga-tag/lunes", "Dia Publicación Lunes"),
Pair("manga-tag/martes", "Dia Publicación Martes"),
Pair("manga-tag/miercoles", "Dia Publicación Miércoles"),
Pair("manga-tag/jueves", "Dia Publicación Jueves"),
Pair("manga-tag/viernes", "Dia Publicación Viernes"),
Pair("manga-tag/sabado", "Dia Publicación Sábado"),
Pair("manga-tag/domingo", "Dia Publicación Domingo"),
Pair("manga-genre/ecchi", "Ecchi"),
Pair("manga-tag/espanol", "Español"),
Pair("manga-genre/exhibicion", "Exhibición"),
Pair("manga-genre/familia", "Familia"),
Pair("manga-genre/fantasia", "Fantasia"),
Pair("manga-tag/fin", "Finalizado"),
Pair("manga-genre/harem", "Harem"),
Pair("manga-genre/manga", "Manga"),
Pair("manga-genre/manhua", "Manhua"),
Pair("manga-genre/manhwa", "Manhwa"),
Pair("manga-genre/misterio", "Misterio"),
Pair("manga-genre/ntr", "Ntr"),
Pair("manga-genre/obsenidad", "Obsenidad"),
Pair("manga-genre/relato vida", "Relato vida"),
Pair("manga-genre/romance", "Romance"),
Pair("manga-genre/sangre", "Sangre"),
Pair("manga-genre/sexo-forzado", "Sexo forzado"),
Pair("manga-genre/sometimiento", "Sometimiento"),
Pair("manga-genre/tragedia", "Tragedia"),
Pair("manga-genre/venganza", "Venganza"),
Pair("manga-genre/vida-escolar", "Vida Escolar"),
Pair("manga-genre/webtoon", "Webtoon")
)
)

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.extension.es.manhwalatino.filters
import android.net.Uri
/**
* Represents a filter that is able to modify a URI.
*/
interface UriFilter {
fun addToUri(uri: Uri.Builder)
}

View File

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.extension.es.manhwalatino.filters
import android.net.Uri
import eu.kanade.tachiyomi.source.model.Filter
/**
* 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 UriPartFilter(
displayName: String,
private val uriParam: String,
private val vals: Array<Pair<String, String>>,
private 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) {
val filter = vals[state].first
uri.appendPath(filter)
}
}
}