parent
9df8e25b0e
commit
f5f2fb636a
@ -5,7 +5,7 @@ ext {
|
|||||||
appName = 'Tachiyomi: Sen Manga'
|
appName = 'Tachiyomi: Sen Manga'
|
||||||
pkgNameSuffix = 'ja.senmanga'
|
pkgNameSuffix = 'ja.senmanga'
|
||||||
extClass = '.SenManga'
|
extClass = '.SenManga'
|
||||||
extVersionCode = 3
|
extVersionCode = 4
|
||||||
libVersion = '1.2'
|
libVersion = '1.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi.extension.ja.senmanga
|
package eu.kanade.tachiyomi.extension.ja.senmanga
|
||||||
|
|
||||||
import android.net.Uri
|
import android.annotation.SuppressLint
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.model.*
|
import eu.kanade.tachiyomi.source.model.*
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.Response
|
import okhttp3.Request
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sen Manga source
|
* Sen Manga source
|
||||||
@ -17,11 +17,11 @@ import java.util.*
|
|||||||
class SenManga : ParsedHttpSource() {
|
class SenManga : ParsedHttpSource() {
|
||||||
override val lang: String = "ja"
|
override val lang: String = "ja"
|
||||||
|
|
||||||
//Latest updates currently returns duplicate manga as it separates manga into chapters
|
override val supportsLatest = true
|
||||||
override val supportsLatest = false
|
|
||||||
override val name = "Sen Manga"
|
override val name = "Sen Manga"
|
||||||
override val baseUrl = "https://raw.senmanga.com"
|
override val baseUrl = "https://raw.senmanga.com"
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
override val client = super.client.newBuilder().addInterceptor {
|
override val client = super.client.newBuilder().addInterceptor {
|
||||||
//Intercept any image requests and add a referer to them
|
//Intercept any image requests and add a referer to them
|
||||||
//Enables bandwidth stealing feature
|
//Enables bandwidth stealing feature
|
||||||
@ -35,75 +35,50 @@ class SenManga : ParsedHttpSource() {
|
|||||||
it.proceed(request)
|
it.proceed(request)
|
||||||
}.build()!!
|
}.build()!!
|
||||||
|
|
||||||
//Sen Manga doesn't follow the specs and decides to use multiple elements with the same ID on the page...
|
override fun popularMangaSelector() = "li.series"
|
||||||
override fun popularMangaSelector() = ".update"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||||
val linkElement = element.select("a")
|
element.select("p.title a").let {
|
||||||
val titleElement = element.select("p.title").first()
|
setUrlWithoutDomain(it.attr("href"))
|
||||||
|
title = it.text()
|
||||||
setUrlWithoutDomain(linkElement.attr("href"))
|
}
|
||||||
title = titleElement.text()
|
thumbnail_url = element.select("img").attr("abs:src")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = "#Navigation > span > ul > li:nth-last-child(2):contains(next page)"
|
override fun popularMangaNextPageSelector() = "ul.pagination a[rel=next]"
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".search-results"
|
override fun searchMangaSelector() = popularMangaSelector()
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||||
val coverImage = element.getElementsByTag("img")
|
|
||||||
|
|
||||||
url = coverImage.parents().attr("href")
|
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||||
|
|
||||||
title = coverImage.attr("alt")
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/directory/popular?page=$page")
|
||||||
|
|
||||||
thumbnail_url = baseUrl + coverImage.attr("src")
|
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val url = HttpUrl.parse("$baseUrl/search")!!.newBuilder()
|
||||||
|
.addQueryParameter("s", query)
|
||||||
|
.addQueryParameter("page", page.toString())
|
||||||
|
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is GenreFilter -> {
|
||||||
|
val genreInclude = filter.state.filter { it.isIncluded() }.joinToString("%2C") { it.id }
|
||||||
|
val genreExclude = filter.state.filter { it.isExcluded() }.joinToString("%2C") { it.id }
|
||||||
|
url.addQueryParameter("genre", genreInclude)
|
||||||
|
url.addQueryParameter("nogenre", genreExclude)
|
||||||
|
}
|
||||||
|
is SortFilter -> url.addQueryParameter("sort", filter.toUriPart())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GET(url.toString(), headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Sen Manga search returns one page max!
|
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||||
override fun searchMangaNextPageSelector() = null
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/directory/popular/page/$page")
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector()
|
|
||||||
= throw UnsupportedOperationException("This method should not be called!")
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element)
|
|
||||||
= throw UnsupportedOperationException("This method should not be called!")
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response)
|
|
||||||
= if (response.request().url().pathSegments().firstOrNull()?.toLowerCase() != "search.php") {
|
|
||||||
//Use popular manga parser if we are not actually doing text search
|
|
||||||
popularMangaParse(response)
|
|
||||||
} else {
|
|
||||||
val document = response.asJsoup()
|
|
||||||
|
|
||||||
val mangas = document.select(searchMangaSelector()).map { element ->
|
|
||||||
searchMangaFromElement(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
MangasPage(mangas, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList)
|
|
||||||
= GET(if (query.isNullOrBlank()) {
|
|
||||||
val genreFilter = filters.find { it is GenreFilter } as GenreFilter
|
|
||||||
val sortFilter = filters.find { it is SortFilter } as SortFilter
|
|
||||||
//If genre sort is not active or sort settings are changed
|
|
||||||
if (!sortFilter.isDefault() || genreFilter.genrePath() == ALL_GENRES_PATH) {
|
|
||||||
val uri = Uri.parse("$baseUrl/directory/")
|
|
||||||
.buildUpon()
|
|
||||||
sortFilter.addToUri(uri)
|
|
||||||
uri.toString()
|
|
||||||
} else "$baseUrl/directory/category/${genreFilter.genrePath()}/"
|
|
||||||
} else {
|
|
||||||
Uri.parse("$baseUrl/Search/")
|
|
||||||
.buildUpon().appendPath(query)
|
|
||||||
.toString()
|
|
||||||
})
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector()
|
|
||||||
= throw UnsupportedOperationException("This method should not be called!")
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
title = document.select("div.panel h1.title").text()
|
title = document.select("div.panel h1.title").text()
|
||||||
@ -113,28 +88,25 @@ class SenManga : ParsedHttpSource() {
|
|||||||
val seriesElement = document.select("ul.series-info")
|
val seriesElement = document.select("ul.series-info")
|
||||||
|
|
||||||
description = seriesElement.select("span").text()
|
description = seriesElement.select("span").text()
|
||||||
author = seriesElement.select("li:eq(4) a").text()
|
author = seriesElement.select("li:eq(4)").text().substringAfter(": ")
|
||||||
artist = seriesElement.select("li:eq(5) a").text()
|
artist = seriesElement.select("li:eq(5)").text().substringAfter(": ")
|
||||||
status = seriesElement.select("li:eq(7)").first()?.text().orEmpty().let { parseStatus(it.substringAfter("Status:")) }
|
status = seriesElement.select("li:eq(7)").first()?.text().orEmpty().let { parseStatus(it.substringAfter("Status:")) }
|
||||||
|
genre = seriesElement.select("li:eq(2) a").joinToString { it.text() }
|
||||||
val genreElement = seriesElement.select("li:eq(2) a")
|
|
||||||
var genres = mutableListOf<String>()
|
|
||||||
genreElement?.forEach { genres.add(it.text()) }
|
|
||||||
genre = genres.joinToString(", ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseStatus(status: String) = when {
|
private fun parseStatus(status: String) = when {
|
||||||
status.contains("Ongoing") -> SManga.ONGOING
|
status.contains("Ongoing") -> SManga.ONGOING
|
||||||
status.contains("Complete") -> SManga.COMPLETED
|
status.contains("Complete") -> SManga.COMPLETED
|
||||||
else -> SManga.UNKNOWN
|
else -> SManga.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int)
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
= throw UnsupportedOperationException("This method should not be called!")
|
return GET("$baseUrl/directory/last_update?page=$page", headers)
|
||||||
|
}
|
||||||
|
|
||||||
//This may be unreliable as Sen Manga breaks the specs by having multiple elements with the same ID
|
override fun chapterListSelector() = "div.group div.element"
|
||||||
override fun chapterListSelector() = "div.element"
|
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||||
val linkElement = element.getElementsByTag("a")
|
val linkElement = element.getElementsByTag("a")
|
||||||
|
|
||||||
@ -179,121 +151,69 @@ class SenManga : ParsedHttpSource() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
//Base URI (document URI but without page index)
|
return listOf(1 .. document.select("select[name=page] option:last-of-type").first().text().toInt()).flatten().map { i ->
|
||||||
val baseUri = Uri.parse(baseUrl).buildUpon().apply {
|
Page(i - 1, "", "${document.location().replace(baseUrl, "$baseUrl/viewer")}/$i")
|
||||||
Uri.parse(document.baseUri()).pathSegments.let {
|
|
||||||
it.take(it.size - 1)
|
|
||||||
}.forEach {
|
|
||||||
appendPath(it)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
//Base Image URI (document URI but without page index and with "viewer" inserted as first path segment
|
|
||||||
val baseImageUri = Uri.parse(baseUrl).buildUpon().appendPath("viewer").apply {
|
|
||||||
baseUri.pathSegments.forEach {
|
|
||||||
appendPath(it)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val token = document.select("img[id=picture]").attr("src").substringAfter("?token=")
|
|
||||||
|
|
||||||
return document.select("select[name=page] > option").map {
|
|
||||||
val index = it.attr("value")
|
|
||||||
|
|
||||||
val uri = baseUri.buildUpon().appendPath(index).build()
|
|
||||||
|
|
||||||
val imageUriBuilder = baseImageUri.buildUpon().appendPath(index).appendQueryParameter("token", token)
|
|
||||||
|
|
||||||
Page(index.toInt() - 1, uri.toString(), imageUriBuilder.toString())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//We are able to get the image URL directly from the page list
|
|
||||||
override fun imageUrlParse(document: Document)
|
override fun imageUrlParse(document: Document)
|
||||||
= throw UnsupportedOperationException("This method should not be called!")
|
= throw UnsupportedOperationException("This method should not be called!")
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
override fun getFilterList() = FilterList(
|
||||||
Filter.Header("NOTE: Ignored if using text search!"),
|
GenreFilter(getGenreList()),
|
||||||
GenreFilter(),
|
|
||||||
Filter.Header("NOTE: Sort ignores genres search!"),
|
|
||||||
SortFilter()
|
SortFilter()
|
||||||
)
|
)
|
||||||
|
|
||||||
private class GenreFilter : Filter.Select<String>("Genre", GENRES.map { it.second }.toTypedArray()) {
|
private class Genre(name: String, val id: String = name) : Filter.TriState(name)
|
||||||
fun genrePath() = GENRES[state].first
|
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
|
||||||
|
|
||||||
|
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||||
|
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
||||||
|
fun toUriPart() = vals[state].first
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SortFilter : UriSelectFilter("Sort", "order", arrayOf(
|
private class SortFilter : UriPartFilter("Sort By", arrayOf(
|
||||||
Pair("popular", "Popularity"),
|
Pair("total_views", "Total Views"),
|
||||||
Pair("title", "Title"),
|
Pair("title", "Title"),
|
||||||
Pair("rating", "Rating")
|
Pair("rank", "Rank"),
|
||||||
), false) {
|
Pair("last_update", "Last Update")
|
||||||
fun isDefault() = state == 0
|
))
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private fun getGenreList(): List<Genre> = listOf(
|
||||||
* Class that creates a select filter. Each entry in the dropdown has a name and a display name.
|
Genre("Action", "Action"),
|
||||||
* If an entry is selected it is appended as a query parameter onto the end of the URI.
|
Genre("Adult", "Adult"),
|
||||||
* If `firstIsUnspecified` is set to true, if the first entry is selected, nothing will be appended on the the URI.
|
Genre("Adventure", "Adventure"),
|
||||||
*/
|
Genre("Comedy", "Comedy"),
|
||||||
//vals: <name, display>
|
Genre("Cooking", "Cooking"),
|
||||||
private open class UriSelectFilter(displayName: String, val uriParam: String, val vals: Array<Pair<String, String>>,
|
Genre("Drama", "Drama"),
|
||||||
val firstIsUnspecified: Boolean = true,
|
Genre("Ecchi", "Ecchi"),
|
||||||
defaultValue: Int = 0) :
|
Genre("Fantasy", "Fantasy"),
|
||||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray(), defaultValue), UriFilter {
|
Genre("Gender Bender", "Gender+Bender"),
|
||||||
override fun addToUri(uri: Uri.Builder) {
|
Genre("Harem", "Harem"),
|
||||||
if (state != 0 || !firstIsUnspecified)
|
Genre("Historical", "Historical"),
|
||||||
uri.appendQueryParameter(uriParam, vals[state].first)
|
Genre("Horror", "Horror"),
|
||||||
}
|
Genre("Josei", "Josei"),
|
||||||
}
|
Genre("Light Novel", "Light+Novel"),
|
||||||
|
Genre("Martial Arts", "Martial+Arts"),
|
||||||
/**
|
Genre("Mature", "Mature"),
|
||||||
* Represents a filter that is able to modify a URI.
|
Genre("Music", "Music"),
|
||||||
*/
|
Genre("Mystery", "Mystery"),
|
||||||
private interface UriFilter {
|
Genre("Psychological", "Psychological"),
|
||||||
fun addToUri(uri: Uri.Builder)
|
Genre("Romance", "Romance"),
|
||||||
}
|
Genre("School Life", "School+Life"),
|
||||||
|
Genre("Sci-Fi", "Sci+Fi"),
|
||||||
companion object {
|
Genre("Seinen", "Seinen"),
|
||||||
private val ALL_GENRES_PATH = "all"
|
Genre("Shoujo", "Shoujo"),
|
||||||
//<path, display name>
|
Genre("Shoujo Ai", "Shoujo+Ai"),
|
||||||
private val GENRES = listOf(
|
Genre("Shounen", "Shounen"),
|
||||||
Pair(ALL_GENRES_PATH, "All"),
|
Genre("Shounen Ai", "Shounen+Ai"),
|
||||||
Pair("Action", "Action"),
|
Genre("Slice of Life", "Slice+of+Life"),
|
||||||
Pair("Adult", "Adult"),
|
Genre("Smut", "Smut"),
|
||||||
Pair("Adventure", "Adventure"),
|
Genre("Sports", "Sports"),
|
||||||
Pair("Comedy", "Comedy"),
|
Genre("Supernatural", "Supernatural"),
|
||||||
Pair("Cooking", "Cooking"),
|
Genre("Tragedy", "Tragedy"),
|
||||||
Pair("Drama", "Drama"),
|
Genre("Webtoons", "Webtoons"),
|
||||||
Pair("Ecchi", "Ecchi"),
|
Genre("Yaoi", "Yaoi"),
|
||||||
Pair("Fantasy", "Fantasy"),
|
Genre("Yuri", "Yuri")
|
||||||
Pair("Gender-Bender", "Gender Bender"),
|
|
||||||
Pair("Harem", "Harem"),
|
|
||||||
Pair("Historical", "Historical"),
|
|
||||||
Pair("Horror", "Horror"),
|
|
||||||
Pair("Josei", "Josei"),
|
|
||||||
Pair("Light_Novel", "Light Novel"),
|
|
||||||
Pair("Martial_Arts", "Martial Arts"),
|
|
||||||
Pair("Mature", "Mature"),
|
|
||||||
Pair("Music", "Music"),
|
|
||||||
Pair("Mystery", "Mystery"),
|
|
||||||
Pair("Psychological", "Psychological"),
|
|
||||||
Pair("Romance", "Romance"),
|
|
||||||
Pair("School_Life", "School Life"),
|
|
||||||
Pair("Sci-Fi", "Sci-Fi"),
|
|
||||||
Pair("Seinen", "Seinen"),
|
|
||||||
Pair("Shoujo", "Shoujo"),
|
|
||||||
Pair("Shoujo-Ai", "Shoujo Ai"),
|
|
||||||
Pair("Shounen", "Shounen"),
|
|
||||||
Pair("Shounen-Ai", "Shounen Ai"),
|
|
||||||
Pair("Slice_of_Life", "Slice of Life"),
|
|
||||||
Pair("Smut", "Smut"),
|
|
||||||
Pair("Sports", "Sports"),
|
|
||||||
Pair("Supernatural", "Supernatural"),
|
|
||||||
Pair("Tragedy", "Tragedy"),
|
|
||||||
Pair("Webtoons", "Webtoons"),
|
|
||||||
Pair("Yaoi", "Yaoi"),
|
|
||||||
Pair("Yuri", "Yuri")
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user