add MangaBall (#11344)
* MangaBall * remove * suggested changes and more * remove this * MangaBall: Fix Korean language code * change to Locale.ROOT as the pattern isn't language specific * only throw if filtered
This commit is contained in:
parent
9627718a40
commit
68b70d54d9
21
src/all/mangaball/AndroidManifest.xml
Normal file
21
src/all/mangaball/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=".all.mangaball.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="mangaball.net" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:pathPattern="/title-detail/..*" />
|
||||||
|
<data android:pathPattern="/chapter-detail/..*" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
8
src/all/mangaball/build.gradle
Normal file
8
src/all/mangaball/build.gradle
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'Manga Ball'
|
||||||
|
extClass = '.MangaBallFactory'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
BIN
src/all/mangaball/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/all/mangaball/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/all/mangaball/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/all/mangaball/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/all/mangaball/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/all/mangaball/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/all/mangaball/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/all/mangaball/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
src/all/mangaball/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/all/mangaball/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@ -0,0 +1,71 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.mangaball
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SearchResponse(
|
||||||
|
val data: List<SearchManga>,
|
||||||
|
private val pagination: Pagination,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class Pagination(
|
||||||
|
@SerialName("current_page")
|
||||||
|
val currentPage: Int,
|
||||||
|
@SerialName("last_page")
|
||||||
|
val lastPage: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun hasNextPage() = pagination.currentPage < pagination.lastPage
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class SearchManga(
|
||||||
|
val url: String,
|
||||||
|
val name: String,
|
||||||
|
val cover: String,
|
||||||
|
val isAdult: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterListResponse(
|
||||||
|
@SerialName("ALL_CHAPTERS")
|
||||||
|
val chapters: List<ChapterContainer>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterContainer(
|
||||||
|
@SerialName("number_float")
|
||||||
|
val number: Float,
|
||||||
|
val translations: List<Chapter>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Chapter(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val language: String,
|
||||||
|
val group: Group,
|
||||||
|
val date: String,
|
||||||
|
val volume: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Group(
|
||||||
|
@SerialName("_id")
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class Yoast(
|
||||||
|
@SerialName("@graph")
|
||||||
|
val graph: List<Graph>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class Graph(
|
||||||
|
@SerialName("@type")
|
||||||
|
val type: String,
|
||||||
|
val url: String? = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,201 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.mangaball
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
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 TriStateFilter<T>(name: String, val value: T) : Filter.TriState(name)
|
||||||
|
|
||||||
|
abstract class TriStateGroupFilter<T>(
|
||||||
|
name: String,
|
||||||
|
options: List<Pair<String, T>>,
|
||||||
|
) : Filter.Group<TriStateFilter<T>>(
|
||||||
|
name,
|
||||||
|
options.map { TriStateFilter(it.first, it.second) },
|
||||||
|
) {
|
||||||
|
val included get() = state.filter { it.isIncluded() }.map { it.value }
|
||||||
|
val excluded get() = state.filter { it.isExcluded() }.map { it.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortFilter : SelectFilter<String>(
|
||||||
|
"Sort By",
|
||||||
|
options = listOf(
|
||||||
|
"Lastest Updated Chapters" to "updated_chapters_desc",
|
||||||
|
"Oldest Updated Chapters" to "updated_chapters_asc",
|
||||||
|
"Lastest Created" to "created_at_desc",
|
||||||
|
"Oldest Created" to "created_at_asc",
|
||||||
|
"Title A-Z" to "name_asc",
|
||||||
|
"Title Z-A" to "name_desc",
|
||||||
|
"Views High to Low" to "views_desc",
|
||||||
|
"Views Low to High" to "views_asc",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class ContentFilter : TriStateGroupFilter<String>(
|
||||||
|
"Content",
|
||||||
|
options = listOf(
|
||||||
|
"Gore" to "685148d115e8b86aae68e4f3",
|
||||||
|
"Sexual Violence" to "685146c5f3ed681c80f257e7",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class FormatFilter : TriStateGroupFilter<String>(
|
||||||
|
"Format",
|
||||||
|
options = listOf(
|
||||||
|
"4-Koma" to "685148d115e8b86aae68e4ec",
|
||||||
|
"Adaptation" to "685148cf15e8b86aae68e4de",
|
||||||
|
"Anthology" to "685148e915e8b86aae68e558",
|
||||||
|
"Award Winning" to "685148fe15e8b86aae68e5a7",
|
||||||
|
"Doujinshi" to "6851490e15e8b86aae68e5da",
|
||||||
|
"Fan Colored" to "6851498215e8b86aae68e704",
|
||||||
|
"Full Color" to "685148d615e8b86aae68e502",
|
||||||
|
"Long Strip" to "685148d915e8b86aae68e517",
|
||||||
|
"Official Colored" to "6851493515e8b86aae68e64a",
|
||||||
|
"Oneshot" to "685148eb15e8b86aae68e56c",
|
||||||
|
"Self-Published" to "6851492e15e8b86aae68e633",
|
||||||
|
"Web Comic" to "685148d715e8b86aae68e50d",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class GenreFilter : TriStateGroupFilter<String>(
|
||||||
|
"Genre",
|
||||||
|
options = listOf(
|
||||||
|
"Action" to "685146c5f3ed681c80f257e3",
|
||||||
|
"Adult" to "689371f0a943baf927094f03",
|
||||||
|
"Adventure" to "685146c5f3ed681c80f257e6",
|
||||||
|
"Boys' Love" to "685148ef15e8b86aae68e573",
|
||||||
|
"Comedy" to "685146c5f3ed681c80f257e5",
|
||||||
|
"Crime" to "685148da15e8b86aae68e51f",
|
||||||
|
"Drama" to "685148cf15e8b86aae68e4dd",
|
||||||
|
"Ecchi" to "6892a73ba943baf927094e37",
|
||||||
|
"Fantasy" to "685146c5f3ed681c80f257ea",
|
||||||
|
"Girls' Love" to "685148da15e8b86aae68e524",
|
||||||
|
"Historical" to "685148db15e8b86aae68e527",
|
||||||
|
"Horror" to "685148da15e8b86aae68e520",
|
||||||
|
"Isekai" to "685146c5f3ed681c80f257e9",
|
||||||
|
"Magical Girls" to "6851490d15e8b86aae68e5d4",
|
||||||
|
"Mature" to "68932d11a943baf927094e7b",
|
||||||
|
"Mecha" to "6851490c15e8b86aae68e5d2",
|
||||||
|
"Medical" to "6851494e15e8b86aae68e66e",
|
||||||
|
"Mystery" to "685148d215e8b86aae68e4f4",
|
||||||
|
"Philosophical" to "685148e215e8b86aae68e544",
|
||||||
|
"Psychological" to "685148d715e8b86aae68e507",
|
||||||
|
"Romance" to "685148cf15e8b86aae68e4db",
|
||||||
|
"Sci-Fi" to "685148cf15e8b86aae68e4da",
|
||||||
|
"Shounen Ai" to "689f0ab1f2e66744c6091524",
|
||||||
|
"Slice of Life" to "685148d015e8b86aae68e4e3",
|
||||||
|
"Smut" to "689371f2a943baf927094f04",
|
||||||
|
"Sports" to "685148f515e8b86aae68e588",
|
||||||
|
"Superhero" to "6851492915e8b86aae68e61c",
|
||||||
|
"Thriller" to "685148d915e8b86aae68e51e",
|
||||||
|
"Tragedy" to "685148db15e8b86aae68e529",
|
||||||
|
"User Created" to "68932c3ea943baf927094e77",
|
||||||
|
"Wuxia" to "6851490715e8b86aae68e5c3",
|
||||||
|
"Yaoi" to "68932f68a943baf927094eaa",
|
||||||
|
"Yuri" to "6896a885a943baf927094f66",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class OriginFilter : TriStateGroupFilter<String>(
|
||||||
|
"Origin",
|
||||||
|
options = listOf(
|
||||||
|
"Comic" to "68ecab8507ec62d87e62780f",
|
||||||
|
"Manga" to "68ecab1e07ec62d87e627806",
|
||||||
|
"Manhua" to "68ecab4807ec62d87e62780b",
|
||||||
|
"Manhwa" to "68ecab3b07ec62d87e627809",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class ThemeFilter : TriStateGroupFilter<String>(
|
||||||
|
"Theme",
|
||||||
|
options = listOf(
|
||||||
|
"Aliens" to "6851490d15e8b86aae68e5d5",
|
||||||
|
"Animals" to "685148e715e8b86aae68e54b",
|
||||||
|
"Comics" to "68bf09ff8fdeab0b6a9bc2b7",
|
||||||
|
"Cooking" to "685148d215e8b86aae68e4f8",
|
||||||
|
"Crossdressing" to "685148df15e8b86aae68e534",
|
||||||
|
"Delinquents" to "685148d915e8b86aae68e519",
|
||||||
|
"Demons" to "685146c5f3ed681c80f257e4",
|
||||||
|
"Genderswap" to "685148d715e8b86aae68e505",
|
||||||
|
"Ghosts" to "685148d615e8b86aae68e501",
|
||||||
|
"Gyaru" to "685148d015e8b86aae68e4e8",
|
||||||
|
"Harem" to "685146c5f3ed681c80f257e8",
|
||||||
|
"Hentai" to "68bfceaf4dbc442a26519889",
|
||||||
|
"Incest" to "685148f215e8b86aae68e584",
|
||||||
|
"Loli" to "685148d715e8b86aae68e506",
|
||||||
|
"Mafia" to "685148d915e8b86aae68e518",
|
||||||
|
"Magic" to "685148d715e8b86aae68e509",
|
||||||
|
"Manhwa 18+" to "68f5f5ce5f29d3c1863dec3a",
|
||||||
|
"Martial Arts" to "6851490615e8b86aae68e5c2",
|
||||||
|
"Military" to "685148e215e8b86aae68e541",
|
||||||
|
"Monster Girls" to "685148db15e8b86aae68e52c",
|
||||||
|
"Monsters" to "685146c5f3ed681c80f257e2",
|
||||||
|
"Music" to "685148d015e8b86aae68e4e4",
|
||||||
|
"Ninja" to "685148d715e8b86aae68e508",
|
||||||
|
"Office Workers" to "685148d315e8b86aae68e4fd",
|
||||||
|
"Police" to "6851498815e8b86aae68e714",
|
||||||
|
"Post-Apocalyptic" to "685148e215e8b86aae68e540",
|
||||||
|
"Reincarnation" to "685146c5f3ed681c80f257e1",
|
||||||
|
"Reverse Harem" to "685148df15e8b86aae68e533",
|
||||||
|
"Samurai" to "6851490415e8b86aae68e5b9",
|
||||||
|
"School Life" to "685148d015e8b86aae68e4e7",
|
||||||
|
"Shota" to "685148d115e8b86aae68e4ed",
|
||||||
|
"Supernatural" to "685148db15e8b86aae68e528",
|
||||||
|
"Survival" to "685148cf15e8b86aae68e4dc",
|
||||||
|
"Time Travel" to "6851490c15e8b86aae68e5d1",
|
||||||
|
"Traditional Games" to "6851493515e8b86aae68e645",
|
||||||
|
"Vampires" to "685148f915e8b86aae68e597",
|
||||||
|
"Video Games" to "685148e115e8b86aae68e53c",
|
||||||
|
"Villainess" to "6851492115e8b86aae68e602",
|
||||||
|
"Virtual Reality" to "68514a1115e8b86aae68e83e",
|
||||||
|
"Zombies" to "6851490c15e8b86aae68e5d3",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class TagIncludeMode : SelectFilter<String>(
|
||||||
|
"Tag Include Mode",
|
||||||
|
options = listOf(
|
||||||
|
"AND" to "and",
|
||||||
|
"OR" to "or",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class TagExcludeMode : SelectFilter<String>(
|
||||||
|
"Tag Exclude Mode",
|
||||||
|
options = listOf(
|
||||||
|
"AND" to "and",
|
||||||
|
"OR" to "or",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class DemographicFilter : SelectFilter<String>(
|
||||||
|
"Magazine Demographic",
|
||||||
|
options = listOf(
|
||||||
|
"Any" to "any",
|
||||||
|
"Shounen" to "shounen",
|
||||||
|
"Shoujo" to "shoujo",
|
||||||
|
"Seinen" to "seinen",
|
||||||
|
"Josei" to "josei",
|
||||||
|
"Yuri" to "yuri",
|
||||||
|
"Yaoi" to "yaoi",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class StatusFilter : SelectFilter<String>(
|
||||||
|
"Publication Status",
|
||||||
|
options = listOf(
|
||||||
|
"Any" to "any",
|
||||||
|
"Ongoing" to "ongoing",
|
||||||
|
"Completed" to "completed",
|
||||||
|
"Hiatus" to "hiatus",
|
||||||
|
"Cancelled" to "cancelled",
|
||||||
|
),
|
||||||
|
)
|
||||||
@ -0,0 +1,403 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.mangaball
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
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.getPreferencesLazy
|
||||||
|
import keiyoushi.utils.parseAs
|
||||||
|
import keiyoushi.utils.tryParse
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.internal.closeQuietly
|
||||||
|
import okio.IOException
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import rx.Observable
|
||||||
|
import java.lang.UnsupportedOperationException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class MangaBall(
|
||||||
|
override val lang: String,
|
||||||
|
private vararg val siteLang: String,
|
||||||
|
) : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
|
override val name = "Manga Ball"
|
||||||
|
override val baseUrl = "https://mangaball.net"
|
||||||
|
override val supportsLatest = true
|
||||||
|
private val preferences by getPreferencesLazy()
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
var request = chain.request()
|
||||||
|
if (request.url.pathSegments[0] == "api") {
|
||||||
|
request = request.newBuilder()
|
||||||
|
.header("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.header("X-CSRF-TOKEN", getCSRF())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
if (!response.isSuccessful && response.code == 403) {
|
||||||
|
response.close()
|
||||||
|
request = request.newBuilder()
|
||||||
|
.header("X-CSRF-TOKEN", getCSRF(forceReset = true))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
chain.proceed(request)
|
||||||
|
} else {
|
||||||
|
response
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private var _csrf: String? = null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun getCSRF(document: Document? = null, forceReset: Boolean = false): String {
|
||||||
|
if (_csrf == null || document != null || forceReset) {
|
||||||
|
val doc = document ?: client.newCall(
|
||||||
|
GET(baseUrl, headers),
|
||||||
|
).execute().asJsoup()
|
||||||
|
|
||||||
|
doc.selectFirst("meta[name=csrf-token]")
|
||||||
|
?.attr("content")
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.also { _csrf = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
return _csrf ?: throw Exception("CSRF token not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.set("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
val filters = getFilterList().apply {
|
||||||
|
firstInstance<SortFilter>().state = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchMangaRequest(page, "", filters)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response) =
|
||||||
|
searchMangaParse(response)
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
|
searchMangaRequest(page, "", getFilterList())
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response) =
|
||||||
|
searchMangaParse(response)
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
return if (query.startsWith("https://")) {
|
||||||
|
deepLink(query)
|
||||||
|
} else {
|
||||||
|
super.fetchSearchManga(page, query, filters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val body = FormBody.Builder().apply {
|
||||||
|
add("search_input", query.trim())
|
||||||
|
add("filters[sort]", filters.firstInstance<SortFilter>().selected)
|
||||||
|
add("filters[page]", page.toString())
|
||||||
|
filters.filterIsInstance<TriStateGroupFilter<String>>().forEach { tags ->
|
||||||
|
tags.included.forEach { tag ->
|
||||||
|
add("filters[tag_included_ids][]", tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add("filters[tag_included_mode]", filters.firstInstance<TagIncludeMode>().selected)
|
||||||
|
filters.filterIsInstance<TriStateGroupFilter<String>>().forEach { tags ->
|
||||||
|
tags.excluded.forEach { tag ->
|
||||||
|
add("filters[tag_excluded_ids][]", tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add("filters[tag_excluded_mode]", filters.firstInstance<TagExcludeMode>().selected)
|
||||||
|
add("filters[contentRating]", "any")
|
||||||
|
add("filters[demographic]", filters.firstInstance<DemographicFilter>().selected)
|
||||||
|
add("filters[person]", "any")
|
||||||
|
add("filters[publicationYear]", "")
|
||||||
|
add("filters[publicationStatus]", filters.firstInstance<StatusFilter>().selected)
|
||||||
|
siteLang.forEach {
|
||||||
|
add("filters[translatedLanguage][]", it)
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return POST("$baseUrl/api/v1/title/search-advanced/", headers, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList() = FilterList(
|
||||||
|
SortFilter(),
|
||||||
|
DemographicFilter(),
|
||||||
|
StatusFilter(),
|
||||||
|
ContentFilter(),
|
||||||
|
FormatFilter(),
|
||||||
|
GenreFilter(),
|
||||||
|
OriginFilter(),
|
||||||
|
ThemeFilter(),
|
||||||
|
TagIncludeMode(),
|
||||||
|
TagExcludeMode(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val data = response.parseAs<SearchResponse>()
|
||||||
|
val hideNsfw = hideNsfwPreference()
|
||||||
|
|
||||||
|
val mangas = data.data
|
||||||
|
.filterNot {
|
||||||
|
it.isAdult && hideNsfw
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
SManga.create().apply {
|
||||||
|
url = it.url.toHttpUrl().pathSegments[1]
|
||||||
|
title = it.name
|
||||||
|
thumbnail_url = it.cover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mangas.isEmpty() && hideNsfw) {
|
||||||
|
throw Exception("All results filtered out due to nsfw filter")
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangas, data.hasNextPage())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deepLink(url: String): Observable<MangasPage> {
|
||||||
|
val httpUrl = url.toHttpUrl()
|
||||||
|
if (
|
||||||
|
httpUrl.host == baseUrl.toHttpUrl().host &&
|
||||||
|
httpUrl.pathSegments.size >= 2 &&
|
||||||
|
httpUrl.pathSegments[0] in listOf("title-detail", "chapter-detail")
|
||||||
|
) {
|
||||||
|
val slug = if (httpUrl.pathSegments[0] == "title-detail") {
|
||||||
|
httpUrl.pathSegments[1]
|
||||||
|
} else {
|
||||||
|
client.newCall(GET(httpUrl, headers)).execute()
|
||||||
|
.use { response ->
|
||||||
|
response.asJsoup()
|
||||||
|
.selectFirst(".yoast-schema-graph")!!.data()
|
||||||
|
.parseAs<Yoast>()
|
||||||
|
.graph.first { it.type == "WebPage" }
|
||||||
|
.url!!.toHttpUrl()
|
||||||
|
.pathSegments[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val manga = SManga.create().apply {
|
||||||
|
this.url = slug
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchMangaDetails(manga).map {
|
||||||
|
MangasPage(listOf(it), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception("Unsupported url")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
return GET(getMangaUrl(manga), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga): String {
|
||||||
|
return "$baseUrl/title-detail/${manga.url}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
getCSRF(document)
|
||||||
|
|
||||||
|
return SManga.create().apply {
|
||||||
|
url = document.location().toHttpUrl().pathSegments[1]
|
||||||
|
title = document.selectFirst("#comicDetail h6")!!.ownText()
|
||||||
|
thumbnail_url = document.selectFirst("img.featured-cover")?.absUrl("src")
|
||||||
|
genre = buildList {
|
||||||
|
document.selectFirst("#featuredComicsCarousel img[src*=/flags/]")
|
||||||
|
?.attr("src")?.also {
|
||||||
|
when {
|
||||||
|
it.contains("jp") -> add("Manga")
|
||||||
|
it.contains("kr") -> add("Manhwa")
|
||||||
|
it.contains("cn") -> add("Manhua")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.select("#comicDetail span[data-tag-id]")
|
||||||
|
.mapTo(this) { it.ownText() }
|
||||||
|
}.joinToString()
|
||||||
|
author = document.select("#comicDetail span[data-person-id]")
|
||||||
|
.eachText().joinToString()
|
||||||
|
description = buildString {
|
||||||
|
document.selectFirst("#descriptionContent p")
|
||||||
|
?.also { append(it.wholeText()) }
|
||||||
|
document.selectFirst("#comicDetail span.badge:contains(Published)")
|
||||||
|
?.also { append("\n\n", it.text()) }
|
||||||
|
val titles = document.select("div.alternate-name-container").text().split("/")
|
||||||
|
if (titles.isNotEmpty()) {
|
||||||
|
append("\n\nAlternative Names: \n")
|
||||||
|
titles.forEach {
|
||||||
|
append("- ", it.trim(), "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.trim()
|
||||||
|
status = when (document.selectFirst("span.badge-status")?.text()) {
|
||||||
|
"Ongoing" -> SManga.ONGOING
|
||||||
|
"Completed" -> SManga.COMPLETED
|
||||||
|
"Hiatus" -> SManga.ON_HIATUS
|
||||||
|
"Cancelled" -> SManga.CANCELLED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
val id = manga.url.substringAfterLast("-")
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
.add("title_id", id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return POST("$baseUrl/api/v1/chapter/chapter-listing-by-title-id/", headers, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
(response.request.body as FormBody).also {
|
||||||
|
updateViews(it.value(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
val data = response.parseAs<ChapterListResponse>()
|
||||||
|
|
||||||
|
return data.chapters.flatMap { chapter ->
|
||||||
|
chapter.translations.mapNotNull { translation ->
|
||||||
|
if (translation.language in siteLang) {
|
||||||
|
SChapter.create().apply {
|
||||||
|
url = translation.id
|
||||||
|
name = buildString {
|
||||||
|
if (translation.volume > 0) {
|
||||||
|
append("Vol. ")
|
||||||
|
append(translation.volume)
|
||||||
|
append(" ")
|
||||||
|
}
|
||||||
|
val number = chapter.number.toString().removeSuffix(".0")
|
||||||
|
if (translation.name.contains(number)) {
|
||||||
|
append(translation.name.trim())
|
||||||
|
} else {
|
||||||
|
append("Ch. ")
|
||||||
|
append(number)
|
||||||
|
append(" ")
|
||||||
|
append(translation.name.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chapter_number = chapter.number
|
||||||
|
date_upload = dateFormat.tryParse(translation.date)
|
||||||
|
scanlator = buildString {
|
||||||
|
append(translation.group.name)
|
||||||
|
// id is usually the name of the site the chapter was scraped from
|
||||||
|
// if not then it is generated id of an active group on the site
|
||||||
|
if (groupIdRegex.matchEntire(translation.group.id) == null) {
|
||||||
|
append(" (")
|
||||||
|
append(translation.group.id)
|
||||||
|
append(")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val groupIdRegex = Regex("""[a-z0-9]{24}""")
|
||||||
|
|
||||||
|
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT)
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
return GET(getChapterUrl(chapter), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChapterUrl(chapter: SChapter): String {
|
||||||
|
return "$baseUrl/chapter-detail/${chapter.url}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
getCSRF(document)
|
||||||
|
|
||||||
|
document.select("script:containsData(titleId)").joinToString(";") { it.data() }.also {
|
||||||
|
val titleId = titleIdRegex.find(it)
|
||||||
|
?.groupValues?.get(1)
|
||||||
|
?: return@also
|
||||||
|
val chapterId = chapterIdRegex.find(it)
|
||||||
|
?.groupValues?.get(1)
|
||||||
|
?: return@also
|
||||||
|
|
||||||
|
updateViews(titleId, chapterId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val script = document.select("script:containsData(chapterImages)").joinToString(";") { it.data() }
|
||||||
|
val images = imagesRegex.find(script)
|
||||||
|
?.groupValues?.get(1)
|
||||||
|
?.parseAs<List<String>>()
|
||||||
|
.orEmpty()
|
||||||
|
|
||||||
|
return images.mapIndexed { idx, img ->
|
||||||
|
Page(idx, imageUrl = img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val imagesRegex = Regex("""const\s+chapterImages\s*=\s*JSON\.parse\(`([^`]+)`\)""")
|
||||||
|
private val titleIdRegex = Regex("""const\s+titleId\s*=\s*`([^`]+)`;""")
|
||||||
|
private val chapterIdRegex = Regex("""const\s+chapterId\s*=\s*`([^`]+)`;""")
|
||||||
|
|
||||||
|
private fun updateViews(titleId: String, chapterId: String = "") {
|
||||||
|
val body = FormBody.Builder()
|
||||||
|
.add("title_id", titleId)
|
||||||
|
.add("chapter_id", chapterId)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val request = POST("$baseUrl/api/v1/views/update/", headers, body)
|
||||||
|
|
||||||
|
client.newCall(request)
|
||||||
|
.enqueue(
|
||||||
|
object : Callback {
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
response.closeQuietly()
|
||||||
|
}
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
Log.e(name, "Failed to update views", e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = NSFW_PREF
|
||||||
|
title = "Hide NSFW content"
|
||||||
|
setDefaultValue(false)
|
||||||
|
}.also(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideNsfwPreference() = preferences.getBoolean(NSFW_PREF, false)
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val NSFW_PREF = "nsfw_pref"
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.mangaball
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
|
class MangaBallFactory : SourceFactory {
|
||||||
|
override fun createSources() = listOf(
|
||||||
|
MangaBall("ar", "ar"),
|
||||||
|
MangaBall("bg", "bg"),
|
||||||
|
MangaBall("bn", "bn"),
|
||||||
|
MangaBall("ca", "ca", "ca-ad", "ca-es", "ca-fr", "ca-it", "ca-pt"),
|
||||||
|
MangaBall("cs", "cs"),
|
||||||
|
MangaBall("da", "da"),
|
||||||
|
MangaBall("de", "de"),
|
||||||
|
MangaBall("el", "el"),
|
||||||
|
MangaBall("en", "en"),
|
||||||
|
MangaBall("es", "es", "es-ar", "es-mx", "es-es", "es-la", "es-419"),
|
||||||
|
MangaBall("fa", "fa"),
|
||||||
|
MangaBall("fi", "fi"),
|
||||||
|
MangaBall("fr", "fr"),
|
||||||
|
MangaBall("he", "he"),
|
||||||
|
MangaBall("hi", "hi"),
|
||||||
|
MangaBall("hu", "hu"),
|
||||||
|
MangaBall("id", "id"),
|
||||||
|
MangaBall("it", "it", "it-it"),
|
||||||
|
MangaBall("is", "ib", "ib-is", "is"),
|
||||||
|
MangaBall("ja", "jp"),
|
||||||
|
MangaBall("ko", "kr"),
|
||||||
|
MangaBall("kn", "kn", "kn-in", "kn-my", "kn-sg", "kn-tw"),
|
||||||
|
MangaBall("ml", "ml", "ml-in", "ml-my", "ml-sg", "ml-tw"),
|
||||||
|
MangaBall("ms", "ms"),
|
||||||
|
MangaBall("ne", "ne"),
|
||||||
|
MangaBall("nl", "nl", "nl-be"),
|
||||||
|
MangaBall("no", "no"),
|
||||||
|
MangaBall("pl", "pl"),
|
||||||
|
MangaBall("pt-BR", "pt-br", "pt-pt"),
|
||||||
|
MangaBall("ro", "ro"),
|
||||||
|
MangaBall("ru", "ru"),
|
||||||
|
MangaBall("sk", "sk"),
|
||||||
|
MangaBall("sl", "sl"),
|
||||||
|
MangaBall("sq", "sq"),
|
||||||
|
MangaBall("sr", "sr", "sr-cyrl"),
|
||||||
|
MangaBall("sv", "sv"),
|
||||||
|
MangaBall("ta", "ta"),
|
||||||
|
MangaBall("th", "th", "th-hk", "th-kh", "th-la", "th-my", "th-sg"),
|
||||||
|
MangaBall("tr", "tr"),
|
||||||
|
MangaBall("uk", "uk"),
|
||||||
|
MangaBall("vi", "vi"),
|
||||||
|
MangaBall("zh", "zh", "zh-cn", "zh-hk", "zh-mo", "zh-sg", "zh-tw"),
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.all.mangaball
|
||||||
|
|
||||||
|
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("MangaBall", "Unable to launch activity", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user