diff --git a/src/all/ninemanga/build.gradle b/src/all/ninemanga/build.gradle new file mode 100644 index 000000000..f62e0d4db --- /dev/null +++ b/src/all/ninemanga/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: NineManga' + pkgNameSuffix = "all.ninemanga" + extClass = '.NineMangaEs; .NineMangaBr; .NineMangaEn; .NineMangaRu; .NineMangaDe; .NineMangaIt; .NineMangaFr' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" \ No newline at end of file diff --git a/src/all/ninemanga/src/eu/kanade/tachiyomi/extension/all/ninemanga/NineManga.kt b/src/all/ninemanga/src/eu/kanade/tachiyomi/extension/all/ninemanga/NineManga.kt new file mode 100644 index 000000000..4f0d781af --- /dev/null +++ b/src/all/ninemanga/src/eu/kanade/tachiyomi/extension/all/ninemanga/NineManga.kt @@ -0,0 +1,127 @@ +package eu.kanade.tachiyomi.extension.all.ninemanga + +import eu.kanade.tachiyomi.source.model.* +import okhttp3.Request +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +open class NineManga(override val name: String, override val baseUrl: String, override val lang: String) : ParsedHttpSource() { + + override val supportsLatest: Boolean = true + + private fun newHeaders() = super.headersBuilder() + .add("Accept-Language", "es-ES,es;q=0.9,en;q=0.8,gl;q=0.7") + .add("Host", baseUrl.substringAfterLast("/")) // like: es.ninemanga.com + .add("Connection", "keep-alive") + .add("Upgrade-Insecure-Requests", "1") + .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) Gecko/20100101 Firefox/60") + .build() + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/list/New-Update/", headers) // "$baseUrl/category/updated_$page.html" + + override fun latestUpdatesSelector() = "ul.direlist > li" + + override fun latestUpdatesFromElement(element: Element) = SManga.create().apply { + element.select("dl.bookinfo").let { + setUrlWithoutDomain(it.select("dd > a.bookname").attr("href") + "?waring=1") // To removes warning message and shows chapter list. + title = it.select("dd > a.bookname").first().text() + thumbnail_url = it.select("dt > a > img").attr("src") + } + } + + override fun latestUpdatesNextPageSelector() = "ul.pageList > li:last-child > a.l" + + override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/index_$page.html", headers) + + override fun popularMangaSelector() = latestUpdatesSelector() + + override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element) + + override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector() + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + document.select("div.bookintro").let { + thumbnail_url = document.select("a.bookface > img").attr("src") + genre = document.select("li[itemprop=genre] > a").joinToString(", ") { + it.text() + } + author = it.select("a[itemprop=author]").text() + artist = "" + description = it.select("p[itemprop=description]").text().orEmpty() + status = parseStatus(it.select("a.red").first().text().orEmpty()) + } + } + + open fun parseStatus(status: String) = when { + status.contains("Ongoing") -> SManga.ONGOING + status.contains("Completed") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun chapterListSelector() = "ul.sub_vol_ul > li" + + override fun chapterFromElement(element: Element) = SChapter.create().apply { + element.select("a.chapter_list_a").let { + name = it.text() + setUrlWithoutDomain(it.attr("href")) + } + date_upload = parseChapterDate(element.select("span").text()) + } + + open fun parseChapterDate(date: String): Long { + val dateWords = date.split(" ") + + if (dateWords.size == 3) { + if(dateWords[1].contains(",")){ + try { + return SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time + } catch (e: ParseException) { + return 0L + } + }else { + val timeAgo = Integer.parseInt(dateWords[0]) + return Calendar.getInstance().apply { + when (dateWords[1]) { + "minutes" -> Calendar.MINUTE + "hours" -> Calendar.HOUR + else -> null + }?.let { + add(it, -timeAgo) + } + }.timeInMillis + } + } + return 0L + } + + override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url, newHeaders()) + + override fun pageListParse(document: Document): List = mutableListOf().apply { + document.select("select#page").first().select("option").forEach { + add(Page(size, baseUrl + it.attr("value"))) + } + } + + override fun imageUrlRequest(page: Page) = GET(page.url, newHeaders()) + + override fun imageUrlParse(document: Document) = document.select("div.pic_box img.manga_pic").first().attr("src").orEmpty() + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + return GET("$baseUrl/search/?name_sel=&wd=$query&author_sel=&author=&artist_sel=&artist=&category_id=&out_category_id=&completed_series=&page=$page.html", headers) + } + + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + //TODO: Implement filters list. + // Array.from(document.querySelectorAll('.optionbox .typelist:nth-child(3) ul')).map(a => Array.from(a.querySelectorAll('li')).map(b => `Genre("${b.querySelector('label').innerText}", "${a.querySelector('li[cate_id]').getAttribute('cate_id')}")`)).join(',\n') + // http://es.ninemanga.com/search/?name_sel=contain&wd=&author_sel=contain&author=&artist_sel=contain&artist=&category_id=&out_category_id=&completed_series=either&type=high +} \ No newline at end of file diff --git a/src/all/ninemanga/src/eu/kanade/tachiyomi/extension/all/ninemanga/NineMangaFactory.kt b/src/all/ninemanga/src/eu/kanade/tachiyomi/extension/all/ninemanga/NineMangaFactory.kt new file mode 100644 index 000000000..656a68a37 --- /dev/null +++ b/src/all/ninemanga/src/eu/kanade/tachiyomi/extension/all/ninemanga/NineMangaFactory.kt @@ -0,0 +1,229 @@ +package eu.kanade.tachiyomi.extension.all.ninemanga + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory +import eu.kanade.tachiyomi.source.model.SManga +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +class NineMangaFactory : SourceFactory { + override fun createSources(): List = getAllNineManga() +} + +fun getAllNineManga(): List { + return listOf( + NineMangaEn(), + NineMangaEs(), + NineMangaBr(), + NineMangaRu(), + NineMangaDe(), + NineMangaIt(), + NineMangaFr() + ) +} + +class NineMangaEn : NineManga("NineMangaEn", "http://en.ninemanga.com", "en") + +class NineMangaEs : NineManga("NineMangaEs", "http://es.ninemanga.com", "es") { + override fun parseStatus(status: String) = when { + status.contains("En curso") -> SManga.ONGOING + status.contains("Completado") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun parseChapterDate(date: String): Long { + val dateWords = date.split(" ") + + if (dateWords.size == 3) { + if(dateWords[1].contains(",")){ + try { + return SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time + } catch (e: ParseException) { + return 0L + } + }else{ + val timeAgo = Integer.parseInt(dateWords[0]) + return Calendar.getInstance().apply { + when (dateWords[1]) { + "minutos" -> Calendar.MINUTE + "horas" -> Calendar.HOUR + else -> null + }?.let { + add(it, -timeAgo) + } + }.timeInMillis + } + } + return 0L + } +} + +class NineMangaBr : NineManga("NineMangaBr", "http://br.ninemanga.com", "br") { + override fun parseStatus(status: String) = when { + status.contains("Em tradução") -> SManga.ONGOING + status.contains("Completo") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun parseChapterDate(date: String): Long { + val dateWords = date.split(" ") + + if (dateWords.size == 3) { + if(dateWords[1].contains(",")){ + try { + return SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time + } catch (e: ParseException) { + return 0L + } + }else{ + val timeAgo = Integer.parseInt(dateWords[0]) + return Calendar.getInstance().apply { + when (dateWords[1]) { + "minutos" -> Calendar.MINUTE + "hora" -> Calendar.HOUR + else -> null + }?.let { + add(it, -timeAgo) + } + }.timeInMillis + } + } + return 0L + } +} + +class NineMangaRu : NineManga("NineMangaRu", "http://ru.ninemanga.com", "ru") { + override fun parseStatus(status: String) = when { + // No Ongoing status + status.contains("завершенный") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun parseChapterDate(date: String): Long { + val dateWords = date.split(" ") + + if (dateWords.size == 3) { + if(dateWords[1].contains(",")){ + try { + return SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time + } catch (e: ParseException) { + return 0L + } + }else{ + val timeAgo = Integer.parseInt(dateWords[0]) + return Calendar.getInstance().apply { + when (dateWords[1]) { + "минут" -> Calendar.MINUTE + "часа" -> Calendar.HOUR + else -> null + }?.let { + add(it, -timeAgo) + } + }.timeInMillis + } + } + return 0L + } +} + +class NineMangaDe : NineManga("NineMangaDe", "http://de.ninemanga.com", "de") { + override fun parseStatus(status: String) = when { + status.contains("Laufende") -> SManga.ONGOING + status.contains("Abgeschlossen") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun parseChapterDate(date: String): Long { + val dateWords = date.split(" ") + + if (dateWords.size == 3) { + try { + return SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time + } catch (e: ParseException) { + return 0L + } + } + else if (dateWords.size == 2) { // Aleman + val timeAgo = Integer.parseInt(dateWords[0]) + return Calendar.getInstance().apply { + when (dateWords[1]) { + "Stunden" -> Calendar.HOUR // Aleman - 2 palabras + else -> null + }?.let { + add(it, -timeAgo) + } + }.timeInMillis + } + return 0L + } +} + +class NineMangaIt : NineManga("NineMangaIt", "http://it.ninemanga.com", "it") { + override fun parseStatus(status: String) = when { + status.contains("In corso") -> SManga.ONGOING + status.contains("Completato") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun parseChapterDate(date: String): Long { + val dateWords = date.split(" ") + + if (dateWords.size == 3) { + if(!dateWords[1].contains(",")){ + try { + return SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time + } catch (e: ParseException) { + return 0L + } + }else{ + val timeAgo = Integer.parseInt(dateWords[0]) + return Calendar.getInstance().apply { + when (dateWords[1]) { + "minuti" -> Calendar.MINUTE + "ore" -> Calendar.HOUR + else -> null + }?.let { + add(it, -timeAgo) + } + }.timeInMillis + } + } + return 0L + } +} + +class NineMangaFr : NineManga("NineMangaFr", "http://fr.ninemanga.com", "fr") { + override fun parseStatus(status: String) = when { + status.contains("En cours") -> SManga.ONGOING + status.contains("Complété") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun parseChapterDate(date: String): Long { + val dateWords = date.split(" ") + + if (dateWords.size == 3) { + try { + return SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(date).time + } catch (e: ParseException) { + return 0L + } + } + + else if(dateWords.size == 5) { + val timeAgo = Integer.parseInt(dateWords[3]) + return Calendar.getInstance().apply { + when (dateWords[4]) { + "minutes" -> Calendar.MINUTE + "heures" -> Calendar.HOUR + else -> null + }?.let { + add(it, -timeAgo) + } + }.timeInMillis + } + return 0L + } +} +