add Masonry multisrc (#1007)
* Masonry Multisrc * fixes * explicit `imageUrl` Co-authored-by: beerpsi <92439990+beerpiss@users.noreply.github.com> --------- Co-authored-by: beerpsi <92439990+beerpiss@users.noreply.github.com>
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,199 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.masonry
|
||||||
|
|
||||||
|
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.model.UpdateStrategy
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import rx.Observable
|
||||||
|
import java.lang.UnsupportedOperationException
|
||||||
|
|
||||||
|
abstract class Masonry(
|
||||||
|
override val name: String,
|
||||||
|
override val baseUrl: String,
|
||||||
|
override val lang: String,
|
||||||
|
) : ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
val url = when (page) {
|
||||||
|
1 -> baseUrl
|
||||||
|
2 -> "$baseUrl/archive/"
|
||||||
|
else -> "$baseUrl/archive/page/${page - 1}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
getTags()
|
||||||
|
return super.popularMangaParse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = ".list-gallery:not(.static) figure:not(:has(a[href*=/video/]))"
|
||||||
|
override fun popularMangaNextPageSelector() = ".pagination-a li.next"
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
|
element.selectFirst("a")!!.also {
|
||||||
|
setUrlWithoutDomain(it.absUrl("href"))
|
||||||
|
title = it.attr("title")
|
||||||
|
}
|
||||||
|
thumbnail_url = element.selectFirst("img")?.imgAttr()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/updates/sort/newest/mpage/$page/", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||||
|
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||||
|
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
return if (query.isNotEmpty()) {
|
||||||
|
val url = "$baseUrl/search/post/".toHttpUrl().newBuilder()
|
||||||
|
.addPathSegment(query.trim())
|
||||||
|
.addEncodedPathSegments("mpage/$page/")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
GET(url, headers)
|
||||||
|
} else {
|
||||||
|
val tagFilter = filters.filterIsInstance<TagFilter>().firstOrNull()
|
||||||
|
val sortFilter = filters.filterIsInstance<SortFilter>().first()
|
||||||
|
|
||||||
|
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||||
|
if (tagFilter == null || tagFilter.selected == "") {
|
||||||
|
addPathSegment("updates")
|
||||||
|
sortFilter.getUriPartIfNeeded("search").also {
|
||||||
|
if (it.isBlank()) {
|
||||||
|
addEncodedPathSegments("page/$page/")
|
||||||
|
} else {
|
||||||
|
addEncodedPathSegments(it)
|
||||||
|
addEncodedPathSegments("mpage/$page/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addPathSegment("tag")
|
||||||
|
addPathSegment(tagFilter.selected)
|
||||||
|
sortFilter.getUriPartIfNeeded("tag").also {
|
||||||
|
if (it.isBlank()) {
|
||||||
|
addEncodedPathSegments("page/$page/")
|
||||||
|
} else {
|
||||||
|
addEncodedPathSegments(it)
|
||||||
|
addEncodedPathSegments("mpage/$page/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
GET(url, headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var tags = emptyList<Pair<String, String>>()
|
||||||
|
private var tagsFetchAttempt = 0
|
||||||
|
|
||||||
|
private fun getTags() {
|
||||||
|
if (tags.isEmpty() && tagsFetchAttempt < 3) {
|
||||||
|
runCatching {
|
||||||
|
tags = client.newCall(GET("$baseUrl/updates/sort/newest/", headers))
|
||||||
|
.execute().asJsoup()
|
||||||
|
.select("#filter-a span:has(> input)")
|
||||||
|
.mapNotNull {
|
||||||
|
Pair(
|
||||||
|
it.select("label").text(),
|
||||||
|
it.select("input").attr("value"),
|
||||||
|
)
|
||||||
|
}.let {
|
||||||
|
listOf(Pair("", "")) + it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tagsFetchAttempt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
val filters = mutableListOf(
|
||||||
|
Filter.Header("Filters ignored with text search"),
|
||||||
|
Filter.Separator(),
|
||||||
|
SortFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (tags.isEmpty()) {
|
||||||
|
filters.add(
|
||||||
|
Filter.Header("Press 'reset' to attempt to load tags"),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
filters.add(
|
||||||
|
TagFilter(tags),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterList(filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||||
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
document.selectFirst("p.link-btn")?.run {
|
||||||
|
artist = select("a[href*=/model/]").eachText().joinToString()
|
||||||
|
genre = select("a[href*=/tag/]").eachText().joinToString()
|
||||||
|
author = selectFirst("a")?.text()
|
||||||
|
}
|
||||||
|
description = document.selectFirst("#content > p")?.text()
|
||||||
|
status = SManga.COMPLETED
|
||||||
|
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
return Observable.just(
|
||||||
|
listOf(
|
||||||
|
SChapter.create().apply {
|
||||||
|
name = "Gallery"
|
||||||
|
url = manga.url
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = throw UnsupportedOperationException()
|
||||||
|
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return document.select(".list-gallery a[href^=https://cdn.]").mapIndexed { idx, img ->
|
||||||
|
Page(idx, imageUrl = img.absUrl("href"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
protected fun Element.imgAttr(): String? {
|
||||||
|
return when {
|
||||||
|
hasAttr("srcset") -> attr("abs:srcset").substringBefore(" ")
|
||||||
|
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
|
||||||
|
hasAttr("data-src") -> attr("abs:data-src")
|
||||||
|
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||||
|
else -> attr("abs:src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.masonry
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
abstract class SelectFilter(
|
||||||
|
name: String,
|
||||||
|
private val options: List<Pair<String, String>>,
|
||||||
|
) : Filter.Select<String>(
|
||||||
|
name,
|
||||||
|
options.map { it.first }.toTypedArray(),
|
||||||
|
) {
|
||||||
|
val selected get() = options[state].second
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortFilter : SelectFilter("Sort by", sortFilterOptions) {
|
||||||
|
fun getUriPartIfNeeded(part: String) =
|
||||||
|
when (part) {
|
||||||
|
"search" -> {
|
||||||
|
when (state) {
|
||||||
|
2 -> ""
|
||||||
|
else -> selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"tag" -> {
|
||||||
|
when (state) {
|
||||||
|
0 -> ""
|
||||||
|
else -> selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val sortFilterOptions = listOf(
|
||||||
|
Pair("Trending", "sort/trending"),
|
||||||
|
Pair("Newest", "sort/newest"),
|
||||||
|
Pair("Popular", "sort/popular"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class TagFilter(options: List<Pair<String, String>>) : SelectFilter("Tags", options)
|
|
@ -0,0 +1,30 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.masonry
|
||||||
|
|
||||||
|
import generator.ThemeSourceData.SingleLang
|
||||||
|
import generator.ThemeSourceGenerator
|
||||||
|
|
||||||
|
class MasonryGenerator : ThemeSourceGenerator {
|
||||||
|
|
||||||
|
override val themePkg = "masonry"
|
||||||
|
|
||||||
|
override val themeClass = "Masonry"
|
||||||
|
|
||||||
|
override val baseVersionCode = 1
|
||||||
|
|
||||||
|
override val sources = listOf(
|
||||||
|
SingleLang("Elite Babes", "https://www.elitebabes.com", "all", isNsfw = true),
|
||||||
|
SingleLang("Femjoy Hunter", "https://www.femjoyhunter.com", "all", isNsfw = true),
|
||||||
|
SingleLang("FTV Hunter", "https://www.ftvhunter.com", "all", isNsfw = true),
|
||||||
|
SingleLang("Joymii Hub", "https://www.joymiihub.com", "all", isNsfw = true),
|
||||||
|
SingleLang("Metart Hunter", "https://www.metarthunter.com", "all", isNsfw = true),
|
||||||
|
SingleLang("Playmate Hunter", "https://pmatehunter.com", "all", isNsfw = true),
|
||||||
|
SingleLang("XArt Hunter", "https://www.xarthunter.com", "all", isNsfw = true),
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
MasonryGenerator().createAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|