FlixScans: rewrite for new site (#3808)
* FlixScans: rewrite for new site * remove log * filters * remove commented * dates * rebrand, remove multisrc
|
@ -1,5 +0,0 @@
|
|||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 6
|
|
@ -1,251 +0,0 @@
|
|||
package eu.kanade.tachiyomi.multisrc.flixscans
|
||||
|
||||
import android.util.Log
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
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 kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
|
||||
abstract class FlixScans(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
protected val apiUrl: String = "$baseUrl/api/v1",
|
||||
protected val cdnUrl: String = baseUrl.replace("://", "://media.").plus("/"),
|
||||
) : HttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
protected open val json: Json by injectLazy()
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$apiUrl/webtoon/pages/home/romance", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val result = response.parseAs<HomeDto>()
|
||||
|
||||
val entries = (result.hot + result.topAll + result.topMonth + result.topWeek)
|
||||
.distinctBy { it.id }
|
||||
.map { it.toSManga(cdnUrl) }
|
||||
|
||||
return MangasPage(entries, false)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$apiUrl/search/advance?page=$page&serie_type=webtoon", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val result = response.parseAs<ApiResponse<BrowseSeries>>()
|
||||
|
||||
val entries = result.data.map { it.toSManga(cdnUrl) }
|
||||
val hasNextPage = result.lastPage > result.currentPage
|
||||
|
||||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
private var fetchGenreList: List<GenreHolder> = emptyList()
|
||||
private var fetchGenreCallOngoing = false
|
||||
private var fetchGenreFailed = false
|
||||
private var fetchGenreAttempt = 0
|
||||
|
||||
private fun fetchGenre() {
|
||||
if (fetchGenreAttempt < 3 && (fetchGenreList.isEmpty() || fetchGenreFailed) && !fetchGenreCallOngoing) {
|
||||
fetchGenreCallOngoing = true
|
||||
|
||||
// fetch genre asynchronously as it sometimes hangs
|
||||
client.newCall(fetchGenreRequest()).enqueue(fetchGenreCallback)
|
||||
}
|
||||
}
|
||||
|
||||
private val fetchGenreCallback = object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
fetchGenreAttempt++
|
||||
fetchGenreFailed = true
|
||||
fetchGenreCallOngoing = false
|
||||
|
||||
e.message?.let { Log.e("$name Filters", it) }
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
fetchGenreCallOngoing = false
|
||||
fetchGenreAttempt++
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
fetchGenreFailed = true
|
||||
response.close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val parsed = runCatching {
|
||||
response.use(::fetchGenreParse)
|
||||
}
|
||||
|
||||
fetchGenreFailed = parsed.isFailure
|
||||
fetchGenreList = parsed.getOrElse {
|
||||
Log.e("$name Filters", it.stackTraceToString())
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchGenreRequest(): Request {
|
||||
return GET("$apiUrl/search/genres", headers)
|
||||
}
|
||||
|
||||
private fun fetchGenreParse(response: Response): List<GenreHolder> {
|
||||
return response.parseAs<List<GenreHolder>>()
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
fetchGenre()
|
||||
|
||||
val filters: MutableList<Filter<*>> = mutableListOf(
|
||||
Filter.Header("Ignored when using Text Search"),
|
||||
MainGenreFilter(),
|
||||
TypeFilter(),
|
||||
StatusFilter(),
|
||||
)
|
||||
|
||||
filters += if (fetchGenreList.isNotEmpty()) {
|
||||
listOf(
|
||||
GenreFilter("Genre", fetchGenreList),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
Filter.Separator(),
|
||||
Filter.Header("Press 'reset' to attempt to show Genres"),
|
||||
)
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (query.isNotEmpty()) {
|
||||
val url = "$apiUrl/search/serie".toHttpUrl().newBuilder()
|
||||
.addPathSegment(query.trim())
|
||||
.addQueryParameter("page", page.toString())
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
val advSearchUrl = apiUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegments("search/advance")
|
||||
addQueryParameter("page", page.toString())
|
||||
addQueryParameter("serie_type", "webtoon")
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is GenreFilter -> {
|
||||
filter.checked.let {
|
||||
if (it.isNotEmpty()) {
|
||||
addQueryParameter("genres", it.joinToString(","))
|
||||
}
|
||||
}
|
||||
}
|
||||
is MainGenreFilter -> {
|
||||
if (filter.state > 0) {
|
||||
addQueryParameter("main_genres", filter.selected)
|
||||
}
|
||||
}
|
||||
is TypeFilter -> {
|
||||
if (filter.state > 0) {
|
||||
addQueryParameter("type", filter.selected)
|
||||
}
|
||||
}
|
||||
is StatusFilter -> {
|
||||
if (filter.state > 0) {
|
||||
addQueryParameter("status", filter.selected)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(advSearchUrl, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val (prefix, id) = getPrefixIdFromUrl(manga.url)
|
||||
|
||||
return GET("$apiUrl/webtoon/series/$id/$prefix", headers)
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val result = response.parseAs<SeriesResponse>()
|
||||
|
||||
return result.serie.toSManga(cdnUrl)
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val (prefix, id) = getPrefixIdFromUrl(manga.url)
|
||||
|
||||
return GET("$apiUrl/webtoon/chapters/$id-desc#$prefix", headers)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val chapters = response.parseAs<List<Chapter>>()
|
||||
val prefix = response.request.url.fragment!!
|
||||
|
||||
return chapters.map { it.toSChapter(prefix) }
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val (prefix, id) = getPrefixIdFromUrl(chapter.url)
|
||||
|
||||
return GET("$apiUrl/webtoon/chapters/chapter/$id/$prefix", headers)
|
||||
}
|
||||
|
||||
protected fun getPrefixIdFromUrl(url: String): Pair<String, String> {
|
||||
return with(url.substringAfterLast("/")) {
|
||||
val split = split("-")
|
||||
|
||||
split[0] to split[1]
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val result = response.parseAs<PageListResponse>()
|
||||
|
||||
return result.chapter.chapterData.webtoon.mapIndexed { i, img ->
|
||||
Page(i, "", cdnUrl + img)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
protected inline fun <reified T> Response.parseAs(): T =
|
||||
use { body.string() }.let(json::decodeFromString)
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
package eu.kanade.tachiyomi.multisrc.flixscans
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jsoup.Jsoup
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
data class ApiResponse<T>(
|
||||
val data: List<T>,
|
||||
@SerialName("current_page") val currentPage: Int,
|
||||
@SerialName("last_page") val lastPage: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HomeDto(
|
||||
val hot: List<BrowseSeries>,
|
||||
val topWeek: List<BrowseSeries>,
|
||||
val topMonth: List<BrowseSeries>,
|
||||
val topAll: List<BrowseSeries>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BrowseSeries(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val slug: String,
|
||||
val prefix: Int,
|
||||
val thumbnail: String?,
|
||||
) {
|
||||
fun toSManga(cdnUrl: String) = SManga.create().apply {
|
||||
title = this@BrowseSeries.title
|
||||
url = "/series/$prefix-$id-$slug"
|
||||
thumbnail_url = thumbnail?.let { cdnUrl + it }
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SearchInput(
|
||||
val title: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GenreHolder(
|
||||
val name: String,
|
||||
val id: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SeriesResponse(
|
||||
val serie: Series,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Series(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val slug: String,
|
||||
val prefix: Int,
|
||||
val thumbnail: String?,
|
||||
val story: String?,
|
||||
val serieType: String?,
|
||||
val mainGenres: String?,
|
||||
val otherNames: List<String>? = emptyList(),
|
||||
val status: String?,
|
||||
val type: String?,
|
||||
val authors: List<GenreHolder>? = emptyList(),
|
||||
val artists: List<GenreHolder>? = emptyList(),
|
||||
val genres: List<GenreHolder>? = emptyList(),
|
||||
) {
|
||||
fun toSManga(cdnUrl: String) = SManga.create().apply {
|
||||
title = this@Series.title
|
||||
url = "/series/$prefix-$id-$slug"
|
||||
thumbnail_url = cdnUrl + thumbnail
|
||||
author = authors?.joinToString { it.name.trim() }
|
||||
artist = artists?.joinToString { it.name.trim() }
|
||||
genre = (otherGenres + genres?.map { it.name.trim() }.orEmpty())
|
||||
.distinct().joinToString { it.trim() }
|
||||
description = story?.let { Jsoup.parse(it).text() }
|
||||
if (otherNames?.isNotEmpty() == true) {
|
||||
if (description.isNullOrEmpty()) {
|
||||
description = "Alternative Names:\n"
|
||||
} else {
|
||||
description += "\n\nAlternative Names:\n"
|
||||
}
|
||||
description += otherNames.joinToString("\n") { "• ${it.trim()}" }
|
||||
}
|
||||
status = when (this@Series.status?.trim()) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"completed" -> SManga.COMPLETED
|
||||
"onhold" -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private val otherGenres = listOfNotNull(serieType, mainGenres, type)
|
||||
.map { word ->
|
||||
word.trim().replaceFirstChar {
|
||||
if (it.isLowerCase()) {
|
||||
it.titlecase(Locale.getDefault())
|
||||
} else {
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Chapter(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val slug: String,
|
||||
val createdAt: String? = null,
|
||||
) {
|
||||
fun toSChapter(prefix: String) = SChapter.create().apply {
|
||||
url = "/read/webtoon/$prefix-$id-$slug"
|
||||
name = this@Chapter.name
|
||||
date_upload = runCatching { dateFormat.parse(createdAt!!)!!.time }.getOrDefault(0L)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PageListResponse(
|
||||
val chapter: ChapterPages,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterPages(
|
||||
val chapterData: ChapterPageData,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterPageData(
|
||||
val webtoon: List<String>,
|
||||
)
|
|
@ -1,62 +0,0 @@
|
|||
package eu.kanade.tachiyomi.multisrc.flixscans
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
abstract class SelectFilter(
|
||||
name: String,
|
||||
private val options: List<String>,
|
||||
) : Filter.Select<String>(
|
||||
name,
|
||||
options.toTypedArray(),
|
||||
) {
|
||||
val selected get() = options[state]
|
||||
}
|
||||
|
||||
class CheckBoxFilter(
|
||||
name: String,
|
||||
val id: String,
|
||||
) : Filter.CheckBox(name)
|
||||
|
||||
class GenreFilter(
|
||||
name: String,
|
||||
private val genres: List<GenreHolder>,
|
||||
) : Filter.Group<CheckBoxFilter>(
|
||||
name,
|
||||
genres.map { CheckBoxFilter(it.name.trim(), it.id.toString()) },
|
||||
) {
|
||||
val checked get() = state.filter { it.state }.map { it.id }
|
||||
}
|
||||
|
||||
class MainGenreFilter : SelectFilter(
|
||||
"Main Genre",
|
||||
listOf(
|
||||
"",
|
||||
"fantasy",
|
||||
"romance",
|
||||
"action",
|
||||
"drama",
|
||||
),
|
||||
)
|
||||
|
||||
class TypeFilter : SelectFilter(
|
||||
"Type",
|
||||
listOf(
|
||||
"",
|
||||
"manhwa",
|
||||
"manhua",
|
||||
"manga",
|
||||
"comic",
|
||||
),
|
||||
)
|
||||
|
||||
class StatusFilter : SelectFilter(
|
||||
"Status",
|
||||
listOf(
|
||||
"",
|
||||
"ongoing",
|
||||
"completed",
|
||||
"droped",
|
||||
"onhold",
|
||||
"soon",
|
||||
),
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
ext {
|
||||
extName = 'Galaxy'
|
||||
extClass = '.GalaxyFactory'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 2.7 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 19 KiB |
|
@ -0,0 +1,327 @@
|
|||
package eu.kanade.tachiyomi.extension.all.galaxy
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import java.util.Calendar
|
||||
|
||||
abstract class Galaxy(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
) : HttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return if (page == 1) {
|
||||
GET("$baseUrl/webtoons/romance/home", headers)
|
||||
} else {
|
||||
GET("$baseUrl/webtoons/action/home", headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val entries = document.select(
|
||||
"""div.tabs div[wire:snapshot*=App\\Models\\Serie], main div:has(h2:matches(Today\'s Hot|الرائج اليوم)) a[wire:snapshot*=App\\Models\\Serie]""",
|
||||
).map { element ->
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain(
|
||||
if (element.tagName().equals("a")) {
|
||||
element.absUrl("href")
|
||||
} else {
|
||||
element.selectFirst("a")!!.absUrl("href")
|
||||
},
|
||||
)
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||
title = element.selectFirst("div.text-sm")!!.text()
|
||||
}
|
||||
}.distinctBy { it.url }
|
||||
|
||||
return MangasPage(entries, response.request.url.pathSegments.getOrNull(1) == "romance")
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = "$baseUrl/latest?serie_type=webtoon&main_genres=romance" +
|
||||
if (page > 1) {
|
||||
"&page=$page"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val entries = document.select("div[wire:snapshot*=App\\\\Models\\\\Serie]").map { element ->
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
thumbnail_url = element.selectFirst("img")?.absUrl("src")
|
||||
title = element.select("div.flex a[href*=/series/]").last()!!.text()
|
||||
}
|
||||
}
|
||||
val hasNextPage = document.selectFirst("[role=navigation] button[wire:click*=nextPage]") != null
|
||||
|
||||
return MangasPage(entries, hasNextPage)
|
||||
}
|
||||
|
||||
private var filters: List<FilterData> = emptyList()
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
protected fun launchIO(block: () -> Unit) = scope.launch {
|
||||
try {
|
||||
block()
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
launchIO {
|
||||
if (filters.isEmpty()) {
|
||||
val document = client.newCall(GET("$baseUrl/search", headers)).execute().asJsoup()
|
||||
|
||||
val mainGenre = FilterData(
|
||||
displayName = document.select("label[for$=main_genres]").text(),
|
||||
options = document.select("select[wire:model.live=main_genres] option").map {
|
||||
it.text() to it.attr("value")
|
||||
},
|
||||
queryParameter = "main_genres",
|
||||
)
|
||||
val typeFilter = FilterData(
|
||||
displayName = document.select("label[for$=type]").text(),
|
||||
options = document.select("select[wire:model.live=type] option").map {
|
||||
it.text() to it.attr("value")
|
||||
},
|
||||
queryParameter = "type",
|
||||
)
|
||||
val statusFilter = FilterData(
|
||||
displayName = document.select("label[for$=status]").text(),
|
||||
options = document.select("select[wire:model.live=status] option").map {
|
||||
it.text() to it.attr("value")
|
||||
},
|
||||
queryParameter = "status",
|
||||
)
|
||||
val genreFilter = FilterData(
|
||||
displayName = if (lang == "ar") {
|
||||
"التصنيفات"
|
||||
} else {
|
||||
"Genre"
|
||||
},
|
||||
options = document.select("div[x-data*=genre] > div").map {
|
||||
it.text() to it.attr("wire:key")
|
||||
},
|
||||
queryParameter = "genre",
|
||||
)
|
||||
|
||||
filters = listOf(mainGenre, typeFilter, statusFilter, genreFilter)
|
||||
}
|
||||
}
|
||||
|
||||
val filters: List<Filter<*>> = filters.map {
|
||||
SelectFilter(
|
||||
it.displayName,
|
||||
it.options,
|
||||
it.queryParameter,
|
||||
)
|
||||
}.ifEmpty {
|
||||
listOf(
|
||||
Filter.Header("Press 'reset' to load filters"),
|
||||
)
|
||||
}
|
||||
|
||||
return FilterList(filters)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("serie_type", "webtoon")
|
||||
addQueryParameter("title", query.trim())
|
||||
filters.filterIsInstance<SelectFilter>().forEach {
|
||||
it.addFilterParameter(this)
|
||||
}
|
||||
if (page > 1) {
|
||||
addQueryParameter("page", page.toString())
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return SManga.create().apply {
|
||||
title = document.select("#full_model h3").text()
|
||||
thumbnail_url = document.selectFirst("main img[src*=series/webtoon]")?.absUrl("src")
|
||||
status = when (document.getQueryParam("status")) {
|
||||
"ongoing", "soon" -> SManga.ONGOING
|
||||
"completed", "droped" -> SManga.COMPLETED
|
||||
"onhold" -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
genre = buildList {
|
||||
document.getQueryParam("type")
|
||||
?.capitalize()?.let(::add)
|
||||
document.select("#full_model a[href*=search?genre]")
|
||||
.eachText().let(::addAll)
|
||||
}.joinToString()
|
||||
author = document.select("#full_model [wire:key^=a-]").eachText().joinToString()
|
||||
artist = document.select("#full_model [wire:key^=r-]").eachText().joinToString()
|
||||
description = buildString {
|
||||
append(document.select("#full_model p").text().trim())
|
||||
append("\n\nAlternative Names:\n")
|
||||
document.select("#full_model [wire:key^=n-]")
|
||||
.joinToString("\n") { "• ${it.text().trim().removeMdEscaped()}" }
|
||||
.let(::append)
|
||||
}.trim()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Document.getQueryParam(queryParam: String): String? {
|
||||
return selectFirst("#full_model a[href*=search?$queryParam]")
|
||||
?.absUrl("href")?.toHttpUrlOrNull()?.queryParameter(queryParam)
|
||||
}
|
||||
|
||||
private fun String.capitalize(): String {
|
||||
val result = StringBuilder(length)
|
||||
var capitalize = true
|
||||
for (char in this) {
|
||||
result.append(
|
||||
if (capitalize) {
|
||||
char.uppercase()
|
||||
} else {
|
||||
char.lowercase()
|
||||
},
|
||||
)
|
||||
capitalize = char.isWhitespace()
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
private val mdRegex = Regex("""&#(\d+);""")
|
||||
|
||||
private fun String.removeMdEscaped(): String {
|
||||
val char = mdRegex.find(this)?.groupValues?.get(1)?.toIntOrNull()
|
||||
?: return this
|
||||
|
||||
return replaceFirst(mdRegex, Char(char).toString())
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return document.select("a[href*=/read/]:not([type=button])").map { element ->
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.absUrl("href"))
|
||||
name = element.select("span.font-normal").text()
|
||||
date_upload = element.selectFirst("div:not(:has(> svg)) > span.text-xs")
|
||||
?.text().parseRelativeDate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun String?.parseRelativeDate(): Long {
|
||||
this ?: return 0L
|
||||
|
||||
val number = Regex("""(\d+)""").find(this)?.value?.toIntOrNull() ?: 0
|
||||
val cal = Calendar.getInstance()
|
||||
|
||||
return when {
|
||||
listOf("second", "ثانية").any { contains(it, true) } -> {
|
||||
cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||
}
|
||||
|
||||
contains("دقيقتين", true) -> {
|
||||
cal.apply { add(Calendar.MINUTE, -2) }.timeInMillis
|
||||
}
|
||||
listOf("minute", "دقائق").any { contains(it, true) } -> {
|
||||
cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||
}
|
||||
|
||||
contains("ساعتان", true) -> {
|
||||
cal.apply { add(Calendar.HOUR, -2) }.timeInMillis
|
||||
}
|
||||
listOf("hour", "ساعات").any { contains(it, true) } -> {
|
||||
cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||
}
|
||||
|
||||
contains("يوم", true) -> {
|
||||
cal.apply { add(Calendar.DAY_OF_YEAR, -1) }.timeInMillis
|
||||
}
|
||||
contains("يومين", true) -> {
|
||||
cal.apply { add(Calendar.DAY_OF_YEAR, -2) }.timeInMillis
|
||||
}
|
||||
listOf("day", "أيام").any { contains(it, true) } -> {
|
||||
cal.apply { add(Calendar.DAY_OF_YEAR, -number) }.timeInMillis
|
||||
}
|
||||
|
||||
contains("أسبوع", true) -> {
|
||||
cal.apply { add(Calendar.WEEK_OF_YEAR, -1) }.timeInMillis
|
||||
}
|
||||
contains("أسبوعين", true) -> {
|
||||
cal.apply { add(Calendar.WEEK_OF_YEAR, -2) }.timeInMillis
|
||||
}
|
||||
listOf("week", "أسابيع").any { contains(it, true) } -> {
|
||||
cal.apply { add(Calendar.WEEK_OF_YEAR, -number) }.timeInMillis
|
||||
}
|
||||
|
||||
contains("شهر", true) -> {
|
||||
cal.apply { add(Calendar.MONTH, -1) }.timeInMillis
|
||||
}
|
||||
contains("شهرين", true) -> {
|
||||
cal.apply { add(Calendar.MONTH, -2) }.timeInMillis
|
||||
}
|
||||
listOf("month", "أشهر").any { contains(it, true) } -> {
|
||||
cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||
}
|
||||
|
||||
contains("سنة", true) -> {
|
||||
cal.apply { add(Calendar.YEAR, -1) }.timeInMillis
|
||||
}
|
||||
contains("سنتان", true) -> {
|
||||
cal.apply { add(Calendar.YEAR, -2) }.timeInMillis
|
||||
}
|
||||
listOf("year", "سنوات").any { contains(it, true) } -> {
|
||||
cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||
}
|
||||
|
||||
else -> 0L
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return document.select("[wire:key^=image] img").mapIndexed { idx, img ->
|
||||
Page(idx, imageUrl = img.absUrl("src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package eu.kanade.tachiyomi.extension.all.galaxy
|
||||
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class GalaxyFactory : SourceFactory {
|
||||
|
||||
class GalaxyWebtoon : Galaxy("Galaxy Webtoon", "https://galaxyaction.net", "en") {
|
||||
override val id = 2602904659965278831
|
||||
}
|
||||
|
||||
class GalaxyManga : Galaxy("Galaxy Manga", "https://galaxymanga.net", "ar") {
|
||||
override val id = 2729515745226258240
|
||||
}
|
||||
|
||||
override fun createSources() = listOf(
|
||||
GalaxyWebtoon(),
|
||||
GalaxyManga(),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package eu.kanade.tachiyomi.extension.all.galaxy
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class SelectFilter(
|
||||
name: String,
|
||||
private val options: List<Pair<String, String>>,
|
||||
private val queryParam: String,
|
||||
) : Filter.Select<String>(
|
||||
name,
|
||||
buildList {
|
||||
add("")
|
||||
addAll(options.map { it.first })
|
||||
}.toTypedArray(),
|
||||
) {
|
||||
fun addFilterParameter(url: HttpUrl.Builder) {
|
||||
if (state == 0) return
|
||||
|
||||
url.addQueryParameter(queryParam, options[state - 1].second)
|
||||
}
|
||||
}
|
||||
|
||||
class FilterData(
|
||||
val displayName: String,
|
||||
val options: List<Pair<String, String>>,
|
||||
val queryParameter: String,
|
||||
)
|
|
@ -1,9 +0,0 @@
|
|||
ext {
|
||||
extName = 'Galaxy Manga'
|
||||
extClass = '.GalaxyManga'
|
||||
themePkg = 'flixscans'
|
||||
baseUrl = 'https://flixscans.net'
|
||||
overrideVersionCode = 28
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 22 KiB |
|
@ -1,34 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.ar.galaxymanga
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.flixscans.FlixScans
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import okhttp3.Request
|
||||
|
||||
class GalaxyManga : FlixScans(
|
||||
"جالاكسي مانجا",
|
||||
"https://flixscans.net",
|
||||
"ar",
|
||||
"https://ar.flixscans.site/api/v1",
|
||||
) {
|
||||
override val versionId = 2
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val (prefix, id) = getPrefixIdFromUrl(manga.url)
|
||||
|
||||
return GET("$apiUrl/series/$id/$prefix", headers)
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val (prefix, id) = getPrefixIdFromUrl(manga.url)
|
||||
|
||||
return GET("$apiUrl/chapters/$id-desc#$prefix", headers)
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val (prefix, id) = getPrefixIdFromUrl(chapter.url)
|
||||
|
||||
return GET("$apiUrl/chapters/webtoon/$id/$prefix", headers)
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
ext {
|
||||
extName = 'Flix Scans'
|
||||
extClass = '.FlixScansNet'
|
||||
themePkg = 'flixscans'
|
||||
baseUrl = 'https://flixscans.org'
|
||||
overrideVersionCode = 0
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 17 KiB |
|
@ -1,10 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.en.flixscans
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.flixscans.FlixScans
|
||||
|
||||
class FlixScansNet : FlixScans(
|
||||
"Flix Scans",
|
||||
"https://flixscans.org",
|
||||
"en",
|
||||
"https://flixscans.site/api/v1",
|
||||
)
|