Likemanga (#17597)
* LikeManga
* fix ajax chapter list pages
* nullable genre parsing
* ratelimit and cleanup
* trailing slash in referer
is it needed tho 🤔
* remove package from AndroidManifest
			
			
This commit is contained in:
		
							parent
							
								
									31d420cd50
								
							
						
					
					
						commit
						9f2e7468ce
					
				
							
								
								
									
										2
									
								
								src/en/likemanga/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/en/likemanga/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<manifest />
 | 
			
		||||
							
								
								
									
										12
									
								
								src/en/likemanga/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/en/likemanga/build.gradle
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
apply plugin: 'com.android.application'
 | 
			
		||||
apply plugin: 'kotlin-android'
 | 
			
		||||
apply plugin: 'kotlinx-serialization'
 | 
			
		||||
 | 
			
		||||
ext {
 | 
			
		||||
    extName = 'LikeManga'
 | 
			
		||||
    pkgNameSuffix = 'en.likemanga'
 | 
			
		||||
    extClass = '.LikeManga'
 | 
			
		||||
    extVersionCode = 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$rootDir/common.gradle"
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/en/likemanga/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/likemanga/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/likemanga/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/likemanga/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/likemanga/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/likemanga/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 6.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/likemanga/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/likemanga/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 12 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/likemanga/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/likemanga/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 18 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/en/likemanga/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/en/likemanga/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 95 KiB  | 
@ -0,0 +1,276 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.en.likemanga
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
 | 
			
		||||
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 kotlinx.serialization.json.Json
 | 
			
		||||
import kotlinx.serialization.json.jsonObject
 | 
			
		||||
import kotlinx.serialization.json.jsonPrimitive
 | 
			
		||||
import okhttp3.HttpUrl.Companion.toHttpUrl
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import org.jsoup.Jsoup
 | 
			
		||||
import org.jsoup.nodes.Document
 | 
			
		||||
import org.jsoup.nodes.Element
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
import java.text.SimpleDateFormat
 | 
			
		||||
import java.util.Locale
 | 
			
		||||
 | 
			
		||||
class LikeManga : ParsedHttpSource() {
 | 
			
		||||
 | 
			
		||||
    override val name = "LikeManga"
 | 
			
		||||
 | 
			
		||||
    override val lang = "en"
 | 
			
		||||
 | 
			
		||||
    override val baseUrl = "https://likemanga.io"
 | 
			
		||||
 | 
			
		||||
    override val supportsLatest = true
 | 
			
		||||
 | 
			
		||||
    override val client = network.cloudflareClient.newBuilder()
 | 
			
		||||
        .rateLimit(1, 2)
 | 
			
		||||
        .build()
 | 
			
		||||
 | 
			
		||||
    override fun headersBuilder() = super.headersBuilder()
 | 
			
		||||
        .add("Referer", "$baseUrl/")
 | 
			
		||||
 | 
			
		||||
    private val json: Json by injectLazy()
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaRequest(page: Int): Request {
 | 
			
		||||
        return searchMangaRequest(page, "", FilterList(SortFilter("top-manga")))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaParse(response: Response) = searchMangaParse(response)
 | 
			
		||||
    override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
 | 
			
		||||
    override fun popularMangaSelector() = searchMangaSelector()
 | 
			
		||||
    override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesRequest(page: Int): Request {
 | 
			
		||||
        return searchMangaRequest(page, "", FilterList(SortFilter("lastest-chap")))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
 | 
			
		||||
    override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
 | 
			
		||||
    override fun latestUpdatesSelector() = searchMangaSelector()
 | 
			
		||||
    override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
			
		||||
        val url = baseUrl.toHttpUrl().newBuilder().apply {
 | 
			
		||||
            addQueryParameter("act", "searchadvance")
 | 
			
		||||
            filters.forEach { filter ->
 | 
			
		||||
                when (filter) {
 | 
			
		||||
                    is GenreFilter -> {
 | 
			
		||||
                        filter.checked?.forEach {
 | 
			
		||||
                            addQueryParameter("f[genres][]", it)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    is ChapterCountFilter -> {
 | 
			
		||||
                        filter.selected?.let {
 | 
			
		||||
                            addQueryParameter("f[min_num_chapter]", it)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    is StatusFilter -> {
 | 
			
		||||
                        filter.selected?.let {
 | 
			
		||||
                            addQueryParameter("f[status]", it)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    is SortFilter -> {
 | 
			
		||||
                        filter.selected?.let {
 | 
			
		||||
                            addQueryParameter("f[sortby]", it)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    else -> {}
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (query.isNotEmpty()) {
 | 
			
		||||
                addQueryParameter("f[keyword]", query.trim())
 | 
			
		||||
            }
 | 
			
		||||
            if (page > 1) {
 | 
			
		||||
                addQueryParameter("pageNum", page.toString())
 | 
			
		||||
            }
 | 
			
		||||
        }.build()
 | 
			
		||||
 | 
			
		||||
        return GET(url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var genresList: List<Pair<String, String>> = emptyList()
 | 
			
		||||
 | 
			
		||||
    private fun parseGenres(document: Document): List<Pair<String, String>> {
 | 
			
		||||
        return document.selectFirst("div.search_genres")
 | 
			
		||||
            ?.select("div.form-check")
 | 
			
		||||
            .orEmpty()
 | 
			
		||||
            .mapNotNull {
 | 
			
		||||
                val label = it.selectFirst("label")
 | 
			
		||||
                    ?.text()?.trim() ?: return@mapNotNull null
 | 
			
		||||
 | 
			
		||||
                val value = it.selectFirst("input")
 | 
			
		||||
                    ?.attr("value") ?: return@mapNotNull null
 | 
			
		||||
 | 
			
		||||
                Pair(label, value)
 | 
			
		||||
            }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getFilterList(): FilterList {
 | 
			
		||||
        val filters: MutableList<Filter<*>> = mutableListOf(
 | 
			
		||||
            SortFilter(),
 | 
			
		||||
            StatusFilter(),
 | 
			
		||||
            ChapterCountFilter(),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        filters += if (genresList.isEmpty()) {
 | 
			
		||||
            listOf(
 | 
			
		||||
                Filter.Separator(),
 | 
			
		||||
                Filter.Header("Press 'reset' to attempt to show Genres"),
 | 
			
		||||
            )
 | 
			
		||||
        } else {
 | 
			
		||||
            listOf(
 | 
			
		||||
                GenreFilter("Genre", genresList),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return FilterList(filters)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaParse(response: Response): MangasPage {
 | 
			
		||||
        if (genresList.isEmpty()) {
 | 
			
		||||
            val document = response.peekBody(Long.MAX_VALUE).string()
 | 
			
		||||
                .let { Jsoup.parse(it, response.request.url.toString()) }
 | 
			
		||||
 | 
			
		||||
            genresList = parseGenres(document)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return super.searchMangaParse(response)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaFromElement(element: Element) = SManga.create().apply {
 | 
			
		||||
        setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
 | 
			
		||||
        thumbnail_url = element.selectFirst("img")?.imgAttr()
 | 
			
		||||
        title = element.select(".title-manga").text()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaSelector() = "div.card-body div.card"
 | 
			
		||||
    override fun searchMangaNextPageSelector() = "ul.pagination a:contains(»)"
 | 
			
		||||
 | 
			
		||||
    override fun mangaDetailsParse(document: Document) = SManga.create().apply {
 | 
			
		||||
        title = document.select("#title-detail-manga").text()
 | 
			
		||||
        thumbnail_url = document.selectFirst(".detail-info img")?.imgAttr()
 | 
			
		||||
        description = document.selectFirst("#summary_shortened")?.text()?.trim()
 | 
			
		||||
        genre = document.select(".list-info a[href*=/genres/]").joinToString { it.text() }
 | 
			
		||||
        status = document.selectFirst(".list-info .status p:nth-child(2)")?.text().parseStatus()
 | 
			
		||||
        author = document.selectFirst(".list-info .author p:nth-child(2)")?.text()
 | 
			
		||||
            ?.takeUnless { it.trim() == "Updating" }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun String?.parseStatus(): Int {
 | 
			
		||||
        if (this == null) return SManga.UNKNOWN
 | 
			
		||||
 | 
			
		||||
        return when {
 | 
			
		||||
            contains("Complete", true) -> SManga.COMPLETED
 | 
			
		||||
            contains("In process", true) -> SManga.ONGOING
 | 
			
		||||
            contains("Pause", true) -> SManga.ON_HIATUS
 | 
			
		||||
            else -> SManga.UNKNOWN
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun chapterListParse(response: Response): List<SChapter> {
 | 
			
		||||
        val document = response.use { it.asJsoup() }
 | 
			
		||||
 | 
			
		||||
        val chapters = document.select(chapterListSelector())
 | 
			
		||||
            .map(::chapterFromElement)
 | 
			
		||||
            .toMutableList()
 | 
			
		||||
 | 
			
		||||
        val lastPage = document.select("div.chapters_pagination a:not(.next)").last()
 | 
			
		||||
            ?.attr("onclick")
 | 
			
		||||
            ?.substringAfter("(")
 | 
			
		||||
            ?.substringBefore(")")
 | 
			
		||||
            ?.toIntOrNull()
 | 
			
		||||
            ?: return chapters
 | 
			
		||||
 | 
			
		||||
        val id = document.select("#title-detail-manga").attr("data-manga")
 | 
			
		||||
            .toIntOrNull() ?: return chapters
 | 
			
		||||
 | 
			
		||||
        for (page in 2..lastPage) {
 | 
			
		||||
            chapters.addAll(fetchAjaxChapterList(id, page))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return chapters
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun fetchAjaxChapterList(id: Int, page: Int): List<SChapter> {
 | 
			
		||||
        val request = ajaxChapterListRequest(id, page)
 | 
			
		||||
        val response = client.newCall(request).execute()
 | 
			
		||||
 | 
			
		||||
        if (!response.isSuccessful) {
 | 
			
		||||
            response.close()
 | 
			
		||||
            return emptyList()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ajaxChapterListParse(response)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun ajaxChapterListRequest(id: Int, page: Int): Request {
 | 
			
		||||
        val url = baseUrl.toHttpUrl().newBuilder().apply {
 | 
			
		||||
            addQueryParameter("act", "ajax")
 | 
			
		||||
            addQueryParameter("code", "load_list_chapter")
 | 
			
		||||
            addQueryParameter("manga_id", id.toString())
 | 
			
		||||
            addQueryParameter("page_num", page.toString())
 | 
			
		||||
            addQueryParameter("chap_id", "0")
 | 
			
		||||
            addQueryParameter("keyword", "")
 | 
			
		||||
        }.build()
 | 
			
		||||
 | 
			
		||||
        return GET(url, headers)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun ajaxChapterListParse(response: Response): List<SChapter> {
 | 
			
		||||
        val responseJson = response.use { json.parseToJsonElement(it.body.string()) }.jsonObject
 | 
			
		||||
        val htmlString = responseJson["list_chap"]!!.jsonPrimitive.content
 | 
			
		||||
        val document = Jsoup.parseBodyFragment(htmlString, response.request.url.toString())
 | 
			
		||||
 | 
			
		||||
        return document.select(chapterListSelector())
 | 
			
		||||
            .map(::chapterFromElement)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun chapterFromElement(element: Element) = SChapter.create().apply {
 | 
			
		||||
        setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
 | 
			
		||||
        name = element.select("a").text()
 | 
			
		||||
        date_upload = element.selectFirst(".chapter-release-date")?.text().parseDate()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun chapterListSelector() = ".wp-manga-chapter"
 | 
			
		||||
 | 
			
		||||
    private fun String?.parseDate(): Long {
 | 
			
		||||
        return runCatching {
 | 
			
		||||
            dateFormat.parse(this!!)!!.time
 | 
			
		||||
        }.getOrDefault(0L)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun pageListParse(document: Document): List<Page> {
 | 
			
		||||
        return document.select(".reading-detail img:not(noscript img)").mapIndexed { i, img ->
 | 
			
		||||
            Page(i, "", img.imgAttr())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun Element.imgAttr(): String? {
 | 
			
		||||
        return when {
 | 
			
		||||
            hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
 | 
			
		||||
            hasAttr("data-src") -> attr("abs:data-src")
 | 
			
		||||
            hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
 | 
			
		||||
            hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
 | 
			
		||||
            else -> attr("abs:src")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not Used")
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        val dateFormat by lazy {
 | 
			
		||||
            SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,71 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.en.likemanga
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
			
		||||
 | 
			
		||||
abstract class SelectFilter(
 | 
			
		||||
    name: String,
 | 
			
		||||
    private val options: List<Pair<String, String>>,
 | 
			
		||||
    defaultValue: String? = null,
 | 
			
		||||
) : Filter.Select<String>(
 | 
			
		||||
    name,
 | 
			
		||||
    options.map { it.first }.toTypedArray(),
 | 
			
		||||
    options.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
 | 
			
		||||
) {
 | 
			
		||||
    val selected get() = options[state].second.takeUnless { it.isEmpty() }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class CheckBoxFilter(
 | 
			
		||||
    name: String,
 | 
			
		||||
    val value: String,
 | 
			
		||||
) : Filter.CheckBox(name)
 | 
			
		||||
 | 
			
		||||
class GenreFilter(
 | 
			
		||||
    name: String,
 | 
			
		||||
    genres: List<Pair<String, String>>,
 | 
			
		||||
) : Filter.Group<CheckBoxFilter>(
 | 
			
		||||
    name,
 | 
			
		||||
    genres.map { CheckBoxFilter(it.first, it.second) },
 | 
			
		||||
) {
 | 
			
		||||
    val checked get() = state.filter { it.state }.map { it.value }.takeUnless { it.isEmpty() }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SortFilter(default: String? = null) : SelectFilter(
 | 
			
		||||
    "Sort By",
 | 
			
		||||
    listOf(
 | 
			
		||||
        Pair("", ""),
 | 
			
		||||
        Pair("Lasted update", "lastest-chap"),
 | 
			
		||||
        Pair("Lasted manga", "lastest-manga"),
 | 
			
		||||
        Pair("Top all", "top-manga"),
 | 
			
		||||
        Pair("Top month", "top-month"),
 | 
			
		||||
        Pair("Top week", "top-week"),
 | 
			
		||||
        Pair("Top day", "top-day"),
 | 
			
		||||
        Pair("Follow", "follow"),
 | 
			
		||||
        Pair("Comments", "comment"),
 | 
			
		||||
        Pair("Number of Chapters", "num-chap"),
 | 
			
		||||
    ),
 | 
			
		||||
    default,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
class StatusFilter : SelectFilter(
 | 
			
		||||
    "Status",
 | 
			
		||||
    listOf(
 | 
			
		||||
        Pair("All", ""),
 | 
			
		||||
        Pair("Complete", "Complete"),
 | 
			
		||||
        Pair("In process", "In process"),
 | 
			
		||||
        Pair("Pause", "Pause"),
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
class ChapterCountFilter : SelectFilter(
 | 
			
		||||
    "Number of Chapters",
 | 
			
		||||
    listOf(
 | 
			
		||||
        Pair("", ""),
 | 
			
		||||
        Pair(">= 0 chapter", "1"),
 | 
			
		||||
        Pair(">= 50 chapter", "50"),
 | 
			
		||||
        Pair(">= 100 chapter", "100"),
 | 
			
		||||
        Pair(">= 200 chapter", "200"),
 | 
			
		||||
        Pair(">= 300 chapter", "300"),
 | 
			
		||||
        Pair(">= 400 chapter", "400"),
 | 
			
		||||
        Pair(">= 500 chapter", "500"),
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user