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