diff --git a/src/all/wpcomics/build.gradle b/src/all/wpcomics/build.gradle new file mode 100644 index 000000000..b57c1ecca --- /dev/null +++ b/src/all/wpcomics/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: WP-Comics' + pkgNameSuffix = 'all.wpcomics' + extClass = '.WPComicsFactory' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/wpcomics/res/mipmap-hdpi/ic_launcher.png b/src/all/wpcomics/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..9603fde52 Binary files /dev/null and b/src/all/wpcomics/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/wpcomics/res/mipmap-mdpi/ic_launcher.png b/src/all/wpcomics/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..76111fee9 Binary files /dev/null and b/src/all/wpcomics/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/wpcomics/res/mipmap-xhdpi/ic_launcher.png b/src/all/wpcomics/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..c50706d45 Binary files /dev/null and b/src/all/wpcomics/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/wpcomics/res/mipmap-xxhdpi/ic_launcher.png b/src/all/wpcomics/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..14a0b2db2 Binary files /dev/null and b/src/all/wpcomics/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/wpcomics/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/wpcomics/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..03bbb5fb1 Binary files /dev/null and b/src/all/wpcomics/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/wpcomics/res/web_hi_res_512.png b/src/all/wpcomics/res/web_hi_res_512.png new file mode 100644 index 000000000..dcbd2befb Binary files /dev/null and b/src/all/wpcomics/res/web_hi_res_512.png differ diff --git a/src/all/wpcomics/src/eu/kanade/tachiyomi/extension/all/wpcomics/WPComics.kt b/src/all/wpcomics/src/eu/kanade/tachiyomi/extension/all/wpcomics/WPComics.kt new file mode 100644 index 000000000..f244e82b4 --- /dev/null +++ b/src/all/wpcomics/src/eu/kanade/tachiyomi/extension/all/wpcomics/WPComics.kt @@ -0,0 +1,156 @@ +package eu.kanade.tachiyomi.extension.all.wpcomics + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.Calendar + +abstract class WPComics( + override val name: String, + override val baseUrl: String, + override val lang: String, + private val dateFormat: SimpleDateFormat = SimpleDateFormat("HH:mm - dd/MM/yyyy Z", Locale.US), + private val gmtOffset: String? = "+0500" +) : ParsedHttpSource() { + + override val supportsLatest = true + + override val client: OkHttpClient = network.cloudflareClient + + // Popular + + open val popularPath = "hot" + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/$popularPath" + if (page > 1) "?page=$page" else "", headers) + } + + override fun popularMangaSelector() = "div.items div.item" + + override fun popularMangaFromElement(element: Element): SManga { + return SManga.create().apply { + element.select("h3 a").let { + title = it.text() + setUrlWithoutDomain(it.attr("abs:href")) + } + thumbnail_url = imageOrNull(element.select("div.image:first-of-type img").first()) + + } + } + + override fun popularMangaNextPageSelector() = "a.next-page" + + // Latest + + override fun latestUpdatesRequest(page: Int): Request { + return GET(baseUrl + if (page > 1) "?page=$page" else "", headers) + } + + override fun latestUpdatesSelector() = popularMangaSelector() + + override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + + // Search + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + return GET("$baseUrl/?s=$query&post_type=comics&page=$page") + } + + override fun searchMangaSelector() = "div.items div.item div.image a" + + override fun searchMangaFromElement(element: Element): SManga { + return SManga.create().apply { + title = element.attr("title") + setUrlWithoutDomain(element.attr("href")) + thumbnail_url = imageOrNull(element.select("img").first()) + } + } + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + // Details + + override fun mangaDetailsParse(document: Document): SManga { + return SManga.create().apply { + document.select("article#item-detail").let { info -> + author = info.select("li.author p.col-xs-8").text() + status = info.select("li.status p.col-xs-8").text().toStatus() + genre = info.select("li.kind p.col-xs-8 a").joinToString { it.text() } + description = info.select("div.detail-content p").text() + thumbnail_url = imageOrNull(info.select("div.col-image img").first()) + } + } + } + + private fun String?.toStatus() = when { + this == null -> SManga.UNKNOWN + this.contains("Updating", ignoreCase = true) -> SManga.ONGOING + this.contains("Complete", ignoreCase = true) -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + // Chapters + + override fun chapterListSelector() = "div.list-chapter li.row:not(.heading)" + + override fun chapterFromElement(element: Element): SChapter { + return SChapter.create().apply { + element.select("a").let { + name = it.text() + setUrlWithoutDomain(it.attr("href")) + } + date_upload = element.select("div.col-xs-4").text().toDate() + } + } + + private fun String?.toDate(): Long { + return try { + if (this?.contains("ago", ignoreCase = true) == true) { + val trimmedDate = this.substringBefore(" ago").removeSuffix("s").split(" ") + val calendar = Calendar.getInstance() + + when (trimmedDate[1]) { + "day" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) } + "hour" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) } + "minute" -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) } + "second" -> calendar.apply { add(Calendar.SECOND, -trimmedDate[0].toInt()) } + } + + calendar.timeInMillis + } else { + dateFormat.parse(if (gmtOffset == null) this?.substringAfterLast(" ") else "$this $gmtOffset").time + } + } catch (_: Exception) { + 0L + } + } + + // Pages + + // sources sometimes have an image element with an empty attr that isn't really an image + private fun imageOrNull(element: Element): String? { + return when { + element.attr("data-original").contains(Regex("""\.(jpg|png)""", RegexOption.IGNORE_CASE)) -> element.attr("abs:data-original") + element.attr("data-src").contains(Regex("""\.(jpg|png)""", RegexOption.IGNORE_CASE)) -> element.attr("abs:data-src") + element.attr("src").contains(Regex("""\.(jpg|png)""", RegexOption.IGNORE_CASE)) -> element.attr("abs:src") + else -> null + } + } + + open val pageListSelector = "div.page-chapter > img, li.blocks-gallery-item img" + + override fun pageListParse(document: Document): List { + return document.select(pageListSelector).mapNotNull { img -> imageOrNull(img) } + .mapIndexed { i, image -> Page(i, "", image)} + } + + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") +} diff --git a/src/all/wpcomics/src/eu/kanade/tachiyomi/extension/all/wpcomics/WPComicsFactory.kt b/src/all/wpcomics/src/eu/kanade/tachiyomi/extension/all/wpcomics/WPComicsFactory.kt new file mode 100644 index 000000000..b36603675 --- /dev/null +++ b/src/all/wpcomics/src/eu/kanade/tachiyomi/extension/all/wpcomics/WPComicsFactory.kt @@ -0,0 +1,55 @@ +package eu.kanade.tachiyomi.extension.all.wpcomics + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.SManga +import okhttp3.Request +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.Locale + +class WPComicsFactory : SourceFactory { + override fun createSources(): List = listOf( + ManhuaPlus(), + ManhuaES(), + MangaSum() + ) +} + +private class ManhuaPlus : WPComics("Manhua Plus", "https://manhuaplus.com", "en") + +private class ManhuaES : WPComics("Manhua ES", "https://manhuaes.com", "en", SimpleDateFormat("HH:mm - dd/MM/yyyy Z", Locale.US), "+0700") { + override val popularPath = "category-comics/manga" + + override fun popularMangaFromElement(element: Element): SManga { + return SManga.create().apply { + element.select("div.image a").let { + title = it.attr("title") + setUrlWithoutDomain(it.attr("href")) + } + thumbnail_url = element.select("div.image img").attr("abs:src") + } + } + + override fun latestUpdatesFromElement(element: Element): SManga { + return SManga.create().apply { + element.select("a.head").let { + title = it.text() + setUrlWithoutDomain(it.attr("href")) + } + thumbnail_url = element.select("img").attr("abs:src") + } + } + + override val pageListSelector = "div.chapter-detail img" +} + +private class MangaSum : WPComics("MangaSum", "https://mangasum.com", "en", SimpleDateFormat("MM/dd/yy", Locale.US), null) { + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/genres?keyword=$query&page=$page", headers) + /** + * TODO - chapter dates come in 3 flavors: relative dates less than a month, time + month/day (current year is implied), + * and MM/dd/yy; see about getting all 3 working (currently at 2/3) + */ +}