Add GalaxyManga[en] & Remove dead Galaxy (multi) (#10654)

* Delete Galaxy (multi)

* GalaxyManga
This commit is contained in:
mrtear 2025-09-21 12:44:54 -06:00 committed by Draff
parent e2895bad8e
commit a687048749
Signed by: Draff
GPG Key ID: E8A89F3211677653
16 changed files with 19 additions and 430 deletions

View File

@ -1,8 +0,0 @@
ext {
extName = 'Galaxy'
extClass = '.GalaxyFactory'
extVersionCode = 5
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,327 +0,0 @@
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("""&amp;#(\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()
}

View File

@ -1,67 +0,0 @@
package eu.kanade.tachiyomi.extension.all.galaxy
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.SourceFactory
import keiyoushi.utils.getPreferencesLazy
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"),
ConfigurableSource {
override val id = 2729515745226258240
override val baseUrl by lazy { getPrefBaseUrl() }
private val preferences: SharedPreferences by getPreferencesLazy()
companion object {
private const val RESTART_APP = ".لتطبيق الإعدادات الجديدة أعد تشغيل التطبيق"
private const val BASE_URL_PREF_TITLE = "تعديل الرابط"
private const val BASE_URL_PREF = "overrideBaseUrl"
private const val BASE_URL_PREF_SUMMARY = ".للاستخدام المؤقت. تحديث التطبيق سيؤدي الى حذف الإعدادات"
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val baseUrlPref = androidx.preference.EditTextPreference(screen.context).apply {
key = BASE_URL_PREF
title = BASE_URL_PREF_TITLE
summary = BASE_URL_PREF_SUMMARY
this.setDefaultValue(super.baseUrl)
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "Default: ${super.baseUrl}"
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP, Toast.LENGTH_LONG).show()
true
}
}
screen.addPreference(baseUrlPref)
}
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, super.baseUrl)!!
init {
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { prefDefaultBaseUrl ->
if (prefDefaultBaseUrl != super.baseUrl) {
preferences.edit()
.putString(BASE_URL_PREF, super.baseUrl)
.putString(DEFAULT_BASE_URL_PREF, super.baseUrl)
.apply()
}
}
}
}
override fun createSources() = listOf(
GalaxyWebtoon(),
GalaxyManga(),
)
}

View File

@ -1,28 +0,0 @@
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,
)

View File

@ -0,0 +1,10 @@
ext {
extName = 'Galaxy Manga'
extClass = '.GalaxyManga'
themePkg = 'mangathemesia'
baseUrl = 'https://galaxymanga.io'
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.extension.en.galaxymanga
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
class GalaxyManga : MangaThemesia(
"Galaxy Manga",
"https://galaxymanga.io",
"en",
)