add Mangataro (#11177)
* Mangataro * Refactor: Simplify search payload creation - Use a custom serializer for search filter parameters. - Remove redundant `.toJsonString()` calls for each filter. - Update filter classes to use appropriate data types (Int, Int?, String?) instead of just Strings, improving type safety. - Change `firstInstanceOrNull` to `firstInstance` for non-nullable filters. * Refactor: Move deeplink handler Move the deeplink handler function to a more logical position after the search parsing logic. * MangaTaro: Implement new search method - Add a new text search method that uses a different API endpoint. This provides more relevant results but ignores filters. - Add a filter option to toggle between the new search and the old filter-based search. - Exclude novels from appearing in search results and manga details.
This commit is contained in:
parent
fe130d5aa8
commit
3968208d9c
21
src/en/mangataro/AndroidManifest.xml
Normal file
21
src/en/mangataro/AndroidManifest.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".en.mangataro.UrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:host="mangataro.org" />
|
||||
<data android:scheme="https" />
|
||||
<data android:pathPattern="/manga/..*" />
|
||||
<data android:host="/read/..*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
8
src/en/mangataro/build.gradle
Normal file
8
src/en/mangataro/build.gradle
Normal file
@ -0,0 +1,8 @@
|
||||
ext {
|
||||
extName = 'MangaTaro'
|
||||
extClass = '.MangaTaro'
|
||||
extVersionCode = 1
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/en/mangataro/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/mangataro/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/en/mangataro/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/mangataro/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/en/mangataro/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/mangataro/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src/en/mangataro/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/mangataro/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
src/en/mangataro/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/mangataro/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@ -0,0 +1,106 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangataro
|
||||
|
||||
import keiyoushi.utils.toJsonString
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.JsonTransformingSerializer
|
||||
|
||||
@Serializable
|
||||
@Suppress("unused")
|
||||
class SearchPayload(
|
||||
private val page: Int,
|
||||
private val search: String,
|
||||
@Serializable(with = StringifiedListSerializer::class)
|
||||
private val years: List<Int>,
|
||||
@Serializable(with = StringifiedListSerializer::class)
|
||||
private val genres: List<Int>,
|
||||
@Serializable(with = StringifiedListSerializer::class)
|
||||
private val types: List<String>,
|
||||
@Serializable(with = StringifiedListSerializer::class)
|
||||
private val statuses: List<String>,
|
||||
private val sort: String,
|
||||
private val genreMatchMode: String,
|
||||
)
|
||||
|
||||
class StringifiedListSerializer<T>(elementSerializer: KSerializer<T>) :
|
||||
JsonTransformingSerializer<List<T>>(ListSerializer(elementSerializer)) {
|
||||
|
||||
override fun transformSerialize(element: JsonElement) =
|
||||
JsonPrimitive(element.toJsonString())
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@Suppress("unused")
|
||||
class SearchQueryPayload(
|
||||
val limit: Int,
|
||||
val query: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class SearchQueryResponse(
|
||||
val results: List<Manga>,
|
||||
) {
|
||||
@Serializable
|
||||
class Manga(
|
||||
val id: Int,
|
||||
val slug: String,
|
||||
val title: String,
|
||||
val thumbnail: String,
|
||||
val type: String,
|
||||
val description: String,
|
||||
val status: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class BrowseManga(
|
||||
val id: String,
|
||||
val url: String,
|
||||
val title: String,
|
||||
val cover: String,
|
||||
val type: String,
|
||||
val description: String,
|
||||
val status: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class MangaUrl(
|
||||
val id: String,
|
||||
val slug: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class MangaDetails(
|
||||
val id: Int,
|
||||
val slug: String,
|
||||
val title: Rendered,
|
||||
val content: Rendered,
|
||||
@SerialName("featured_media")
|
||||
val featuredMedia: Int,
|
||||
@SerialName("class_list")
|
||||
private val classList: List<String>,
|
||||
) {
|
||||
fun getFromClassList(type: String): List<String> {
|
||||
return classList.filter { it.startsWith("$type-") }
|
||||
.map {
|
||||
it.substringAfter("$type-")
|
||||
.split("-")
|
||||
.joinToString(" ") { word -> word.replaceFirstChar { it.titlecase() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Thumbnail(
|
||||
@SerialName("source_url")
|
||||
val url: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Rendered(
|
||||
val rendered: String,
|
||||
)
|
||||
@ -0,0 +1,320 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangataro
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import java.util.Calendar
|
||||
|
||||
abstract class SelectFilter<T>(
|
||||
name: String,
|
||||
private val options: List<Pair<String, T>>,
|
||||
) : Filter.Select<String>(
|
||||
name,
|
||||
options.map { it.first }.toTypedArray(),
|
||||
) {
|
||||
val selected get() = options[state].second
|
||||
}
|
||||
|
||||
class CheckBoxFilter<T>(name: String, val value: T) : Filter.CheckBox(name)
|
||||
|
||||
abstract class CheckBoxGroup<T>(
|
||||
name: String,
|
||||
options: List<Pair<String, T>>,
|
||||
) : Filter.Group<CheckBoxFilter<T>>(
|
||||
name,
|
||||
options.map { CheckBoxFilter(it.first, it.second) },
|
||||
) {
|
||||
val checked get() = state.filter { it.state }.map { it.value }
|
||||
}
|
||||
|
||||
class SearchWithFilters : Filter.CheckBox("Apply filters to Text Search", false)
|
||||
|
||||
class TypeFilter : SelectFilter<String?>(
|
||||
name = "Type",
|
||||
options = listOf(
|
||||
"All" to null,
|
||||
"Manga" to "Manga",
|
||||
"Manhwa" to "Manhwa",
|
||||
"Manhua" to "Manhua",
|
||||
),
|
||||
)
|
||||
|
||||
class StatusFilter : SelectFilter<String?>(
|
||||
name = "Status",
|
||||
options = listOf(
|
||||
"All" to null,
|
||||
"Completed" to "Completed",
|
||||
"Ongoing" to "Ongoing",
|
||||
),
|
||||
)
|
||||
|
||||
class YearFilter : SelectFilter<Int?>(
|
||||
name = "Year",
|
||||
options = buildList {
|
||||
add("All" to null)
|
||||
val current = Calendar.getInstance().get(Calendar.YEAR)
|
||||
(current downTo 1949).mapTo(this) { it.toString() to it }
|
||||
},
|
||||
)
|
||||
|
||||
class TagFilter : CheckBoxGroup<Int>(
|
||||
name = "Tags",
|
||||
options = tags,
|
||||
)
|
||||
|
||||
class TagFilterMatch : SelectFilter<String>(
|
||||
name = "Tag Match",
|
||||
options = listOf(
|
||||
"Any" to "any",
|
||||
"All" to "all",
|
||||
),
|
||||
)
|
||||
|
||||
class SortFilter(
|
||||
state: Selection = Selection(0, false),
|
||||
) : Filter.Sort(
|
||||
name = "Sort",
|
||||
values = sort.map { it.first }.toTypedArray(),
|
||||
state = state,
|
||||
) {
|
||||
private val sortDirection get() = if (state?.ascending == true) {
|
||||
"asc"
|
||||
} else {
|
||||
"desc"
|
||||
}
|
||||
val selected get() = "${sort[state?.index ?: 0].second}_$sortDirection"
|
||||
|
||||
companion object {
|
||||
val popular = FilterList(
|
||||
SortFilter(Selection(3, false)),
|
||||
TagFilterMatch(),
|
||||
)
|
||||
val latest = FilterList(
|
||||
SortFilter(Selection(0, false)),
|
||||
TagFilterMatch(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val tags = listOf(
|
||||
"4-Koma" to 2094,
|
||||
"Abandoned Children" to 1050,
|
||||
"Ability Steal" to 2386,
|
||||
"Absent Parents" to 2397,
|
||||
"Academy" to 4012,
|
||||
"Accelerated Growth" to 999,
|
||||
"Action" to 7,
|
||||
"Adaptation" to 1351,
|
||||
"Adapted to Anime" to 1000,
|
||||
"Adapted to Drama CD" to 5072,
|
||||
"Adapted to Game" to 1001,
|
||||
"Adapted to Manga" to 5069,
|
||||
"Adapted to Manhua" to 963,
|
||||
"Adapted to Manhwa" to 1002,
|
||||
"Adapted to Visual Novel" to 5070,
|
||||
"Adopted Children" to 1058,
|
||||
"Adult" to 88,
|
||||
"Adult Cast" to 16,
|
||||
"Adventure" to 12,
|
||||
"Age Regression" to 2861,
|
||||
"Alchemy" to 964,
|
||||
"Aliens" to 965,
|
||||
"Alternate World" to 966,
|
||||
"Ancient Times" to 2862,
|
||||
"Animals" to 1135,
|
||||
"Anthology" to 2096,
|
||||
"Anthropomorphic" to 93,
|
||||
"Appearance Different from Actual Age" to 5071,
|
||||
"Aristocracy" to 1037,
|
||||
"Army" to 2387,
|
||||
"Army Building" to 1005,
|
||||
"Arranged Marriage" to 1047,
|
||||
"Arrogant Characters" to 2401,
|
||||
"Artbook" to 4551,
|
||||
"Artifacts" to 4004,
|
||||
"Assassins" to 2863,
|
||||
"Avant Garde" to 98,
|
||||
"Award Winning" to 17,
|
||||
"Battle Academy" to 5279,
|
||||
"Battle Competition" to 2388,
|
||||
"BD" to 1137,
|
||||
"Beast Companions" to 968,
|
||||
"Beautiful Female Lead" to 969,
|
||||
"Betrayal" to 2864,
|
||||
"Blacksmith" to 971,
|
||||
"Boys Love" to 80,
|
||||
"Boys' Love" to 4237,
|
||||
"Brotherhood" to 1038,
|
||||
"Business Management" to 2398,
|
||||
"Calm Protagonist" to 1006,
|
||||
"Cautious Protagonist" to 2392,
|
||||
"CGDCT" to 94,
|
||||
"Character growth" to 4929,
|
||||
"Childcare" to 51,
|
||||
"Children's" to 4818,
|
||||
"Cold Love Interests" to 2393,
|
||||
"Cold Protagonist" to 2394,
|
||||
"Combat Sports" to 92,
|
||||
"Comedy" to 20,
|
||||
"Cooking" to 2118,
|
||||
"Crime" to 1753,
|
||||
"Crossdressing" to 81,
|
||||
"Cruel Characters" to 2395,
|
||||
"Cunning Protagonist" to 1039,
|
||||
"Cute Stuffs" to 4817,
|
||||
"Dark" to 4930,
|
||||
"Death of Loved Ones" to 1044,
|
||||
"Delinquents" to 52,
|
||||
"Demons" to 1376,
|
||||
"Dense Protagonist" to 975,
|
||||
"Detective" to 58,
|
||||
"Devoted love interests" to 4008,
|
||||
"Doujinshi" to 79,
|
||||
"Dragons" to 1040,
|
||||
"Drama" to 8,
|
||||
"Drugs" to 1048,
|
||||
"Dungeon" to 4795,
|
||||
"Dungeons" to 1011,
|
||||
"Ecchi" to 40,
|
||||
"Educational" to 66,
|
||||
"Elemental Magic" to 976,
|
||||
"Elves" to 4010,
|
||||
"Erotica" to 71,
|
||||
"Evolution" to 4931,
|
||||
"Family" to 4005,
|
||||
"Fantasy" to 2,
|
||||
"French" to 108,
|
||||
"Full Color" to 34,
|
||||
"Gag Humor" to 59,
|
||||
"Game" to 36,
|
||||
"Game Elements" to 979,
|
||||
"Gender Bender" to 106,
|
||||
"Genderswap" to 2093,
|
||||
"Ghosts" to 1134,
|
||||
"Girls Love" to 49,
|
||||
"Gore" to 23,
|
||||
"Gourmet" to 46,
|
||||
"Gyaru" to 4739,
|
||||
"Harem" to 64,
|
||||
"Hentai" to 82,
|
||||
"High Stakes Game" to 76,
|
||||
"Historical" to 18,
|
||||
"Horror" to 44,
|
||||
"Human nonhuman relationship" to 4932,
|
||||
"Idols (Female)" to 95,
|
||||
"Idols (Male)" to 101,
|
||||
"Indonesian" to 109,
|
||||
"Isekai" to 3,
|
||||
"Iyashikei" to 91,
|
||||
"Josei" to 43,
|
||||
"Kids" to 107,
|
||||
"Kingdoms" to 4006,
|
||||
"Level System" to 1020,
|
||||
"Light Novel" to 55,
|
||||
"Loli" to 4207,
|
||||
"Lolicon" to 89,
|
||||
"Long Strip" to 1172,
|
||||
"Love interest falls in love first" to 4007,
|
||||
"Love Polygon" to 41,
|
||||
"Love Status Quo" to 100,
|
||||
"Mafia" to 2061,
|
||||
"Magic" to 35,
|
||||
"Magical Girls" to 2095,
|
||||
"Magical Sex Shift" to 68,
|
||||
"Mahou Shoujo" to 97,
|
||||
"Male" to 4933,
|
||||
"Manga" to 14,
|
||||
"Mangataro Exclusive" to 4792,
|
||||
"Manhua" to 33,
|
||||
"Manhwa" to 6,
|
||||
"Martial Arts" to 4,
|
||||
"Master-disciple relationship" to 4013,
|
||||
"Mature" to 87,
|
||||
"Mecha" to 74,
|
||||
"Medical" to 67,
|
||||
"Memoir" to 96,
|
||||
"Military" to 30,
|
||||
"Misunderstandings" to 1023,
|
||||
"Monster Girls" to 1414,
|
||||
"Monsters" to 37,
|
||||
"Multiple POV" to 1024,
|
||||
"Murim" to 38,
|
||||
"Music" to 83,
|
||||
"Mystery" to 24,
|
||||
"Mythology" to 53,
|
||||
"Ninja" to 4601,
|
||||
"OEL" to 103,
|
||||
"Office Workers" to 2062,
|
||||
"Official Colored" to 4834,
|
||||
"One-shot" to 48,
|
||||
"Oneshot" to 4210,
|
||||
"Organized Crime" to 50,
|
||||
"Otaku Culture" to 70,
|
||||
"Parody" to 21,
|
||||
"Performing Arts" to 72,
|
||||
"Pets" to 105,
|
||||
"Philosophical" to 63,
|
||||
"Police" to 1136,
|
||||
"Post-Apocalyptic" to 1557,
|
||||
"Psychological" to 26,
|
||||
"Racing" to 102,
|
||||
"Reincarnated in another world" to 4014,
|
||||
"Reincarnation" to 5,
|
||||
"Reverse Harem" to 61,
|
||||
"Romance" to 29,
|
||||
"Romantic Subtext" to 28,
|
||||
"Samurai" to 60,
|
||||
"School" to 10,
|
||||
"School Life" to 73,
|
||||
"Sci-Fi" to 15,
|
||||
"Seinen" to 19,
|
||||
"Self-Published" to 1413,
|
||||
"Sexual Violence" to 4116,
|
||||
"Shoujo" to 42,
|
||||
"Shoujo Ai" to 90,
|
||||
"Shounen" to 13,
|
||||
"Shounen Ai" to 104,
|
||||
"Showbiz" to 54,
|
||||
"Skill books" to 4009,
|
||||
"Slice of Life" to 47,
|
||||
"Smut" to 85,
|
||||
"Space" to 84,
|
||||
"Sports" to 25,
|
||||
"Strategy Game" to 77,
|
||||
"Super Power" to 22,
|
||||
"Superhero" to 4098,
|
||||
"Supernatural" to 9,
|
||||
"Survival" to 56,
|
||||
"Suspense" to 75,
|
||||
"Sword And Magic" to 995,
|
||||
"Team Sports" to 27,
|
||||
"Thai" to 591,
|
||||
"Thriller" to 1341,
|
||||
"Time Travel" to 32,
|
||||
"Traditional Games" to 4742,
|
||||
"Tragedy" to 65,
|
||||
"Transported to another world" to 4011,
|
||||
"Urban Fantasy" to 110,
|
||||
"Vampire" to 45,
|
||||
"Vampires" to 2102,
|
||||
"Video Game" to 31,
|
||||
"Video Games" to 1366,
|
||||
"Villainess" to 62,
|
||||
"Virtual Reality" to 4485,
|
||||
"Visual Arts" to 57,
|
||||
"Web Comic" to 1173,
|
||||
"Webtoon" to 39,
|
||||
"Workplace" to 69,
|
||||
"Wuxia" to 1350,
|
||||
"Xianxia" to 2391,
|
||||
"Xuanhuan" to 962,
|
||||
"Yaoi" to 86,
|
||||
"Yuri" to 99,
|
||||
"Zombies" to 1342,
|
||||
)
|
||||
|
||||
private val sort = listOf(
|
||||
"Latest Updates" to "post",
|
||||
"Release Date" to "release",
|
||||
"Title A-Z" to "title",
|
||||
"Popular" to "popular",
|
||||
)
|
||||
@ -0,0 +1,318 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangataro
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
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.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import keiyoushi.utils.firstInstance
|
||||
import keiyoushi.utils.firstInstanceOrNull
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.toJsonString
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import rx.Observable
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.util.Calendar
|
||||
|
||||
class MangaTaro : HttpSource() {
|
||||
|
||||
override val name = "MangaTaro"
|
||||
|
||||
override val baseUrl = "https://mangataro.org"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
searchMangaRequest(page, "", SortFilter.popular)
|
||||
|
||||
override fun popularMangaParse(response: Response) =
|
||||
searchMangaParse(response)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
searchMangaRequest(page, "", SortFilter.latest)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
searchMangaParse(response)
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (query.startsWith("https://")) {
|
||||
deeplinkHandler(query)
|
||||
} else if (
|
||||
query.isNotBlank() &&
|
||||
filters.firstInstanceOrNull<SearchWithFilters>()?.state == false
|
||||
) {
|
||||
querySearch(query)
|
||||
} else {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun querySearch(query: String): Observable<MangasPage> {
|
||||
val body = SearchQueryPayload(
|
||||
query = query.trim(),
|
||||
limit = 25,
|
||||
).toJsonString().toRequestBody("application/json".toMediaType())
|
||||
|
||||
return client.newCall(POST("$baseUrl/auth/search", headers, body))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val data = response.parseAs<SearchQueryResponse>().results
|
||||
|
||||
val mangas = data.filter { it.type != "Novel" }
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
url = MangaUrl(it.id.toString(), it.slug).toJsonString()
|
||||
title = it.title
|
||||
thumbnail_url = it.thumbnail
|
||||
description = it.description
|
||||
status = when (it.status) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MangasPage(
|
||||
mangas = mangas,
|
||||
hasNextPage = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val body = SearchPayload(
|
||||
page = page,
|
||||
search = query.trim(),
|
||||
years = filters.firstInstanceOrNull<YearFilter>()
|
||||
?.selected.let(::listOfNotNull),
|
||||
genres = filters.firstInstanceOrNull<TagFilter>()
|
||||
?.checked.orEmpty(),
|
||||
types = filters.firstInstanceOrNull<TypeFilter>()
|
||||
?.selected.let(::listOfNotNull),
|
||||
statuses = filters.firstInstanceOrNull<StatusFilter>()
|
||||
?.selected.let(::listOfNotNull),
|
||||
sort = filters.firstInstance<SortFilter>().selected,
|
||||
genreMatchMode = filters.firstInstance<TagFilterMatch>().selected,
|
||||
).toJsonString().toRequestBody("application/json".toMediaType())
|
||||
|
||||
return POST("$baseUrl/wp-json/manga/v1/load", headers, body)
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
SearchWithFilters(),
|
||||
Filter.Header("If unchecked, all filters will be ignored with search query"),
|
||||
Filter.Header("But will give more relevant results"),
|
||||
Filter.Separator(),
|
||||
SortFilter(),
|
||||
TypeFilter(),
|
||||
StatusFilter(),
|
||||
YearFilter(),
|
||||
TagFilter(),
|
||||
TagFilterMatch(),
|
||||
)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<List<BrowseManga>>()
|
||||
|
||||
val mangas = data.filter { it.type != "Novel" }
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
url = MangaUrl(id = it.id, slug = it.url.toSlug()).toJsonString()
|
||||
title = it.title
|
||||
thumbnail_url = it.cover
|
||||
description = it.description
|
||||
status = when (it.status) {
|
||||
"Ongoing" -> SManga.ONGOING
|
||||
"Completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(
|
||||
mangas = mangas,
|
||||
hasNextPage = data.size == 24,
|
||||
)
|
||||
}
|
||||
|
||||
private fun deeplinkHandler(url: String): Observable<MangasPage> {
|
||||
val slug = url.toSlug()
|
||||
|
||||
return client.newCall(GET("$baseUrl/manga/$slug", headers))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
val document = it.asJsoup()
|
||||
|
||||
val id = document.body().dataset()["manga-id"]!!
|
||||
val status = when (document.selectFirst(".manga-page-wrapper span.capitalize")?.text()?.lowercase()) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
if (document.selectFirst(".manga-page-wrapper span:contains(Novel)") != null) {
|
||||
throw Exception("Novels are not supported")
|
||||
}
|
||||
|
||||
id to status
|
||||
}
|
||||
.switchMap {
|
||||
client.newCall(mangaDetailsRequest(it.first, it.second))
|
||||
.asObservableSuccess()
|
||||
.map(::mangaDetailsParse)
|
||||
}
|
||||
.map { MangasPage(listOf(it), false) }
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val id = manga.url.parseAs<MangaUrl>().id
|
||||
|
||||
return mangaDetailsRequest(id, manga.status)
|
||||
}
|
||||
|
||||
private fun mangaDetailsRequest(id: String, status: Int): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegments("wp-json/wp/v2/manga")
|
||||
addPathSegment(id)
|
||||
fragment(status.toString())
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val data = response.parseAs<MangaDetails>()
|
||||
val thumbnail = getThumbnail(data.featuredMedia)
|
||||
|
||||
return SManga.create().apply {
|
||||
url = MangaUrl(data.id.toString(), data.slug).toJsonString()
|
||||
title = data.title.rendered
|
||||
description = Jsoup.parseBodyFragment(data.content.rendered).wholeText()
|
||||
genre = buildSet {
|
||||
addAll(data.getFromClassList("tag"))
|
||||
addAll(data.getFromClassList("type"))
|
||||
}.joinToString()
|
||||
author = data.getFromClassList("manga_author").joinToString()
|
||||
status = response.request.url.fragment!!.toInt()
|
||||
thumbnail_url = thumbnail
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun getThumbnail(mediaId: Int): String {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegments("wp-json/wp/v2/media")
|
||||
addPathSegment(mediaId.toString())
|
||||
}.build()
|
||||
|
||||
return client.newCall(GET(url, headers)).execute()
|
||||
.parseAs<Thumbnail>()
|
||||
.url
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String {
|
||||
val slug = manga.url.parseAs<MangaUrl>().slug
|
||||
|
||||
return "$baseUrl/manga/$slug"
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return GET(getMangaUrl(manga), headers)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val placeholders = listOf("", "N/A", "—")
|
||||
var hasScanlator = false
|
||||
|
||||
val chapters = document.select(".chapter-list a").map {
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(it.absUrl("href"))
|
||||
val details = it.select("> div + div > div")
|
||||
name = buildString {
|
||||
append(details[0].selectFirst("span")!!.ownText())
|
||||
details[1].text().also { title ->
|
||||
if (title !in placeholders) {
|
||||
append(": ", title)
|
||||
}
|
||||
}
|
||||
}
|
||||
details[2].text().let { group ->
|
||||
if (group !in placeholders) {
|
||||
scanlator = group
|
||||
hasScanlator = true
|
||||
}
|
||||
}
|
||||
date_upload = details[3].text().parseRelativeDate()
|
||||
}
|
||||
}
|
||||
|
||||
if (hasScanlator) {
|
||||
chapters.onEach { it.scanlator = it.scanlator ?: "\u200B" } // Insert zero-width space
|
||||
}
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return document.select("img.comic-image").mapIndexed { idx, img ->
|
||||
val imageUrl = when {
|
||||
img.hasAttr("data-src") -> img.absUrl("data-src")
|
||||
else -> img.absUrl("src")
|
||||
}
|
||||
Page(idx, imageUrl = imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toSlug() = toHttpUrl().let { url ->
|
||||
val path = url.pathSegments.filter(String::isNotBlank)
|
||||
|
||||
if ((path.size == 2 && path[0] == "manga") || (path.size == 3 && path[0] == "read")) {
|
||||
path[1]
|
||||
} else {
|
||||
throw Exception("Expected manga or read path, got $this")
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.parseRelativeDate(): Long {
|
||||
val calendar = Calendar.getInstance()
|
||||
val (amount, unit) = relativeDateRegex.matchEntire(this)?.destructured
|
||||
?: return 0L
|
||||
|
||||
when (unit) {
|
||||
"h" -> calendar.add(Calendar.HOUR, -amount.toInt())
|
||||
"w" -> calendar.add(Calendar.WEEK_OF_YEAR, -amount.toInt())
|
||||
"mo" -> calendar.add(Calendar.MONTH, -amount.toInt())
|
||||
"y" -> calendar.add(Calendar.YEAR, -amount.toInt())
|
||||
}
|
||||
|
||||
return calendar.timeInMillis
|
||||
}
|
||||
|
||||
private val relativeDateRegex = Regex("""(\d+)(h|w|mo|y) ago""")
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package eu.kanade.tachiyomi.extension.en.mangataro
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class UrlActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", intent.data.toString())
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("MangaTaro", "Unable to launch activity", e)
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user