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