Compare commits

..

No commits in common. "c2b107a8bd17b366286dcbeba06fcdfd9fc0ac7e" and "feb6c74f5c5c3668a0d23744f296cff92e207075" have entirely different histories.

954 changed files with 4535 additions and 11979 deletions

View File

@ -720,10 +720,6 @@ And for a release build of Tachiyomi:
### Android Debugger
> [!IMPORTANT]
> If you didn't build the main app from source with debug enabled and are using a release/beta APK, you **need** a rooted device.
> If you are using an emulator instead, make sure you choose a profile **without** Google Play.
You can leverage the Android Debugger to step through your extension while debugging.
You *cannot* simply use Android Studio's `Debug 'module.name'` -> this will most likely result in an

View File

@ -63,7 +63,6 @@ android {
release {
signingConfig signingConfigs.release
minifyEnabled false
vcsInfo.include false
}
}
@ -75,10 +74,6 @@ android {
buildConfig true
}
packaging {
resources.excludes.add("kotlin-tooling-metadata.json")
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

View File

@ -4,7 +4,7 @@ coroutines_version = "1.6.4"
serialization_version = "1.4.0"
[libraries]
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.4.1" }
gradle-agp = { module = "com.android.tools.build:gradle", version = "8.2.1" }
gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_version" }
gradle-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" }
gradle-kotlinter = { module = "org.jmailen.gradle:kotlinter-gradle", version = "3.13.0" }

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 5
baseVersionCode = 4

View File

@ -30,10 +30,11 @@ abstract class FansubsCat(
override val name: String,
override val baseUrl: String,
override val lang: String,
val apiBaseUrl: String,
val isHentaiSite: Boolean,
) : HttpSource() {
private val apiBaseUrl = "https://api.fansubs.cat"
override val supportsLatest = true
override fun headersBuilder(): Headers.Builder = Headers.Builder()
@ -90,7 +91,7 @@ abstract class FansubsCat(
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$apiBaseUrl/manga/popular/$page", headers)
return GET("$apiBaseUrl/manga/popular/$page?hentai=$isHentaiSite", headers)
}
override fun popularMangaParse(response: Response): MangasPage = parseMangaFromJson(response)
@ -98,7 +99,7 @@ abstract class FansubsCat(
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$apiBaseUrl/manga/recent/$page", headers)
return GET("$apiBaseUrl/manga/recent/$page?hentai=$isHentaiSite", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = parseMangaFromJson(response)
@ -109,15 +110,13 @@ abstract class FansubsCat(
val filterList = if (filters.isEmpty()) getFilterList() else filters
val mangaTypeFilter = filterList.find { it is MangaTypeFilter } as MangaTypeFilter
val stateFilter = filterList.find { it is StateFilter } as StateFilter
val demographyFilter = filterList.find { it is DemographyFilter } as DemographyFilter
val genreFilter = filterList.find { it is GenreTagFilter } as GenreTagFilter
val themeFilter = filterList.find { it is ThemeTagFilter } as ThemeTagFilter
val builder = "$apiBaseUrl/manga/search/$page".toHttpUrl().newBuilder()
val builder = "$apiBaseUrl/manga/search/$page?hentai=$isHentaiSite".toHttpUrl().newBuilder()
mangaTypeFilter.addQueryParameter(builder)
stateFilter.addQueryParameter(builder)
if (!isHentaiSite) {
val demographyFilter = filterList.find { it is DemographyFilter } as DemographyFilter
demographyFilter.addQueryParameter(builder)
}
demographyFilter.addQueryParameter(builder)
genreFilter.addQueryParameter(builder)
themeFilter.addQueryParameter(builder)
if (query.isNotBlank()) {
@ -132,7 +131,7 @@ abstract class FansubsCat(
override fun mangaDetailsRequest(manga: SManga): Request {
return GET(
"$apiBaseUrl/manga/details/${manga.url.substringAfterLast('/')}",
"$apiBaseUrl/manga/details/${manga.url.substringAfterLast('/')}?hentai=$isHentaiSite",
headers,
)
}
@ -167,7 +166,7 @@ abstract class FansubsCat(
override fun chapterListRequest(manga: SManga): Request {
return GET(
"$apiBaseUrl/manga/chapters/${manga.url.substringAfterLast('/')}",
"$apiBaseUrl/manga/chapters/${manga.url.substringAfterLast('/')}?hentai=$isHentaiSite",
headers,
)
}
@ -179,7 +178,7 @@ abstract class FansubsCat(
override fun pageListRequest(chapter: SChapter): Request {
return GET(
"$apiBaseUrl/manga/pages/${chapter.url.substringAfterLast('/')}",
"$apiBaseUrl/manga/pages/${chapter.url.substringAfterLast('/')}?hentai=$isHentaiSite",
headers,
)
}

View File

@ -0,0 +1,5 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 6

View File

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

View File

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

View File

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

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 2
baseVersionCode = 1

View File

@ -294,7 +294,7 @@ abstract class GalleryAdults(
val categoryFilters = filters.filterIsInstance<CategoryFilters>().firstOrNull()
// Only for query string or multiple tags
val url = "$baseUrl/search/".toHttpUrl().newBuilder().apply {
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
getSortOrderURIs().forEachIndexed { index, pair ->
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
}
@ -310,7 +310,7 @@ abstract class GalleryAdults(
addEncodedQueryParameter(intermediateSearchKey, buildQueryString(selectedGenres.map { it.name }, query))
addPageUri(page)
}
return GET(url.build(), headers)
return GET(url.build())
}
protected open val advancedSearchKey = "key"
@ -331,7 +331,7 @@ abstract class GalleryAdults(
// Advanced search
val advancedSearchFilters = filters.filterIsInstance<AdvancedTextFilter>()
val url = "$baseUrl/$advancedSearchUri/".toHttpUrl().newBuilder().apply {
val url = "$baseUrl/$advancedSearchUri".toHttpUrl().newBuilder().apply {
getSortOrderURIs().forEachIndexed { index, pair ->
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
}
@ -379,7 +379,7 @@ abstract class GalleryAdults(
addEncodedQueryParameter(advancedSearchKey, keys.joinToString("+"))
addPageUri(page)
}
return GET(url.build(), headers)
return GET(url.build())
}
/**

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 23
baseVersionCode = 22

View File

@ -53,13 +53,7 @@ abstract class GroupLe(
.contains("internal/redirect") or (response.code == 301)
)
) {
if (originalRequest.url.toString().contains("/list?")) {
throw IOException("Смените домен: Поисковик > Расширения > $name > ⚙\uFE0F")
}
throw IOException(
"URL серии изменился. Перенесите/мигрируйте с $name " +
"на $name (или смежный с GroupLe), чтобы список глав обновился",
)
throw IOException("Ссылка на мангу была изменена. Перемигрируйте мангу на тот же (или смежный с GroupLe) источник или передобавьте из Поисковика/Каталога.")
}
response
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 12
baseVersionCode = 11

View File

@ -194,14 +194,12 @@ open class Kemono(
override fun imageRequest(page: Page): Request {
val imageUrl = page.imageUrl!!
if (!preferences.getBoolean(USE_LOW_RES_IMG, false)) return GET(imageUrl, headers)
val index = imageUrl.indexOf('/', 8)
val index = imageUrl.indexOf('/', startIndex = 8) // https://
val url = buildString {
append(imageUrl, 0, index)
append("/thumbnail/data")
append(imageUrl.substring(index))
append("/thumbnail")
append(imageUrl, index, imageUrl.length)
}
return GET(url, headers)
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 6
baseVersionCode = 5

View File

@ -319,7 +319,7 @@ abstract class LectorTmo(
return GET(chapter.url, tmoHeaders)
}
override fun pageListParse(document: Document): List<Page> {
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
var doc = redirectToReadPage(document)
val currentUrl = doc.location()
@ -336,24 +336,21 @@ abstract class LectorTmo(
.build()
doc = client.newCall(GET(newUrl, redirectHeaders)).execute().asJsoup()
}
val imagesScript = doc.selectFirst("script:containsData(var dirPath):containsData(var images)")
imagesScript?.data()?.let {
val dirPath = DIRPATH_REGEX.find(imagesScript.data())?.groupValues?.get(1)
val images = IMAGES_REGEX.find(imagesScript.data())?.groupValues?.get(1)?.split(",")?.map { img ->
img.trim().removeSurrounding("\"")
}
if (dirPath != null && images != null) {
return images.mapIndexed { i, img ->
Page(i, doc.location(), "$dirPath$img")
}
}
}
doc.select("div.viewer-container img:not(noscript img)").let {
return it.mapIndexed { i, img ->
Page(i, doc.location(), img.imgAttr())
}
doc.select("div.viewer-container img:not(noscript img)").forEach {
add(
Page(
size,
doc.location(),
it.let {
if (it.hasAttr("data-src")) {
it.attr("abs:data-src")
} else {
it.attr("abs:src")
}
},
),
)
}
}
@ -423,13 +420,6 @@ abstract class LectorTmo(
return document
}
private fun Element.imgAttr(): String {
return when {
this.hasAttr("data-src") -> this.attr("abs:data-src")
else -> this.attr("abs:src")
}
}
private fun String.unescapeUrl(): String {
return if (this.startsWith("http:\\/\\/") || this.startsWith("https:\\/\\/")) {
this.replace("\\/", "/")
@ -615,9 +605,6 @@ abstract class LectorTmo(
}
companion object {
val DIRPATH_REGEX = """var\s+dirPath\s*=\s*'(.*?)'\s*;""".toRegex()
val IMAGES_REGEX = """var\s+images\s*=.*\[(.*?)\]\s*'\s*\)\s*;""".toRegex()
private const val SCANLATOR_PREF = "scanlatorPref"
private const val SCANLATOR_PREF_TITLE = "Mostrar todos los scanlator"
private const val SCANLATOR_PREF_SUMMARY = "Se mostraran capítulos repetidos pero con diferentes Scanlators"

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 28
baseVersionCode = 25

View File

@ -1,295 +0,0 @@
package eu.kanade.tachiyomi.multisrc.libgroup
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class Data<T>(
val data: T,
)
@Serializable
class Constants(
@SerialName("ageRestriction") val ageRestrictions: List<IdLabelSiteType>,
@SerialName("format") val formats: List<IdNameSiteType>,
val genres: List<IdNameSiteType>,
val imageServers: List<ImageServer>,
@SerialName("scanlateStatus") val scanlateStatuses: List<IdLabelSiteType>,
@SerialName("status") val titleStatuses: List<IdLabelSiteType>,
val tags: List<IdNameSiteType>,
val types: List<IdLabelSiteType>,
) {
@Serializable
class IdLabelSiteType(
val id: Int,
val label: String,
@SerialName("site_ids") val siteIds: List<Int>,
)
@Serializable
class IdNameSiteType(
val id: Int,
val name: String,
@SerialName("site_ids") val siteIds: List<Int>,
)
@Serializable
class ImageServer(
val id: String,
val label: String,
val url: String,
@SerialName("site_ids") val siteIds: List<Int>,
)
fun getServer(isServers: String?, siteId: Int): ImageServer =
if (!isServers.isNullOrBlank()) {
imageServers.first { it.id == isServers && it.siteIds.contains(siteId) }
} else {
imageServers.first { it.siteIds.contains(siteId) }
}
fun getCategories(siteId: Int): List<IdLabelSiteType> = types.filter { it.siteIds.contains(siteId) }
fun getFormats(siteId: Int): List<IdNameSiteType> = formats.filter { it.siteIds.contains(siteId) }
fun getGenres(siteId: Int): List<IdNameSiteType> = genres.filter { it.siteIds.contains(siteId) }
fun getTags(siteId: Int): List<IdNameSiteType> = tags.filter { it.siteIds.contains(siteId) }
fun getScanlateStatuses(siteId: Int): List<IdLabelSiteType> = scanlateStatuses.filter { it.siteIds.contains(siteId) }
fun getTitleStatuses(siteId: Int): List<IdLabelSiteType> = titleStatuses.filter { it.siteIds.contains(siteId) }
fun getAgeRestrictions(siteId: Int): List<IdLabelSiteType> = ageRestrictions.filter { it.siteIds.contains(siteId) }
}
@Serializable
class MangasPageDto(
val data: List<MangaShort>,
val meta: MangaPageMeta,
) {
@Serializable
class MangaPageMeta(
@SerialName("has_next_page") val hasNextPage: Boolean,
)
fun mapToSManga(isEng: String): List<SManga> {
return this.data.map { it.toSManga(isEng) }
}
}
@Serializable
class MangaShort(
val name: String,
@SerialName("rus_name") val rusName: String?,
@SerialName("eng_name") val engName: String?,
@SerialName("slug_url") val slugUrl: String,
val cover: Cover,
) {
@Serializable
data class Cover(
val default: String?,
)
fun toSManga(isEng: String) = SManga.create().apply {
title = getSelectedLanguage(isEng, rusName, engName, name)
thumbnail_url = cover.default.orEmpty()
url = "/$slugUrl"
}
}
@Serializable
class Manga(
val type: LabelType,
val ageRestriction: LabelType,
val rating: Rating,
val genres: List<NameType>,
val tags: List<NameType>,
@SerialName("rus_name") val rusName: String?,
@SerialName("eng_name") val engName: String?,
val name: String,
val cover: MangaShort.Cover,
val authors: List<NameType>,
val artists: List<NameType>,
val status: LabelType,
val scanlateStatus: LabelType,
@SerialName("is_licensed") val isLicensed: Boolean,
val otherNames: List<String>,
val summary: String,
) {
@Serializable
class LabelType(
val label: String,
)
@Serializable
class NameType(
val name: String,
)
@Serializable
class Rating(
val average: Float,
val votes: Int,
)
fun toSManga(isEng: String): SManga = SManga.create().apply {
title = getSelectedLanguage(isEng, rusName, engName, name)
thumbnail_url = cover.default
author = authors.joinToString { it.name }
artist = artists.joinToString { it.name }
status = parseStatus(isLicensed, scanlateStatus.label, this@Manga.status.label)
genre = type.label.ifBlank { "Манга" } + ", " + ageRestriction.label + ", " +
genres.joinToString { it.name.trim() } + ", " + tags.joinToString { it.name.trim() }
description = getOppositeLanguage(isEng, rusName, engName) + rating.average.parseAverage() + " " + rating.average +
" (голосов: " + rating.votes + ")\n" + otherNames.joinAltNames() + summary
}
private fun Float.parseAverage(): String {
return when {
this > 9.5 -> "★★★★★"
this > 8.5 -> "★★★★✬"
this > 7.5 -> "★★★★☆"
this > 6.5 -> "★★★✬☆"
this > 5.5 -> "★★★☆☆"
this > 4.5 -> "★★✬☆☆"
this > 3.5 -> "★★☆☆☆"
this > 2.5 -> "★✬☆☆☆"
this > 1.5 -> "★☆☆☆☆"
this > 0.5 -> "✬☆☆☆☆"
else -> "☆☆☆☆☆"
}
}
private fun parseStatus(isLicensed: Boolean, statusTranslate: String, statusTitle: String): Int = when {
isLicensed -> SManga.LICENSED
statusTranslate == "Завершён" && statusTitle == "Приостановлен" || statusTranslate == "Заморожен" || statusTranslate == "Заброшен" -> SManga.ON_HIATUS
statusTranslate == "Завершён" && statusTitle == "Выпуск прекращён" -> SManga.CANCELLED
statusTranslate == "Продолжается" -> SManga.ONGOING
statusTranslate == "Выходит" -> SManga.ONGOING
statusTranslate == "Завершён" -> SManga.COMPLETED
statusTranslate == "Вышло" -> SManga.PUBLISHING_FINISHED
else -> when (statusTitle) {
"Онгоинг" -> SManga.ONGOING
"Анонс" -> SManga.ONGOING
"Завершён" -> SManga.COMPLETED
"Приостановлен" -> SManga.ON_HIATUS
"Выпуск прекращён" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
private fun List<String>.joinAltNames(): String = when {
this.isNotEmpty() -> "Альтернативные названия:\n" + this.joinToString(" / ") + "\n\n"
else -> ""
}
}
private fun getSelectedLanguage(isEng: String, rusName: String?, engName: String?, name: String): String = when {
isEng == "rus" && rusName.orEmpty().isNotEmpty() -> rusName!!
isEng == "eng" && engName.orEmpty().isNotEmpty() -> engName!!
else -> name
}
private fun getOppositeLanguage(isEng: String, rusName: String?, engName: String?): String = when {
isEng == "eng" && rusName.orEmpty().isNotEmpty() -> rusName + "\n"
isEng == "rus" && engName.orEmpty().isNotEmpty() -> engName + "\n"
else -> ""
}
@Serializable
class Chapter(
val id: Int,
@SerialName("branches_count") val branchesCount: Int,
val branches: List<Branch>,
val name: String?,
val number: String,
val volume: String,
@SerialName("item_number") val itemNumber: Float?,
) {
@Serializable
class Branch(
@SerialName("branch_id") val branchId: Int?,
@SerialName("created_at") val createdAt: String,
val teams: List<Team>,
val user: User,
) {
@Serializable
class Team(
val name: String,
)
@Serializable
class User(
val username: String,
)
}
private fun first(branchId: Int? = null): Branch? {
return runCatching { if (branchId != null) branches.first { it.branchId == branchId } else branches.first() }.getOrNull()
}
private fun getTeamName(branchId: Int? = null): String? {
return runCatching { first(branchId)!!.teams.first().name }.getOrNull()
}
private fun getUserName(branchId: Int? = null): String? {
return runCatching { first(branchId)!!.user.username }.getOrNull()
}
fun toSChapter(slugUrl: String, branchId: Int? = null, isScanUser: Boolean): SChapter = SChapter.create().apply {
val chapterName = "Том $volume. Глава $number"
name = if (this@Chapter.name.isNullOrBlank()) chapterName else "$chapterName - ${this@Chapter.name}"
val branchStr = if (branchId != null) "&branch_id=$branchId" else ""
url = "/$slugUrl/chapter?$branchStr&volume=$volume&number=$number"
scanlator = getTeamName(branchId) ?: if (isScanUser) getUserName(branchId) else null
date_upload = runCatching { LibGroup.simpleDateFormat.parse(first(branchId)!!.createdAt)!!.time }.getOrDefault(0L)
chapter_number = itemNumber ?: -1f
}
}
fun List<Chapter>.getBranchCount(): Int = this.maxOf { chapter -> chapter.branches.size }
@Serializable
class Branch(
val id: Int,
)
@Serializable
class Pages(
val pages: List<MangaPage>,
) {
@Serializable
class MangaPage(
val slug: Int,
val url: String,
)
fun toPageList(): List<Page> = pages.map { Page(it.slug, it.url) }
}
@Serializable
class AuthToken(
private val auth: Auth,
private val token: Token,
) {
@Serializable
class Auth(
val id: Int,
)
@Serializable
class Token(
val timestamp: Long,
@SerialName("expires_in") val expiresIn: Long,
@SerialName("token_type") val tokenType: String,
@SerialName("access_token") val accessToken: String,
)
fun isExpired(): Boolean {
val currentTime = System.currentTimeMillis()
val expiresIn = token.timestamp + (token.expiresIn * 1000)
return expiresIn < currentTime
}
fun getToken(): String = "${token.tokenType} ${token.accessToken}"
fun getUserId(): Int = auth.id
}

View File

@ -19,7 +19,7 @@ class LibUrlActivity : Activity() {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 0) {
val titleid = pathSegments[2]
val titleid = pathSegments[0]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${LibGroup.PREFIX_SLUG_SEARCH}$titleid")

View File

@ -1,9 +1,7 @@
package eu.kanade.tachiyomi.extension.en.likemanga
package eu.kanade.tachiyomi.multisrc.likemanga
import android.util.Base64
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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
@ -22,29 +20,25 @@ import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class LikeManga : ParsedHttpSource() {
override val name = "LikeManga"
override val baseUrl = "https://likemanga.io"
override val lang = "en"
abstract class LikeManga(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : ParsedHttpSource() {
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(1, 2)
.build()
private val json: Json by injectLazy()
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int): Request {
return searchMangaRequest(page, "", FilterList(SortFilter("top-manga")))
}
@ -63,19 +57,6 @@ class LikeManga : ParsedHttpSource() {
override fun latestUpdatesSelector() = searchMangaSelector()
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(URL_SEARCH_PREFIX)) {
val url = "$baseUrl/${query.substringAfter(URL_SEARCH_PREFIX)}"
return client.newCall(GET(url, headers)).asObservableSuccess().map { response ->
MangasPage(
mangas = listOf(mangaDetailsParse(response).apply { setUrlWithoutDomain(url) }),
hasNextPage = false,
)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
addQueryParameter("act", "searchadvance")
@ -259,6 +240,12 @@ class LikeManga : ParsedHttpSource() {
override fun chapterListSelector() = ".wp-manga-chapter"
private fun String?.parseDate(): Long {
return runCatching {
dateFormat.parse(this!!)!!.time
}.getOrDefault(0L)
}
override fun pageListParse(document: Document): List<Page> {
val element = document.selectFirst("div.reading input#next_img_token")
@ -290,14 +277,12 @@ class LikeManga : ParsedHttpSource() {
}
}
private fun String?.parseDate(): Long =
try { dateFormat.parse(this!!)!!.time } catch (_: Exception) { 0L }
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
companion object {
const val URL_SEARCH_PREFIX = "slug:"
private val dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH)
val dateFormat by lazy {
SimpleDateFormat("MMMM dd, yyyy", Locale.ENGLISH)
}
private val chapterPageCountRegex = Regex("""load_list_chapter\((\d+)\)""")
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.en.likemanga
package eu.kanade.tachiyomi.multisrc.likemanga
import eu.kanade.tachiyomi.source.model.Filter

View File

@ -615,7 +615,6 @@ abstract class Madara(
"Đã hoàn thành",
"Завершено",
"Tamamlanan",
"Complété",
)
protected val ongoingStatusList: Array<String> = arrayOf(
@ -623,7 +622,7 @@ abstract class Madara(
"Em Andamento", "En cours", "En Cours", "En cours de publication", "Ativo", "Lançando", "Đang Tiến Hành", "Devam Ediyor",
"Devam ediyor", "In Corso", "In Arrivo", "مستمرة", "مستمر", "En Curso", "En curso", "Emision",
"Curso", "En marcha", "Publicandose", "En emision", "连载中", "Em Lançamento", "Devam Ediyo",
"Đang làm", "Em postagem", "Devam Eden", "Em progresso", "Em curso",
"Đang làm", "Em postagem", "Devam Eden", "Em progresso",
)
protected val hiatusStatusList: Array<String> = arrayOf(
@ -636,7 +635,6 @@ abstract class Madara(
"متوقف",
"En Pause",
"Заморожено",
"En attente",
)
protected val canceledStatusList: Array<String> = arrayOf(
@ -648,7 +646,6 @@ abstract class Madara(
"ملغي",
"Abandonné",
"Заброшено",
"Annulé",
)
override fun mangaDetailsParse(document: Document): SManga {

View File

@ -208,7 +208,7 @@ constructor(
override fun searchMangaNextPageSelector(): String? = ".pagination a[rel=next]"
protected open fun parseSearchDirectory(page: Int): MangasPage {
protected fun parseSearchDirectory(page: Int): MangasPage {
val manga = searchDirectory.subList((page - 1) * 24, min(page * 24, searchDirectory.size))
.map {
SManga.create().apply {

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 3
baseVersionCode = 2

View File

@ -36,12 +36,12 @@ abstract class Senkuro(
override val supportsLatest = false
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", "Tachiyomi (+https://github.com/keiyoushi/extensions-source)")
.add("User-Agent", "Tachiyomi (+https://github.com/tachiyomiorg/tachiyomi)")
.add("Content-Type", "application/json")
override val client: OkHttpClient =
network.client.newBuilder()
.rateLimit(3)
.rateLimit(5)
.build()
private inline fun <reified T : Any> T.toJsonRequestBody(): RequestBody =

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="eu.kanade.tachiyomi.multisrc.terrascan.TerraScanUrlActivity"
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="${SOURCEHOST}"
android:pathPattern="/manga/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,245 +0,0 @@
package eu.kanade.tachiyomi.multisrc.terrascan
import eu.kanade.tachiyomi.network.GET
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
abstract class TerraScan(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("dd-MM-yyyy", Locale("pt", "BR")),
) : ParsedHttpSource() {
override val supportsLatest: Boolean = true
override val client = network.cloudflareClient
private val noRedirectClient = network.cloudflareClient.newBuilder()
.followRedirects(false)
.build()
private val json: Json by injectLazy()
private var genresList: List<Genre> = emptyList()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga?q=p&page=$page", headers)
open val popularMangaTitleSelector: String = "p, h3"
open val popularMangaThumbnailSelector: String = "img"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst(popularMangaTitleSelector)!!.ownText()
thumbnail_url = element.selectFirst(popularMangaThumbnailSelector)?.srcAttr()
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
}
override fun popularMangaNextPageSelector() = ".pagination > .page-item:not(.disabled):last-child"
override fun popularMangaSelector(): String = ".series-paginated .grid-item-series, .series-paginated .series"
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
if (genresList.isEmpty()) {
genresList = parseGenres(document)
}
val mangas = document.select(popularMangaSelector())
.map(::popularMangaFromElement)
return MangasPage(mangas, document.selectFirst(popularMangaNextPageSelector()) != null)
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/manga?q=u&page=$page", headers)
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesSelector() = popularMangaSelector()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(URL_SEARCH_PREFIX)) {
val slug = query.substringAfter(URL_SEARCH_PREFIX)
return client.newCall(GET("$baseUrl/manga/$slug", headers))
.asObservableSuccess().map { response ->
MangasPage(listOf(mangaDetailsParse(response)), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder()
if (query.isNotBlank()) {
url.addPathSegment("search")
.addQueryParameter("q", query)
return GET(url.build(), headers)
}
url.addPathSegment("manga")
filters.forEach { filter ->
when (filter) {
is GenreFilter -> {
filter.state.forEach {
if (it.state) {
url.addQueryParameter(it.query, it.value)
}
}
}
else -> {}
}
}
url.addQueryParameter("page", "$page")
return GET(url.build(), headers)
}
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = null
override fun searchMangaSelector() = ".col-6.col-sm-3.col-md-3.col-lg-2.p-1"
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.url.pathSegments.contains("search")) {
return searchByQueryMangaParse(response)
}
return super.searchMangaParse(response)
}
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<out Any>>()
if (genresList.isNotEmpty()) {
filters += GenreFilter(
title = "Gêneros",
genres = genresList,
)
} else {
filters += Filter.Header("Aperte 'Redefinir' mostrar os gêneros disponíveis")
}
return FilterList(filters)
}
open val mangaDetailsContainerSelector: String = "main"
open val mangaDetailsTitleSelector: String = "h1"
open val mangaDetailsThumbnailSelector: String = "img"
open val mangaDetailsDescriptionSelector: String = "p"
open val mangaDetailsGenreSelector: String = ".card:has(h5:contains(Categorias)) a, .card:has(h5:contains(Categorias)) div"
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
with(document.selectFirst(mangaDetailsContainerSelector)!!) {
title = selectFirst(mangaDetailsTitleSelector)!!.text()
thumbnail_url = selectFirst(mangaDetailsThumbnailSelector)?.absUrl("href")
description = selectFirst(mangaDetailsDescriptionSelector)?.text()
genre = document.select(mangaDetailsGenreSelector)
.joinToString { it.ownText() }
}
setUrlWithoutDomain(document.location())
}
override fun chapterFromElement(element: Element) = SChapter.create().apply {
with(element.selectFirst("h5")!!) {
name = ownText()
date_upload = selectFirst("div")!!.ownText().toDate()
}
setUrlWithoutDomain(element.absUrl("href"))
}
override fun chapterListSelector() = ".col-chapter a"
override fun pageListParse(document: Document): List<Page> {
val mangaChapterUrl = document.location()
val maxPage = findPageCount(mangaChapterUrl)
return (1..maxPage).map { page -> Page(page - 1, "$mangaChapterUrl/$page") }
}
override fun imageUrlParse(document: Document) = document.selectFirst("main img")!!.srcAttr()
private fun searchByQueryMangaParse(response: Response): MangasPage {
val fragment = Jsoup.parseBodyFragment(
json.decodeFromString<String>(response.body.string()),
baseUrl,
)
return MangasPage(
mangas = fragment.select(searchMangaSelector()).map(::searchMangaFromElement),
hasNextPage = false,
)
}
private fun findPageCount(pageUrl: String): Int {
var lowerBound = 1
var upperBound = 100
while (lowerBound <= upperBound) {
val midpoint = lowerBound + (upperBound - lowerBound) / 2
val request = Request.Builder().apply {
url("$pageUrl/$midpoint")
headers(headers)
head()
}.build()
val response = try {
noRedirectClient.newCall(request).execute()
} catch (e: Exception) {
throw Exception("Failed to fetch $pageUrl")
}
if (response.code == 302) {
upperBound = midpoint - 1
} else {
lowerBound = midpoint + 1
}
}
return lowerBound
}
private fun Element.srcAttr(): String = when {
hasAttr("data-src") -> absUrl("data-src")
else -> absUrl("src")
}
private fun String.toDate() = try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
open val genreFilterSelector: String = "form div > div:has(input) div"
private fun parseGenres(document: Document): List<Genre> {
return document.select(genreFilterSelector)
.map { element ->
val input = element.selectFirst("input")!!
Genre(
name = element.selectFirst("label")!!.ownText(),
query = input.attr("name"),
value = input.attr("value"),
)
}
}
companion object {
const val URL_SEARCH_PREFIX = "slug:"
}
}

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 6
baseVersionCode = 5
dependencies {
api(project(":lib:i18n"))

View File

@ -163,8 +163,6 @@ abstract class WPComics(
val minuteWords = listOf("minute", "phút")
val hourWords = listOf("hour", "giờ")
val dayWords = listOf("day", "ngày")
val monthWords = listOf("month", "tháng")
val yearWords = listOf("year", "năm")
val agoWords = listOf("ago", "trước")
return try {
@ -173,8 +171,6 @@ abstract class WPComics(
val calendar = Calendar.getInstance()
when {
yearWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.YEAR, -trimmedDate[0].toInt()) }
monthWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) }
dayWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
hourWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
minuteWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }

View File

@ -8,9 +8,9 @@ import androidx.preference.PreferenceScreen
import okhttp3.Headers
/**
* Helper function to return UserAgentType based on SharedPreference value
*/
/**
* Helper function to return UserAgentType based on SharedPreference value
*/
fun SharedPreferences.getPrefUAType(): UserAgentType {
return when (getString(PREF_KEY_RANDOM_UA, "off")) {
"mobile" -> UserAgentType.MOBILE

View File

@ -1,7 +1,7 @@
ext {
extName = 'Akuma'
extClass = '.AkumaFactory'
extVersionCode = 4
extClass = '.Akuma'
extVersionCode = 1
isNsfw = true
}

View File

@ -21,20 +21,15 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.io.IOException
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class Akuma(
override val lang: String,
private val akumaLang: String,
) : ParsedHttpSource() {
class Akuma : ParsedHttpSource() {
override val name = "Akuma"
override val baseUrl = "https://akuma.moe"
override val lang = "all"
override val supportsLatest = false
private var nextHash: String? = null
@ -43,9 +38,6 @@ class Akuma(
private val ddosGuardIntercept = DDosGuardInterceptor(network.client)
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(ddosGuardIntercept)
.addInterceptor(::tokenInterceptor)
@ -110,19 +102,12 @@ class Akuma(
.add("view", "3")
.build()
val url = baseUrl.toHttpUrlOrNull()!!.newBuilder()
if (page == 1) {
return if (page == 1) {
nextHash = null
POST(baseUrl, headers, payload)
} else {
url.addQueryParameter("cursor", nextHash)
POST("$baseUrl/?cursor=$nextHash", headers, payload)
}
if (lang != "all") {
// append like `q=language:english$`
url.addQueryParameter("q", "language:$akumaLang$")
}
return POST(url.toString(), headers, payload)
}
override fun popularMangaSelector() = ".post-loop li"
@ -131,10 +116,6 @@ class Akuma(
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
if (document.text().contains("Max keywords of 3 exceeded.")) {
throw Exception("Login required for more than 3 filters")
} else if (document.text().contains("Max keywords of 8 exceeded.")) throw Exception("Only max of 8 filters are allowed")
val mangas = document.select(popularMangaSelector()).map { element ->
popularMangaFromElement(element)
}
@ -173,39 +154,8 @@ class Akuma(
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val request = popularMangaRequest(page)
val finalQuery: MutableList<String> = mutableListOf(query)
if (lang != "all") {
finalQuery.add("language: $akumaLang$")
}
filters.forEach { filter ->
when (filter) {
is TextFilter -> {
if (filter.state.isNotEmpty()) {
finalQuery.addAll(
filter.state.split(",").filter { it.isNotBlank() }.map {
(if (it.trim().startsWith("-")) "-" else "") + "${filter.tag}:\"${it.trim().replace("-", "")}\""
},
)
}
}
is OptionFilter -> {
if (filter.state > 0) finalQuery.add("opt:${filter.getValue()}")
}
is CategoryFilter -> {
filter.state.forEach {
when {
it.isIncluded() -> finalQuery.add("category:\"${it.name}\"")
it.isExcluded() -> finalQuery.add("-category:\"${it.name}\"")
}
}
}
else -> {}
}
}
val url = request.url.newBuilder()
.setQueryParameter("q", finalQuery.joinToString(" "))
.addQueryParameter("q", query.trim())
.build()
return request.newBuilder()
@ -218,62 +168,24 @@ class Akuma(
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun mangaDetailsParse(document: Document) = with(document) {
SManga.create().apply {
title = select(".entry-title").text()
thumbnail_url = select(".img-thumbnail").attr("abs:src")
author = select(".group~.value").eachText().joinToString()
artist = select(".artist~.value").eachText().joinToString()
val characters = select(".character~.value").eachText()
val parodies = select(".parody~.value").eachText()
val males = select(".male~.value")
.map { "${it.text()}" }
val females = select(".female~.value")
.map { "${it.text()}" }
val others = select(".other~.value")
.map { "${it.text()}" }
// show all in tags for quickly searching
genre = (males + females + others).joinToString()
description = buildString {
append(
"Full English and Japanese title: \n",
select(".entry-title").text(),
"\n",
select(".entry-title+span").text(),
"\n\n",
)
// translated should show up in the description
append("Language: ", select(".language~.value").eachText().joinToString(), "\n")
append("Pages: ", select(".pages .value").text(), "\n")
append("Upload Date: ", select(".date .value>time").text().replace(" ", ", ") + " UTC", "\n")
append("Categories: ", selectFirst(".info-list .value")?.text() ?: "Unknown", "\n\n")
// show followings for easy to reference
parodies.takeIf { it.isNotEmpty() }?.let { append("Parodies: ", parodies.joinToString(), "\n") }
characters.takeIf { it.isNotEmpty() }?.let { append("Characters: ", characters.joinToString(), "\n") }
}
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.UNKNOWN
}
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.select(".entry-title").text()
thumbnail_url = document.select(".img-thumbnail").attr("abs:src")
author = document.select("li.meta-data > span.artist + span.value").text()
genre = document.select(".info-list a").joinToString { it.text() }
description = document.select(".pages span.value").text() + " Pages"
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.COMPLETED
}
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return listOf(
SChapter.create().apply {
setUrlWithoutDomain("${response.request.url}/1")
name = "Chapter"
date_upload = try {
dateFormat.parse(document.select(".date .value>time").text())!!.time
} catch (_: ParseException) {
0L
}
},
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.just(
listOf(
SChapter.create().apply {
url = "${manga.url}/1"
name = "Chapter"
},
),
)
}
@ -289,8 +201,6 @@ class Akuma(
pageList.add(Page(i, "$url/$i"))
}
pageList[0].imageUrl = imageUrlParse(document)
return pageList
}
@ -298,8 +208,6 @@ class Akuma(
return document.select(".entry-content img").attr("abs:src")
}
override fun getFilterList(): FilterList = getFilters()
companion object {
const val PREFIX_ID = "id:"
}

View File

@ -1,36 +0,0 @@
package eu.kanade.tachiyomi.extension.all.akuma
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class AkumaFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
Akuma("all", "all"),
Akuma("en", "english"),
Akuma("id", "indonesian"),
Akuma("jv", "javanese"),
Akuma("ca", "catalan"),
Akuma("ceb", "cebuano"),
Akuma("cs", "czech"),
Akuma("da", "danish"),
Akuma("de", "german"),
Akuma("et", "estonian"),
Akuma("es", "spanish"),
Akuma("eo", "esperanto"),
Akuma("fr", "french"),
Akuma("it", "italian"),
Akuma("hi", "hindi"),
Akuma("hu", "hungarian"),
Akuma("nl", "dutch"),
Akuma("pl", "polish"),
Akuma("pt", "portuguese"),
Akuma("vi", "vietnamese"),
Akuma("tr", "turkish"),
Akuma("ru", "russian"),
Akuma("uk", "ukrainian"),
Akuma("ar", "arabic"),
Akuma("ko", "korean"),
Akuma("zh", "chinese"),
Akuma("ja", "japanese"),
)
}

View File

@ -1,49 +0,0 @@
package eu.kanade.tachiyomi.extension.all.akuma
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TextFilter("Female Tags", "female"),
TextFilter("Male Tags", "male"),
TextFilter("Other Tags", "other"),
CategoryFilter(),
TextFilter("Groups", "group"),
TextFilter("Artists", "artist"),
TextFilter("Parody", "parody"),
TextFilter("Characters", "character"),
Filter.Separator(),
Filter.Header("Search in favorites, read, or commented"),
OptionFilter(),
)
}
internal class TextFilter(name: String, val tag: String) : Filter.Text(name)
internal class OptionFilter(val value: List<Pair<String, String>> = options) : Filter.Select<String>("Options", options.map { it.first }.toTypedArray()) {
fun getValue() = options[state].second
}
internal open class TagTriState(name: String) : Filter.TriState(name)
internal class CategoryFilter() :
Filter.Group<Filter.TriState>("Categories", categoryList.map { TagTriState(it) })
private val categoryList = listOf(
"Doujinshi",
"Manga",
"Image Set",
"Artist CG",
"Game CG",
"Western",
"Non-H",
"Cosplay",
"Misc",
)
private val options = listOf(
"None" to "",
"Favorited only" to "favorited",
"Read only" to "read",
"Commented only" to "commented",
)

View File

@ -18,15 +18,6 @@ class AsmHentai(
lang = lang,
) {
override val supportsLatest = mangaLang.isNotBlank()
override val supportSpeechless: Boolean = true
override fun Element.mangaLang() =
select("a:has(.flag)").attr("href")
.removeSuffix("/").substringAfterLast("/")
.let {
// Include Speechless in search results
if (it == LANGUAGE_SPEECHLESS) mangaLang else it
}
override fun Element.mangaUrl() =
selectFirst(".image a")?.attr("abs:href")
@ -34,6 +25,10 @@ class AsmHentai(
override fun Element.mangaThumbnail() =
selectFirst(".image img")?.imgAttr()
override fun Element.mangaLang() =
select("a:has(.flag)").attr("href")
.removeSuffix("/").substringAfterLast("/")
override fun popularMangaSelector() = ".preview_item"
override val favoritePath = "inc/user.php?act=favs"

View File

@ -1,7 +0,0 @@
ext {
extName = 'Galaxy'
extClass = '.GalaxyFactory'
extVersionCode = 2
}
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,19 +0,0 @@
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://ayoub-zrr.xyz", "ar") {
override val id = 2729515745226258240
}
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

@ -1,8 +0,0 @@
ext {
extName = '3Hentai'
extClass = '.Hentai3Factory'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,207 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hentai3
import eu.kanade.tachiyomi.network.GET
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.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class Hentai3(
override val lang: String = "all",
private val searchLang: String = "",
) : HttpSource() {
override val name = "3Hentai"
override val baseUrl = "https://3hentai.net"
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.set("referer", "$baseUrl/")
.set("origin", baseUrl)
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/${if (searchLang.isNotEmpty()) "language/$searchLang/${if (page > 1) page else ""}?" else "search?q=pages%3A>0&pages=$page&"}sort=popular", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val doc = response.asJsoup()
val mangas = doc.select("a[href*=/d/]").map(::popularMangaFromElement)
val hasNextPage = doc.selectFirst("a[rel=next]") != null
return MangasPage(mangas, hasNextPage)
}
private fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.selectFirst("div")!!.ownText()
setUrlWithoutDomain(element.absUrl("href"))
thumbnail_url = element.selectFirst("img:not([class])")!!.absUrl("src")
}
}
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/${if (searchLang.isNotEmpty()) "language/$searchLang/$page" else "search?q=pages%3A>0&pages=$page"}", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val tags = mutableListOf<String>()
var singleTag: Pair<String, String>? = null
var sort = ""
if (searchLang.isNotEmpty()) tags.add("language:$searchLang")
filters.forEach {
when (it) {
is SelectFilter -> sort = it.getValue()
is TextFilter -> {
if (it.state.isNotEmpty()) {
val splitted = it.state.split(",").filter(String::isNotBlank)
if (splitted.size < 2 && it.type != "tags") {
singleTag = it.type to it.state.replace(" ", "-")
} else {
splitted.map { tag ->
val trimmed = tag.trim().lowercase()
tags.add(
buildString {
if (trimmed.startsWith('-')) append("-")
append(it.type, ":'")
append(trimmed.removePrefix("-"), if (it.specific.isNotEmpty()) " (${it.specific})'" else "'")
},
)
}
}
}
}
else -> {}
}
}
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (singleTag != null) {
addPathSegment(singleTag!!.first)
addPathSegment(singleTag!!.second)
if (page > 1) addPathSegment(page.toString())
} else {
addPathSegment("search")
addQueryParameter(
"q",
when {
tags.isNotEmpty() -> tags.joinToString()
query.isNotEmpty() -> query
else -> "page:>0"
},
)
if (page > 1) addQueryParameter("page", page.toString())
}
addQueryParameter("sort", sort)
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
// Details
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
fun String.capitalizeEach() = this.split(" ").joinToString(" ") { s ->
s.replaceFirstChar { sr ->
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
}
}
return SManga.create().apply {
val authors = document.select("a[href*=/groups/]").eachText().joinToString()
val artists = document.select("a[href*=/artists/]").eachText().joinToString()
initialized = true
title = document.select("h1 > span").text()
author = authors.ifEmpty { artists }
artist = artists.ifEmpty { authors }
genre = document.select("a[href*=/tags/]").eachText().joinToString {
val capitalized = it.capitalizeEach()
if (capitalized.contains("male")) {
capitalized.replace("(female)", "").replace("(male)", "")
} else {
"$capitalized"
}
}
description = buildString {
document.select("a[href*=/characters/]").eachText().joinToString().ifEmpty { null }?.let {
append("Characters: ", it.capitalizeEach(), "\n\n")
}
document.select("a[href*=/series/]").eachText().joinToString().ifEmpty { null }?.let {
append("Series: ", it.capitalizeEach(), "\n\n")
}
document.select("a[href*=/groups/]").eachText().joinToString().ifEmpty { null }?.let {
append("Groups: ", it.capitalizeEach(), "\n\n")
}
document.select("a[href*=/language/]").eachText().joinToString().ifEmpty { null }?.let {
append("Languages: ", it.capitalizeEach(), "\n\n")
}
append(document.select("div.tag-container:contains(pages:)").text(), "\n")
}
thumbnail_url = document.selectFirst("img[src*=thumbnail].w-96")?.absUrl("src")
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
}
}
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZZZ", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
// Chapters
override fun chapterListParse(response: Response): List<SChapter> {
val doc = response.asJsoup()
return listOf(
SChapter.create().apply {
name = "Chapter"
setUrlWithoutDomain(response.request.url.toString())
date_upload = try {
dateFormat.parse(doc.select("time").text())!!.time
} catch (_: ParseException) {
0L
}
},
)
}
// Pages
override fun pageListParse(response: Response): List<Page> {
val images = response.asJsoup().select("img:not([class], [src*=thumb], [src*=cover])")
return images.mapIndexed { index, image ->
val imageUrl = image.absUrl("src")
Page(index, imageUrl = imageUrl.replace(Regex("t(?=\\.)"), ""))
}
}
override fun getFilterList() = getFilters()
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
}

View File

@ -1,38 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hentai3
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class Hentai3Factory : SourceFactory {
override fun createSources(): List<Source> = listOf(
Hentai3("all", ""),
Hentai3("en", "english"),
Hentai3("ja", "japanese"),
Hentai3("ko", "korean"),
Hentai3("zh", "chinese"),
Hentai3("mo", "mongolian"),
Hentai3("es", "spanish"),
Hentai3("pt", "Portuguese"),
Hentai3("id", "indonesian"),
Hentai3("jv", "javanese"),
Hentai3("tl", "tagalog"),
Hentai3("vi", "vietnamese"),
Hentai3("th", "thai"),
Hentai3("my", "burmese"),
Hentai3("tr", "turkish"),
Hentai3("ru", "russian"),
Hentai3("uk", "ukrainian"),
Hentai3("po", "polish"),
Hentai3("fi", "finnish"),
Hentai3("de", "german"),
Hentai3("it", "italian"),
Hentai3("fr", "french"),
Hentai3("nl", "dutch"),
Hentai3("cs", "czech"),
Hentai3("hu", "hungarian"),
Hentai3("bg", "bulgarian"),
Hentai3("is", "icelandic"),
Hentai3("la", "latin"),
Hentai3("ar", "arabic"),
)
}

View File

@ -1,39 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hentai3
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
SelectFilter("Sort by", getSortsList),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
Filter.Header("Use 'Male Tags' or 'Female Tags' for specific categories. 'Tags' searches all categories."),
TextFilter("Tags", "tags"),
TextFilter("Male Tags", "tags", "male"),
TextFilter("Female Tags", "tags", "female"),
TextFilter("Series", "series"),
TextFilter("Characters", "characters"),
TextFilter("Artists", "artist"),
TextFilter("Groups", "groups"),
TextFilter("Languages", "language"),
Filter.Separator(),
Filter.Header("Filter by pages, for example: (>20)"),
TextFilter("Pages", "page"),
)
}
internal open class TextFilter(name: String, val type: String, val specific: String = "") : Filter.Text(name)
internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
fun getValue() = vals[state].second
}
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Recent", ""),
Pair("Popular: All Time", "popular"),
Pair("Popular: Week", "popular-7d"),
Pair("Popular: Today", "popular-24h"),
)

View File

@ -1,10 +0,0 @@
ext {
extName = 'HentaiEra'
extClass = '.HentaiEraFactory'
themePkg = 'galleryadults'
baseUrl = 'https://hentaiera.com'
overrideVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,105 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hentaiera
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
import eu.kanade.tachiyomi.multisrc.galleryadults.Genre
import eu.kanade.tachiyomi.multisrc.galleryadults.SearchFlagFilter
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
import eu.kanade.tachiyomi.multisrc.galleryadults.toBinary
import eu.kanade.tachiyomi.network.GET
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
class HentaiEra(
lang: String = "all",
override val mangaLang: String = LANGUAGE_MULTI,
) : GalleryAdults(
"HentaiEra",
"https://hentaiera.com",
lang = lang,
) {
override val supportsLatest = true
override val useIntermediateSearch: Boolean = true
override val supportSpeechless: Boolean = true
override fun Element.mangaTitle(selector: String): String? =
mangaFullTitle(selector.replace("caption", "gallery_title")).let {
if (preferences.shortTitle) it?.shortenTitle() else it
}
override fun Element.mangaLang() =
select("a:has(.g_flag)").attr("href")
.removeSuffix("/").substringAfterLast("/")
.let {
// Include Speechless in search results
if (it == LANGUAGE_SPEECHLESS) mangaLang else it
}
override fun popularMangaRequest(page: Int): Request {
// Only for query string or multiple tags
val url = "$baseUrl/search/".toHttpUrl().newBuilder().apply {
addQueryParameter("pp", "1")
getLanguageURIs().forEach { pair ->
addQueryParameter(
pair.second,
toBinary(mangaLang == pair.first || mangaLang == LANGUAGE_MULTI),
)
}
addPageUri(page)
}
return GET(url.build(), headers)
}
/* Details */
override fun Element.getInfo(tag: String): String {
return select("li:has(.tags_text:contains($tag)) .tag .item_name")
.joinToString {
val name = it.ownText()
if (tag.contains(regexTag)) {
genres[name] = it.parent()!!.attr("href")
.removeSuffix("/").substringAfterLast('/')
}
listOf(
name,
it.select(".split_tag").text()
.trim()
.removePrefix("| "),
)
.filter { s -> s.isNotBlank() }
.joinToString()
}
}
override fun Element.getCover() =
selectFirst(".left_cover img")?.imgAttr()
override fun tagsParser(document: Document): List<Genre> {
return document.select("h2.gallery_title a")
.mapNotNull {
Genre(
it.text(),
it.attr("href")
.removeSuffix("/").substringAfterLast('/'),
)
}
}
override val mangaDetailInfoSelector = ".gallery_first"
/* Pages */
override val thumbnailSelector = ".gthumb"
override val pageUri = "view"
override fun getCategoryURIs() = listOf(
SearchFlagFilter("Manga", "mg"),
SearchFlagFilter("Doujinshi", "dj"),
SearchFlagFilter("Western", "ws"),
SearchFlagFilter("Image Set", "is"),
SearchFlagFilter("Artist CG", "ac"),
SearchFlagFilter("Game CG", "gc"),
)
}

View File

@ -1,19 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hentaiera
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class HentaiEraFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
HentaiEra("en", GalleryAdults.LANGUAGE_ENGLISH),
HentaiEra("ja", GalleryAdults.LANGUAGE_JAPANESE),
HentaiEra("es", GalleryAdults.LANGUAGE_SPANISH),
HentaiEra("fr", GalleryAdults.LANGUAGE_FRENCH),
HentaiEra("ko", GalleryAdults.LANGUAGE_KOREAN),
HentaiEra("de", GalleryAdults.LANGUAGE_GERMAN),
HentaiEra("ru", GalleryAdults.LANGUAGE_RUSSIAN),
HentaiEra("all", GalleryAdults.LANGUAGE_MULTI),
)
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Hitomi'
extClass = '.HitomiFactory'
extVersionCode = 31
extVersionCode = 28
isNsfw = true
}

View File

@ -3,50 +3,79 @@ package eu.kanade.tachiyomi.extension.all.hitomi
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
SelectFilter("Sort by", getSortsList),
TypeFilter("Types"),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TextFilter("Groups", "group"),
TextFilter("Artists", "artist"),
TextFilter("Series", "series"),
TextFilter("Characters", "character"),
TextFilter("Male Tags", "male"),
TextFilter("Female Tags", "female"),
Filter.Header("Please don't put Female/Male tags here, they won't work!"),
TextFilter("Tags", "tag"),
)
typealias OrderType = Pair<String?, String>
typealias ParsedFilter = Pair<String, OrderType>
private fun parseFilter(query: StringBuilder, area: String, filterState: String) {
filterState
.trim()
.split(',')
.filter { it.isNotBlank() }
.forEach {
val trimmed = it.trim()
val negativePrefix = if (trimmed.startsWith("-")) "-" else ""
query.append(" $negativePrefix$area:${trimmed.removePrefix("-").replace(" ", "_")}")
}
}
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
internal open class SelectFilter(name: String, val vals: List<Triple<String, String?, String>>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
fun getArea() = vals[state].second
fun getValue() = vals[state].third
fun parseFilters(filters: FilterList): ParsedFilter {
val query = StringBuilder()
var order: OrderType = Pair("date", "added")
filters.forEach { filter ->
when (filter) {
is SortFilter -> {
order = filter.getOrder
}
is AreaFilter -> {
parseFilter(query, filter.getAreaName, filter.state)
}
else -> { /* Do Nothing */ }
}
}
return Pair(query.toString(), order)
}
internal class TypeFilter(name: String) :
Filter.Group<CheckBoxFilter>(
name,
listOf(
Pair("Anime", "anime"),
Pair("Artist CG", "artistcg"),
Pair("Doujinshi", "doujinshi"),
Pair("Game CG", "gamecg"),
Pair("Image Set", "imageset"),
Pair("Manga", "manga"),
).map { CheckBoxFilter(it.first, it.second, true) },
)
internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state)
private val getSortsList: List<Triple<String, String?, String>> = listOf(
Triple("Date Added", null, "index"),
Triple("Date Published", "date", "published"),
Triple("Popular: Today", "popular", "today"),
Triple("Popular: Week", "popular", "week"),
Triple("Popular: Month", "popular", "month"),
Triple("Popular: Year", "popular", "year"),
Triple("Random", "popular", "year"),
private class OrderFilter(val name: String, val order: OrderType) {
val getFilterName: String
get() = name
val getOrder: OrderType
get() = order
}
private class SortFilter : UriPartFilter(
"Sort By",
arrayOf(
OrderFilter("Date Added", Pair(null, "index")),
OrderFilter("Date Published", Pair("date", "published")),
OrderFilter("Popular: Today", Pair("popular", "today")),
OrderFilter("Popular: Week", Pair("popular", "week")),
OrderFilter("Popular: Month", Pair("popular", "month")),
OrderFilter("Popular: Year", Pair("popular", "year")),
),
)
private open class UriPartFilter(displayName: String, val vals: Array<OrderFilter>) :
Filter.Select<String>(displayName, vals.map { it.getFilterName }.toTypedArray()) {
val getOrder: OrderType
get() = vals[state].getOrder
}
private class AreaFilter(displayName: String, val areaName: String) :
Filter.Text(displayName) {
val getAreaName: String
get() = areaName
}
fun getFilterListInternal(): FilterList = FilterList(
SortFilter(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
AreaFilter("Artist(s)", "artist"),
AreaFilter("Character(s)", "character"),
AreaFilter("Group(s)", "group"),
AreaFilter("Series", "series"),
AreaFilter("Female Tag(s)", "female"),
AreaFilter("Male Tag(s)", "male"),
Filter.Header("Don't put Female/Male tags here, they won't work!"),
AreaFilter("Tag(s)", "tag"),
)

View File

@ -1,7 +1,12 @@
package eu.kanade.tachiyomi.extension.all.hitomi
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
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
@ -17,16 +22,16 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.LinkedList
import java.util.Locale
@ -36,7 +41,7 @@ import kotlin.math.min
class Hitomi(
override val lang: String,
private val nozomiLang: String,
) : HttpSource() {
) : ConfigurableSource, HttpSource() {
override val name = "Hitomi"
@ -52,13 +57,19 @@ class Hitomi(
override val client = network.cloudflareClient
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private var iconified = preferences.getBoolean(PREF_TAG_GENDER_ICON, false)
override fun headersBuilder() = super.headersBuilder()
.set("referer", "$baseUrl/")
.set("origin", baseUrl)
override fun fetchPopularManga(page: Int): Observable<MangasPage> = Observable.fromCallable {
runBlocking {
val entries = getGalleryIDsFromNozomi("popular", "year", nozomiLang, page.nextPageRange())
val entries = getGalleryIDsFromNozomi("popular", "today", nozomiLang, page.nextPageRange())
.toMangaList()
MangasPage(entries, entries.size >= 24)
@ -77,23 +88,26 @@ class Hitomi(
private lateinit var searchResponse: List<Int>
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.fromCallable {
val parsedFilter = parseFilters(filters)
runBlocking {
if (page == 1) {
searchResponse = hitomiSearch(
query.trim(),
filters,
"$query${parsedFilter.first}".trim(),
parsedFilter.second,
nozomiLang,
)
).toList()
}
val end = min(page * 25, searchResponse.size)
val entries = searchResponse.subList((page - 1) * 25, end)
.toMangaList()
MangasPage(entries, end < searchResponse.size)
MangasPage(entries, end != searchResponse.size)
}
}
override fun getFilterList() = getFilters()
override fun getFilterList(): FilterList = getFilterListInternal()
private fun Int.nextPageRange(): LongRange {
val byteOffset = ((this - 1) * 25) * 4L
@ -101,73 +115,27 @@ class Hitomi(
}
private suspend fun getRangedResponse(url: String, range: LongRange?): ByteArray {
val request = when (range) {
null -> GET(url, headers)
else -> {
val rangeHeaders = headersBuilder()
.set("Range", "bytes=${range.first}-${range.last}")
.build()
GET(url, rangeHeaders, CacheControl.FORCE_NETWORK)
}
val rangeHeaders = when (range) {
null -> headers
else -> headersBuilder()
.set("Range", "bytes=${range.first}-${range.last}")
.build()
}
return client.newCall(request).awaitSuccess().use { it.body.bytes() }
return client.newCall(GET(url, rangeHeaders)).awaitSuccess().use { it.body.bytes() }
}
private suspend fun hitomiSearch(
query: String,
filters: FilterList,
order: OrderType,
language: String = "all",
): List<Int> =
): Set<Int> =
coroutineScope {
var sortBy: Pair<String?, String> = Pair(null, "index")
var random = false
val terms = query
.trim()
.replace(Regex("""^\?"""), "")
.lowercase()
.split(Regex("\\s+"))
.toMutableList()
filters.forEach {
when (it) {
is SelectFilter -> {
sortBy = Pair(it.getArea(), it.getValue())
random = (it.vals[it.state].first == "Random")
}
is TypeFilter -> {
val (activeFilter, inactiveFilters) = it.state.partition { stIt -> stIt.state }
terms += when {
inactiveFilters.size < 5 -> inactiveFilters.map { fil -> "-type:${fil.value}" }
inactiveFilters.size == 5 -> listOf("type:${activeFilter[0].value}")
else -> listOf("type: none")
}
}
is TextFilter -> {
if (it.state.isNotEmpty()) {
terms += it.state.split(",").filter(String::isNotBlank).map { tag ->
val trimmed = tag.trim()
buildString {
if (trimmed.startsWith('-')) {
append("-")
}
append(it.type)
append(":")
append(trimmed.lowercase().removePrefix("-"))
}
}
}
}
else -> {}
}
}
if (language != "all" && sortBy == Pair(null, "index") && !terms.any { it.contains(":") }) {
terms += "language:$language"
}
val positiveTerms = LinkedList<String>()
val negativeTerms = LinkedList<String>()
@ -182,35 +150,22 @@ class Hitomi(
val positiveResults = positiveTerms.map {
async {
try {
getGalleryIDsForQuery(it, language)
} catch (e: IllegalArgumentException) {
if (e.message?.equals("HTTP error 404") == true) {
throw Exception("Unknown query: \"$it\"")
} else {
throw e
}
}
runCatching {
getGalleryIDsForQuery(it, language, order)
}.getOrDefault(emptySet())
}
}
val negativeResults = negativeTerms.map {
async {
try {
getGalleryIDsForQuery(it, language)
} catch (e: IllegalArgumentException) {
if (e.message?.equals("HTTP error 404") == true) {
throw Exception("Unknown query: \"$it\"")
} else {
throw e
}
}
runCatching {
getGalleryIDsForQuery(it, language, order)
}.getOrDefault(emptySet())
}
}
val results = when {
positiveTerms.isEmpty() || sortBy != Pair(null, "index")
-> getGalleryIDsFromNozomi(sortBy.first, sortBy.second, language)
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(order.first, order.second, language)
else -> emptySet()
}.toMutableSet()
@ -235,17 +190,14 @@ class Hitomi(
filterNegative(it.await())
}
if (random) {
results.toList().shuffled()
} else {
results.toList()
}
results
}
// search.js
private suspend fun getGalleryIDsForQuery(
query: String,
language: String = "all",
order: OrderType,
): Set<Int> {
query.replace("_", " ").let {
if (it.indexOf(':') > -1) {
@ -268,6 +220,20 @@ class Hitomi(
}
}
if (area != null) {
if (order.first != null) {
area = "$area/${order.first}"
if (tag.isBlank()) {
tag = order.second
} else {
area = "$area/${order.second}"
}
}
} else {
area = order.first
tag = order.second
}
return getGalleryIDsFromNozomi(area, tag, lang)
}
@ -469,18 +435,12 @@ class Hitomi(
private suspend fun Collection<Int>.toMangaList() = coroutineScope {
map { id ->
async {
try {
runCatching {
client.newCall(GET("$ltnUrl/galleries/$id.js", headers))
.awaitSuccess()
.parseScriptAs<Gallery>()
.toSManga()
} catch (e: IllegalArgumentException) {
if (e.message?.equals("HTTP error 404") == true) {
return@async null
} else {
throw e
}
}
}.getOrNull()
}
}.awaitAll().filterNotNull()
}
@ -490,7 +450,7 @@ class Hitomi(
url = galleryurl
author = groups?.joinToString { it.formatted }
artist = artists?.joinToString { it.formatted }
genre = tags?.joinToString { it.formatted }
genre = tags?.joinToString { it.getFormatted(iconified) }
thumbnail_url = files.first().let {
val hash = it.hash
val imageId = imageIdFromHash(hash)
@ -499,15 +459,14 @@ class Hitomi(
"https://${subDomain}tn.$domain/webpbigtn/${thumbPathFromHash(hash)}/$hash.webp"
}
description = buildString {
parodys?.joinToString { it.formatted }?.let {
append("Series: ", it, "\n")
}
characters?.joinToString { it.formatted }?.let {
append("Characters: ", it, "\n")
}
append("Type: ", type, "\n")
parodys?.joinToString { it.formatted }?.let {
append("Parodies: ", it, "\n")
}
append("Pages: ", files.size, "\n")
language?.let { append("Language: ", language) }
append("Language: ", language)
}
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
@ -528,21 +487,26 @@ class Hitomi(
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListRequest(manga: SManga): Request {
val id = manga.url
.substringAfterLast("-")
.substringBefore(".")
return GET("$ltnUrl/galleries/$id.js#${manga.url}", headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val gallery = response.parseScriptAs<Gallery>()
val mangaUrl = response.request.url.fragment!!
return listOf(
SChapter.create().apply {
name = "Chapter"
url = gallery.galleryurl
url = mangaUrl
scanlator = gallery.type
date_upload = try {
date_upload = runCatching {
dateFormat.parse(gallery.date.substringBeforeLast("-"))!!.time
} catch (_: ParseException) {
0L
}
}.getOrDefault(0L)
},
)
}
@ -561,9 +525,6 @@ class Hitomi(
override fun pageListParse(response: Response) = runBlocking {
val gallery = response.parseScriptAs<Gallery>()
val id = gallery.galleryurl
.substringAfterLast("-")
.substringBefore(".")
gallery.files.mapIndexed { idx, img ->
val hash = img.hash
@ -659,9 +620,28 @@ class Hitomi(
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = PREF_TAG_GENDER_ICON
title = "Show gender as text or icon in tags (requires refresh)"
summaryOff = "Show gender as text"
summaryOn = "Show gender as icon"
setOnPreferenceChangeListener { _, newValue ->
iconified = newValue == true
true
}
}.also(screen::addPreference)
}
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
companion object {
private const val PREF_TAG_GENDER_ICON = "pref_tag_gender_icon"
}
}

View File

@ -4,12 +4,12 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
@Serializable
class Gallery(
data class Gallery(
val galleryurl: String,
val title: String,
val date: String,
val type: String?,
val language: String?,
val type: String,
val language: String,
val tags: List<Tag>?,
val artists: List<Artist>?,
val groups: List<Group>?,
@ -19,49 +19,49 @@ class Gallery(
)
@Serializable
class ImageFile(
data class ImageFile(
val hash: String,
)
@Serializable
class Tag(
private val female: JsonPrimitive?,
private val male: JsonPrimitive?,
private val tag: String,
data class Tag(
val female: JsonPrimitive?,
val male: JsonPrimitive?,
val tag: String,
) {
val formatted get() = if (female?.content == "1") {
tag.toCamelCase() + ""
fun getFormatted(iconified: Boolean) = if (female?.content == "1") {
tag.toCamelCase() + if (iconified) "" else " (Female)"
} else if (male?.content == "1") {
tag.toCamelCase() + ""
tag.toCamelCase() + if (iconified) "" else " (Male)"
} else {
tag.toCamelCase()
}
}
@Serializable
class Artist(
private val artist: String,
data class Artist(
val artist: String,
) {
val formatted get() = artist.toCamelCase()
}
@Serializable
class Group(
private val group: String,
data class Group(
val group: String,
) {
val formatted get() = group.toCamelCase()
}
@Serializable
class Character(
private val character: String,
data class Character(
val character: String,
) {
val formatted get() = character.toCamelCase()
}
@Serializable
class Parody(
private val parody: String,
data class Parody(
val parody: String,
) {
val formatted get() = parody.toCamelCase()
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.MangaFireFactory'
themePkg = 'mangareader'
baseUrl = 'https://mangafire.to'
overrideVersionCode = 5
overrideVersionCode = 3
isNsfw = true
}

View File

@ -138,13 +138,13 @@ open class MangaFire(
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
val result = json.decodeFromString<ResponseDto<String>>(response.body.string()).result
val document = Jsoup.parse(result)
val selector = if (isVolume) "div.unit" else "ul li"
val elements = document.select(selector)
val elements = document.select("ul li")
if (elements.size > 0) {
val linkToFirstChapter = elements[0].selectFirst(Evaluator.Tag("a"))!!.attr("href")
val mangaId = linkToFirstChapter.toString().substringAfter('.').substringBefore('/')
val type = if (isVolume) volumeType else chapterType
val request = GET("$baseUrl/ajax/read/$mangaId/$type/$langCode", headers)
val request = GET("$baseUrl/ajax/read/$mangaId/chapter/$langCode", headers)
val response = client.newCall(request).execute()
val res = json.decodeFromString<ResponseDto<ChapterIdsDto>>(response.body.string()).result.html
val chapterInfoDocument = Jsoup.parse(res)
@ -177,7 +177,6 @@ open class MangaFire(
val element = elements[i]
val number = element.attr("data-number").toFloatOrNull() ?: -1f
if (chapter.chapter_number != number) throw Exception("Chapter number doesn't match. Try updating again.")
chapter.name = element.select(Evaluator.Tag("span"))[0].ownText()
val date = element.select(Evaluator.Tag("span"))[1].ownText()
chapter.date_upload = try {
dateFormat.parse(date)!!.time

View File

@ -1,7 +1,7 @@
ext {
extName = 'MangaPark'
extClass = '.MangaParkFactory'
extVersionCode = 20
extVersionCode = 19
isNsfw = true
}

View File

@ -4,7 +4,6 @@ import android.app.Application
import android.widget.Toast
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.cookieinterceptor.CookieInterceptor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
@ -18,14 +17,10 @@ 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 kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
@ -33,8 +28,6 @@ import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
class MangaPark(
override val lang: String,
@ -60,7 +53,6 @@ class MangaPark(
private val json: Json by injectLazy()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::siteSettingsInterceptor)
.addNetworkInterceptor(CookieInterceptor(domain, "nsfw" to "2"))
.rateLimitHost(apiUrl.toHttpUrl(), 1)
.build()
@ -98,6 +90,8 @@ class MangaPark(
}
override fun searchMangaParse(response: Response): MangasPage {
runCatching(::getGenres)
val result = response.parseAs<SearchResponse>()
val entries = result.data.searchComics.items.map { it.data.toSManga() }
@ -132,10 +126,6 @@ class MangaPark(
}
override fun getFilterList(): FilterList {
CoroutineScope(Dispatchers.IO).launch {
runCatching(::getGenres)
}
val filters = mutableListOf<Filter<*>>(
SortFilter(),
OriginalLanguageFilter(),
@ -185,13 +175,7 @@ class MangaPark(
override fun chapterListParse(response: Response): List<SChapter> {
val result = response.parseAs<ChapterListResponse>()
return if (preference.getBoolean(DUPLICATE_CHAPTER_PREF_KEY, false)) {
result.data.chapterList.flatMap {
it.data.dupChapters.map { it.data.toSChapter() }
}.reversed()
} else {
result.data.chapterList.map { it.data.toSChapter() }.reversed()
}
return result.data.chapterList.map { it.data.toSChapter() }.reversed()
}
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast("#")
@ -227,13 +211,6 @@ class MangaPark(
true
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = DUPLICATE_CHAPTER_PREF_KEY
title = "Fetch Duplicate Chapters"
summary = "Refresh chapter list to apply changes"
setDefaultValue(false)
}.also(screen::addPreference)
}
private inline fun <reified T> Response.parseAs(): T =
@ -245,35 +222,6 @@ class MangaPark(
private inline fun <reified T : Any> T.toJsonRequestBody() =
json.encodeToString(this).toRequestBody(JSON_MEDIA_TYPE)
private val cookiesNotSet = AtomicBoolean(true)
private val latch = CountDownLatch(1)
// sets necessary cookies to not block genres like `Hentai`
private fun siteSettingsInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val settingsUrl = "$baseUrl/aok/settings-save"
if (
request.url.toString() != settingsUrl &&
request.url.host == domain
) {
if (cookiesNotSet.getAndSet(false)) {
val payload =
"""{"data":{"general_autoLangs":[],"general_userLangs":[],"general_excGenres":[],"general_prefLangs":[]}}"""
.toRequestBody(JSON_MEDIA_TYPE)
client.newCall(POST(settingsUrl, headers, payload)).execute().close()
latch.countDown()
} else {
latch.await()
}
}
return chain.proceed(request)
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
@ -300,7 +248,5 @@ class MangaPark(
"parkmanga.org",
"mpark.to",
)
private const val DUPLICATE_CHAPTER_PREF_KEY = "pref_dup_chapters"
}
}

View File

@ -12,34 +12,34 @@ typealias ChapterListResponse = Data<ChapterList>
typealias PageListResponse = Data<ChapterPages>
@Serializable
class Data<T>(val data: T)
data class Data<T>(val data: T)
@Serializable
class Items<T>(val items: List<T>)
data class Items<T>(val items: List<T>)
@Serializable
class SearchComics(
data class SearchComics(
@SerialName("get_searchComic") val searchComics: Items<Data<MangaParkComic>>,
)
@Serializable
class ComicNode(
data class ComicNode(
@SerialName("get_comicNode") val comic: Data<MangaParkComic>,
)
@Serializable
class MangaParkComic(
private val id: String,
private val name: String,
private val altNames: List<String>? = null,
private val authors: List<String>? = null,
private val artists: List<String>? = null,
private val genres: List<String>? = null,
private val originalStatus: String? = null,
private val uploadStatus: String? = null,
private val summary: String? = null,
@SerialName("urlCoverOri") private val cover: String? = null,
private val urlPath: String,
data class MangaParkComic(
val id: String,
val name: String,
val altNames: List<String>? = null,
val authors: List<String>? = null,
val artists: List<String>? = null,
val genres: List<String>? = null,
val originalStatus: String? = null,
val uploadStatus: String? = null,
val summary: String? = null,
@SerialName("urlCoverOri") val cover: String? = null,
val urlPath: String,
) {
fun toSManga() = SManga.create().apply {
url = "$urlPath#$id"
@ -100,21 +100,18 @@ class MangaParkComic(
}
@Serializable
class ChapterList(
data class ChapterList(
@SerialName("get_comicChapterList") val chapterList: List<Data<MangaParkChapter>>,
)
@Serializable
class MangaParkChapter(
private val id: String,
@SerialName("dname") private val displayName: String,
private val title: String? = null,
private val dateCreate: Long? = null,
private val dateModify: Long? = null,
private val urlPath: String,
private val srcTitle: String? = null,
private val userNode: Data<Name>? = null,
val dupChapters: List<Data<MangaParkChapter>> = emptyList(),
data class MangaParkChapter(
val id: String,
@SerialName("dname") val displayName: String,
val title: String? = null,
val dateCreate: Long? = null,
val dateModify: Long? = null,
val urlPath: String,
) {
fun toSChapter() = SChapter.create().apply {
url = "$urlPath#$id"
@ -123,24 +120,20 @@ class MangaParkChapter(
title?.let { append(": ", it) }
}
date_upload = dateModify ?: dateCreate ?: 0L
scanlator = userNode?.data?.name ?: srcTitle ?: "Unknown"
}
}
@Serializable
class Name(val name: String)
@Serializable
class ChapterPages(
data class ChapterPages(
@SerialName("get_chapterNode") val chapterPages: Data<ImageFiles>,
)
@Serializable
class ImageFiles(
data class ImageFiles(
val imageFile: UrlList,
)
@Serializable
class UrlList(
data class UrlList(
val urlList: List<String>,
)

View File

@ -4,28 +4,28 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class GraphQL<T>(
private val variables: T,
private val query: String,
data class GraphQL<T>(
val variables: T,
val query: String,
)
@Serializable
class SearchVariables(private val select: SearchPayload)
data class SearchVariables(val select: SearchPayload)
@Serializable
class SearchPayload(
@SerialName("word") private val query: String? = null,
private val incGenres: List<String>? = null,
private val excGenres: List<String>? = null,
private val incTLangs: List<String>? = null,
private val incOLangs: List<String>? = null,
private val sortby: String? = null,
private val chapCount: String? = null,
private val origStatus: String? = null,
private val siteStatus: String? = null,
private val page: Int,
private val size: Int,
data class SearchPayload(
@SerialName("word") val query: String? = null,
val incGenres: List<String>? = null,
val excGenres: List<String>? = null,
val incTLangs: List<String>? = null,
val incOLangs: List<String>? = null,
val sortby: String? = null,
val chapCount: String? = null,
val origStatus: String? = null,
val siteStatus: String? = null,
val page: Int,
val size: Int,
)
@Serializable
class IdVariables(private val id: String)
data class IdVariables(val id: String)

View File

@ -75,28 +75,6 @@ val CHAPTERS_QUERY = buildQuery {
dateModify
dateCreate
urlPath
srcTitle
userNode {
data {
name
}
}
dupChapters {
data {
id
dname
title
dateModify
dateCreate
urlPath
srcTitle
userNode {
data {
name
}
}
}
}
}
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Meitua.top'
extClass = '.MeituaTop'
extVersionCode = 6
extVersionCode = 5
isNsfw = true
}

View File

@ -23,7 +23,7 @@ class MeituaTop : HttpSource() {
override val lang = "all"
override val supportsLatest = false
override val baseUrl = "https://mt1.meitu1.sbs"
override val baseUrl = "https://meitu1.xyz"
override fun popularMangaRequest(page: Int) = GET("$baseUrl/arttype/0b-$page.html", headers)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,253 +0,0 @@
package eu.kanade.tachiyomi.extension.all.pandachaika
import eu.kanade.tachiyomi.network.GET
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.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.lang.String.CASE_INSENSITIVE_ORDER
import java.math.BigInteger
class PandaChaika(
override val lang: String = "all",
private val searchLang: String = "",
) : HttpSource() {
override val name = "PandaChaika"
override val baseUrl = "https://panda.chaika.moe"
private val baseSearchUrl = "$baseUrl/search"
override val supportsLatest = true
override val client = network.cloudflareClient
.newBuilder()
.addInterceptor(::Intercept)
.build()
private val json: Json by injectLazy()
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseSearchUrl/?tags=$searchLang&sort=rating&apply=&json=&page=$page", headers)
}
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseSearchUrl/?tags=$searchLang&sort=public_date&apply=&json=&page=$page", headers)
}
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
if (num < 0) return minPages to maxPages
return when (query.firstOrNull()) {
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
'=' -> when (query[1]) {
'>' -> limitedNum() to maxPages
'<' -> 1 to limitedNum(maxPages)
else -> limitedNum() to limitedNum()
}
else -> limitedNum() to limitedNum()
}
}
override fun searchMangaParse(response: Response): MangasPage {
val library = response.parseAs<ArchiveResponse>()
val mangas = library.archives.map(LongArchive::toSManga)
val hasNextPage = library.has_next
return MangasPage(mangas, hasNextPage)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseSearchUrl.toHttpUrl().newBuilder().apply {
val tags = mutableListOf<String>()
var reason = ""
var uploader = ""
var pagesMin = 1
var pagesMax = 9999
tags.add(searchLang)
filters.forEach {
when (it) {
is SortFilter -> {
addQueryParameter("sort", it.getValue())
addQueryParameter("asc_desc", if (it.state!!.ascending) "asc" else "desc")
}
is SelectFilter -> {
addQueryParameter("category", it.vals[it.state].replace("All", ""))
}
is PageFilter -> {
if (it.state.isNotBlank()) {
val (min, max) = parsePageRange(it.state)
pagesMin = min
pagesMax = max
}
}
is TextFilter -> {
if (it.state.isNotEmpty()) {
when (it.type) {
"reason" -> reason = it.state
"uploader" -> uploader = it.state
else -> {
it.state.split(",").filter(String::isNotBlank).map { tag ->
val trimmed = tag.trim()
tags.add(
buildString {
if (trimmed.startsWith('-')) append("-")
append(it.type)
if (it.type.isNotBlank()) append(":")
append(trimmed.lowercase().removePrefix("-"))
},
)
}
}
}
}
}
else -> {}
}
}
addQueryParameter("title", query)
addQueryParameter("tags", tags.joinToString())
addQueryParameter("filecount_from", pagesMin.toString())
addQueryParameter("filecount_to", pagesMax.toString())
addQueryParameter("reason", reason)
addQueryParameter("uploader", uploader)
addQueryParameter("page", page.toString())
addQueryParameter("apply", "")
addQueryParameter("json", "")
}.build()
return GET(url, headers)
}
override fun chapterListRequest(manga: SManga): Request {
return GET("$baseUrl/api?archive=${manga.url}", headers)
}
override fun getFilterList() = getFilters()
// Details
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.just(manga.apply { initialized = true })
}
// Chapters
override fun chapterListParse(response: Response): List<SChapter> {
val archive = response.parseAs<Archive>()
return listOf(
SChapter.create().apply {
name = "Chapter"
url = archive.download.substringBefore("/download/")
date_upload = archive.posted * 1000
},
)
}
override fun getMangaUrl(manga: SManga) = "$baseUrl/archive/${manga.url}"
override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}"
// Pages
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
fun List<String>.sort() = this.sortedWith(compareBy(CASE_INSENSITIVE_ORDER) { it })
val url = "$baseUrl${chapter.url}/download/"
val (fileType, contentLength) = getZipType(url)
val remoteZip = ZipHandler(url, client, headers, fileType, contentLength).populate()
val fileListing = remoteZip.files().sort()
val files = remoteZip.toJson()
return Observable.just(
fileListing.mapIndexed { index, filename ->
Page(index, imageUrl = "https://127.0.0.1/#$filename&$files")
},
)
}
private fun getZipType(url: String): Pair<String, BigInteger> {
val request = Request.Builder()
.url(url)
.headers(headers)
.method("HEAD", null)
.build()
val contentLength = (
client.newCall(request).execute().header("content-length")
?: throw Exception("Could not get Content-Length of URL")
)
.toBigInteger()
return (if (contentLength > Int.MAX_VALUE.toBigInteger()) "zip64" else "zip") to contentLength
}
private fun Intercept(chain: Interceptor.Chain): Response {
val url = chain.request().url.toString()
return if (url.startsWith("https://127.0.0.1/#")) {
val fragment = url.toHttpUrl().fragment!!
val remoteZip = fragment.substringAfter("&").parseAs<Zip>()
val filename = fragment.substringBefore("&")
val byteArray = remoteZip.fetch(filename, client)
var type = filename.substringAfterLast('.').lowercase()
type = if (type == "jpg") "jpeg" else type
Response.Builder().body(byteArray.toResponseBody("image/$type".toMediaType()))
.request(chain.request())
.protocol(Protocol.HTTP_1_0)
.code(200)
.message("")
.build()
} else {
chain.proceed(chain.request())
}
}
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private inline fun <reified T> String.parseAs(): T {
return json.decodeFromString(this)
}
private fun Zip.toJson(): String {
return json.encodeToString(this)
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException()
override fun mangaDetailsParse(response: Response): SManga = throw UnsupportedOperationException()
}

View File

@ -1,102 +0,0 @@
package eu.kanade.tachiyomi.extension.all.pandachaika
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
val dateReformat = SimpleDateFormat("EEEE, d MMM yyyy HH:mm (z)", Locale.ENGLISH)
fun filterTags(include: String = "", exclude: List<String> = emptyList(), tags: List<String>): String {
return tags.filter { it.startsWith("$include:") && exclude.none { substring -> it.startsWith("$substring:") } }
.joinToString {
it.substringAfter(":").replace("_", " ").split(" ").joinToString(" ") { s ->
s.replaceFirstChar { sr ->
if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString()
}
}
}
}
fun getReadableSize(bytes: Double): String {
return when {
bytes >= 300 * 1024 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0 * 1024.0))} GB"
bytes >= 100 * 1024 -> "${"%.2f".format(bytes / (1024.0 * 1024.0))} MB"
bytes >= 1024 -> "${"%.2f".format(bytes / (1024.0))} KB"
else -> "$bytes B"
}
}
@Serializable
class Archive(
val download: String,
val posted: Long,
)
@Serializable
class LongArchive(
private val thumbnail: String,
private val title: String,
private val id: Int,
private val posted: Long?,
private val public_date: Long?,
private val filecount: Int,
private val filesize: Double,
private val tags: List<String>,
private val title_jpn: String?,
private val uploader: String,
) {
fun toSManga() = SManga.create().apply {
val groups = filterTags("group", tags = tags)
val artists = filterTags("artist", tags = tags)
val publishers = filterTags("publisher", tags = tags)
val male = filterTags("male", tags = tags)
val female = filterTags("female", tags = tags)
val others = filterTags(exclude = listOf("female", "male", "artist", "publisher", "group", "parody"), tags = tags)
val parodies = filterTags("parody", tags = tags)
url = id.toString()
title = this@LongArchive.title
thumbnail_url = thumbnail
author = groups.ifEmpty { artists }
artist = artists
genre = listOf(male, female, others).joinToString()
description = buildString {
append("Uploader: ", uploader.ifEmpty { "Anonymous" }, "\n")
publishers.takeIf { it.isNotBlank() }?.let {
append("Publishers: ", it, "\n\n")
}
parodies.takeIf { it.isNotBlank() }?.let {
append("Parodies: ", it, "\n\n")
}
male.takeIf { it.isNotBlank() }?.let {
append("Male tags: ", it, "\n\n")
}
female.takeIf { it.isNotBlank() }?.let {
append("Female tags: ", it, "\n\n")
}
others.takeIf { it.isNotBlank() }?.let {
append("Other tags: ", it, "\n\n")
}
title_jpn?.let { append("Japanese Title: ", it, "\n") }
append("Pages: ", filecount, "\n")
append("File Size: ", getReadableSize(filesize), "\n")
try {
append("Public Date: ", dateReformat.format(Date(public_date!! * 1000)), "\n")
} catch (_: Exception) {}
try {
append("Posted: ", dateReformat.format(Date(posted!! * 1000)), "\n")
} catch (_: Exception) {}
}
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
initialized = true
}
}
@Serializable
class ArchiveResponse(
val archives: List<LongArchive>,
val has_next: Boolean,
)

View File

@ -1,29 +0,0 @@
package eu.kanade.tachiyomi.extension.all.pandachaika
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class PandaChaikaFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
PandaChaika(),
PandaChaika("en", "english"),
PandaChaika("zh", "chinese"),
PandaChaika("ko", "korean"),
PandaChaika("es", "spanish"),
PandaChaika("ru", "russian"),
PandaChaika("pt", "portuguese"),
PandaChaika("fr", "french"),
PandaChaika("th", "thai"),
PandaChaika("vi", "vietnamese"),
PandaChaika("ja", "japanese"),
PandaChaika("id", "indonesian"),
PandaChaika("ar", "arabic"),
PandaChaika("uk", "ukrainian"),
PandaChaika("tr", "turkish"),
PandaChaika("cs", "czech"),
PandaChaika("tl", "tagalog"),
PandaChaika("fi", "finnish"),
PandaChaika("jv", "javanese"),
PandaChaika("el", "greek"),
)
}

View File

@ -1,62 +0,0 @@
package eu.kanade.tachiyomi.extension.all.pandachaika
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.Filter.Sort.Selection
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
SortFilter("Sort by", Selection(0, false), getSortsList),
SelectFilter("Types", getTypes),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
Filter.Header("Use 'Male Tags' or 'Female Tags' for specific categories. 'Tags' searches all categories."),
TextFilter("Tags", ""),
TextFilter("Male Tags", "male"),
TextFilter("Female Tags", "female"),
TextFilter("Artists", "artist"),
TextFilter("Parodies", "parody"),
Filter.Separator(),
TextFilter("Reason", "reason"),
TextFilter("Uploader", "reason"),
Filter.Separator(),
Filter.Header("Filter by pages, for example: (>20)"),
PageFilter("Pages"),
)
}
internal open class PageFilter(name: String) : Filter.Text(name)
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
internal open class SelectFilter(name: String, val vals: List<String>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it }.toTypedArray(), state)
internal open class SortFilter(name: String, selection: Selection, private val vals: List<Pair<String, String>>) :
Filter.Sort(name, vals.map { it.first }.toTypedArray(), selection) {
fun getValue() = vals[state!!.index].second
}
private val getTypes = listOf(
"All",
"Doujinshi",
"Manga",
"Image Set",
"Artist CG",
"Game CG",
"Western",
"Non-H",
"Misc",
)
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Public Date", "public_date"),
Pair("Posted Date", "posted_date"),
Pair("Title", "title"),
Pair("Japanese Title", "title_jpn"),
Pair("Rating", "rating"),
Pair("Images", "images"),
Pair("File Size", "size"),
Pair("Category", "category"),
)

View File

@ -1,287 +0,0 @@
package eu.kanade.tachiyomi.extension.all.pandachaika
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.inflateRaw
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseAllCDs
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseEOCD
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseEOCD64
import eu.kanade.tachiyomi.extension.all.pandachaika.ZipParser.parseLocalFile
import eu.kanade.tachiyomi.network.GET
import kotlinx.serialization.Serializable
import okhttp3.Headers
import okhttp3.OkHttpClient
import java.io.ByteArrayOutputStream
import java.math.BigInteger
import java.nio.ByteBuffer
import java.nio.ByteOrder.LITTLE_ENDIAN
import java.util.zip.Inflater
import kotlin.text.Charsets.UTF_8
const val CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE = 0x02014b50
const val END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50
const val END_OF_CENTRAL_DIRECTORY_64_SIGNATURE = 0x06064b50
const val LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50
class EndOfCentralDirectory(
val centralDirectoryByteSize: BigInteger,
val centralDirectoryByteOffset: BigInteger,
)
@Serializable
class CentralDirectoryRecord(
val length: Int,
val compressedSize: Int,
val localFileHeaderRelativeOffset: Int,
val filename: String,
)
class LocalFileHeader(
val compressedData: ByteArray,
val compressionMethod: Int,
)
@Serializable
class Zip(
private val url: String,
private val centralDirectoryRecords: List<CentralDirectoryRecord>,
) {
fun files(): List<String> {
return centralDirectoryRecords.map {
it.filename
}
}
fun fetch(path: String, client: OkHttpClient): ByteArray {
val file = centralDirectoryRecords.find { it.filename == path }
?: throw Exception("File not found in ZIP: $path")
val MAX_LOCAL_FILE_HEADER_SIZE = 256 + 32 + 30 + 100
val headersBuilder = Headers.Builder()
.set(
"Range",
"bytes=${file.localFileHeaderRelativeOffset}-${
file.localFileHeaderRelativeOffset +
file.compressedSize +
MAX_LOCAL_FILE_HEADER_SIZE
}",
).build()
val request = GET(url, headersBuilder)
val response = client.newCall(request).execute()
val byteArray = response.body.byteStream().use { it.readBytes() }
val localFile = parseLocalFile(byteArray, file.compressedSize)
?: throw Exception("Failed to parse local file header in ZIP")
return if (localFile.compressionMethod == 0) {
localFile.compressedData
} else {
inflateRaw(localFile.compressedData)
}
}
}
class ZipHandler(
private val url: String,
private val client: OkHttpClient,
private val additionalHeaders: Headers = Headers.Builder().build(),
private val zipType: String = "zip",
private val contentLength: BigInteger,
) {
fun populate(): Zip {
val endOfCentralDirectory = fetchEndOfCentralDirectory(contentLength, zipType)
val centralDirectoryRecords = fetchCentralDirectoryRecords(endOfCentralDirectory)
return Zip(
url,
centralDirectoryRecords,
)
}
private fun fetchEndOfCentralDirectory(zipByteLength: BigInteger, zipType: String): EndOfCentralDirectory {
val EOCD_MAX_BYTES = 128.toBigInteger()
val eocdInitialOffset = maxOf(0.toBigInteger(), zipByteLength - EOCD_MAX_BYTES)
val headers = additionalHeaders
.newBuilder()
.set("Range", "bytes=$eocdInitialOffset-$zipByteLength")
.build()
val request = GET(url, headers)
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
throw Exception("Could not fetch ZIP: HTTP status ${response.code}")
}
val eocdBuffer = response.body.byteStream().use { it.readBytes() }
if (eocdBuffer.isEmpty()) throw Exception("Could not get Range request to start looking for EOCD")
val eocd =
(if (zipType == "zip64") parseEOCD64(eocdBuffer) else parseEOCD(eocdBuffer))
?: throw Exception("Could not get EOCD record of the ZIP")
return eocd
}
private fun fetchCentralDirectoryRecords(endOfCentralDirectory: EndOfCentralDirectory): List<CentralDirectoryRecord> {
val headersBuilder = Headers.Builder()
.set(
"Range",
"bytes=${endOfCentralDirectory.centralDirectoryByteOffset}-${
endOfCentralDirectory.centralDirectoryByteOffset +
endOfCentralDirectory.centralDirectoryByteSize
}",
).build()
val request = GET(url, headersBuilder)
val response = client.newCall(request).execute()
val cdBuffer = response.body.byteStream().use { it.readBytes() }
return parseAllCDs(cdBuffer)
}
}
object ZipParser {
fun parseAllCDs(buffer: ByteArray): List<CentralDirectoryRecord> {
val cds = ArrayList<CentralDirectoryRecord>()
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
var i = 0
while (i <= buffer.size - 4) {
val signature = view.getInt(i)
if (signature == CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE) {
val cd = parseCD(buffer.sliceArray(i until buffer.size))
if (cd != null) {
cds.add(cd)
i += cd.length - 1
continue
}
} else if (signature == END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
break
}
i++
}
return cds
}
fun parseCD(buffer: ByteArray): CentralDirectoryRecord? {
val MIN_CD_LENGTH = 46
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
for (i in 0..buffer.size - MIN_CD_LENGTH) {
if (view.getInt(i) == CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE) {
val filenameLength = view.getShort(i + 28).toInt()
val extraFieldLength = view.getShort(i + 30).toInt()
val fileCommentLength = view.getShort(i + 32).toInt()
return CentralDirectoryRecord(
length = 46 + filenameLength + extraFieldLength + fileCommentLength,
compressedSize = view.getInt(i + 20),
localFileHeaderRelativeOffset = view.getInt(i + 42),
filename = buffer.sliceArray(i + 46 until i + 46 + filenameLength).toString(UTF_8),
)
}
}
return null
}
fun parseEOCD(buffer: ByteArray): EndOfCentralDirectory? {
val MIN_EOCD_LENGTH = 22
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
for (i in 0 until buffer.size - MIN_EOCD_LENGTH + 1) {
if (view.getInt(i) == END_OF_CENTRAL_DIRECTORY_SIGNATURE) {
return EndOfCentralDirectory(
centralDirectoryByteSize = view.getInt(i + 12).toBigInteger(),
centralDirectoryByteOffset = view.getInt(i + 16).toBigInteger(),
)
}
}
return null
}
fun parseEOCD64(buffer: ByteArray): EndOfCentralDirectory? {
val MIN_EOCD_LENGTH = 56
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
for (i in 0 until buffer.size - MIN_EOCD_LENGTH + 1) {
if (view.getInt(i) == END_OF_CENTRAL_DIRECTORY_64_SIGNATURE) {
return EndOfCentralDirectory(
centralDirectoryByteSize = view.getLong(i + 40).toBigInteger(),
centralDirectoryByteOffset = view.getLong(i + 48).toBigInteger(),
)
}
}
return null
}
fun parseLocalFile(buffer: ByteArray, compressedSizeOverride: Int = 0): LocalFileHeader? {
val MIN_LOCAL_FILE_LENGTH = 30
val view = ByteBuffer.wrap(buffer).order(LITTLE_ENDIAN)
for (i in 0..buffer.size - MIN_LOCAL_FILE_LENGTH) {
if (view.getInt(i) == LOCAL_FILE_HEADER_SIGNATURE) {
val filenameLength = view.getShort(i + 26).toInt() and 0xFFFF
val extraFieldLength = view.getShort(i + 28).toInt() and 0xFFFF
val bitflags = view.getShort(i + 6).toInt() and 0xFFFF
val hasDataDescriptor = (bitflags shr 3) and 1 != 0
val headerEndOffset = i + 30 + filenameLength + extraFieldLength
val regularCompressedSize = view.getInt(i + 18)
val compressedData = if (hasDataDescriptor) {
buffer.copyOfRange(
headerEndOffset,
headerEndOffset + compressedSizeOverride,
)
} else {
buffer.copyOfRange(
headerEndOffset,
headerEndOffset + regularCompressedSize,
)
}
return LocalFileHeader(
compressedData = compressedData,
compressionMethod = view.getShort(i + 8).toInt(),
)
}
}
return null
}
fun inflateRaw(compressedData: ByteArray): ByteArray {
val inflater = Inflater(true)
inflater.setInput(compressedData)
val buffer = ByteArray(8192)
val output = ByteArrayOutputStream()
try {
while (!inflater.finished()) {
val count = inflater.inflate(buffer)
if (count > 0) {
output.write(buffer, 0, count)
}
}
} catch (e: Exception) {
throw Exception("Invalid compressed data format: ${e.message}", e)
} finally {
inflater.end()
output.close()
}
return output.toByteArray()
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Pururin'
extClass = '.PururinFactory'
extVersionCode = 9
extVersionCode = 8
isNsfw = true
}

View File

@ -7,33 +7,27 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.injectLazy
abstract class Pururin(
override val lang: String = "all",
private val searchLang: Pair<String, String>? = null,
private val searchLang: String? = null,
private val langPath: String = "",
) : ParsedHttpSource() {
override val name = "Pururin"
final override val baseUrl = "https://pururin.to"
override val baseUrl = "https://pururin.to"
override val supportsLatest = true
override val client = network.cloudflareClient
private val json: Json by injectLazy()
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers)
}
@ -51,6 +45,7 @@ abstract class Pururin(
override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]"
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/browse$langPath?page=$page", headers)
}
@ -63,131 +58,40 @@ abstract class Pururin(
// Search
private fun List<Pair<String, String>>.toValue(): String {
return "[${this.joinToString(",") { "{\"id\":${it.first},\"name\":\"${it.second}\"}" }}]"
}
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
if (num < 0) return minPages to maxPages
return when (query.firstOrNull()) {
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
'=' -> when (query[1]) {
'>' -> limitedNum() to maxPages
'<' -> 1 to limitedNum(maxPages)
else -> limitedNum() to limitedNum()
}
else -> limitedNum() to limitedNum()
}
}
@Serializable
class Tag(
val id: Int,
val name: String,
)
private fun findTagByNameSubstring(tags: List<Tag>, substring: String): Pair<String, String>? {
val tag = tags.find { it.name.contains(substring, ignoreCase = true) }
return tag?.let { Pair(tag.id.toString(), tag.name) }
}
private fun tagSearch(tag: String, type: String): Pair<String, String>? {
val requestBody = FormBody.Builder()
.add("text", tag)
.build()
val request = Request.Builder()
.url("$baseUrl/api/get/tags/search")
.headers(headers)
.post(requestBody)
.build()
val response = client.newCall(request).execute()
return findTagByNameSubstring(response.parseAs<List<Tag>>(), type)
private fun List<String>.toValue(): String {
return "[${this.joinToString(",")}]"
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val includeTags = mutableListOf<Pair<String, String>>()
val excludeTags = mutableListOf<Pair<String, String>>()
var pagesMin = 1
var pagesMax = 9999
var sortBy = "newest"
val includeTags = mutableListOf<String>()
val excludeTags = mutableListOf<String>()
var pagesMin: Int
var pagesMax: Int
if (searchLang != null) includeTags.add(searchLang)
filters.forEach {
when (it) {
is SelectFilter -> sortBy = it.getValue()
is TypeFilter -> {
val (_, inactiveFilters) = it.state.partition { stIt -> stIt.state }
excludeTags += inactiveFilters.map { fil -> Pair(fil.value, "${fil.name} [Category]") }
}
is PageFilter -> {
if (it.state.isNotEmpty()) {
val (min, max) = parsePageRange(it.state)
pagesMin = min
pagesMax = max
}
}
is TextFilter -> {
if (it.state.isNotEmpty()) {
it.state.split(",").filter(String::isNotBlank).map { tag ->
val trimmed = tag.trim()
if (trimmed.startsWith('-')) {
tagSearch(trimmed.lowercase().removePrefix("-"), it.type)?.let { tagInfo ->
excludeTags.add(tagInfo)
}
} else {
tagSearch(trimmed.lowercase(), it.type)?.let { tagInfo ->
includeTags.add(tagInfo)
}
}
}
}
}
else -> {}
filters.filterIsInstance<TagGroup<*>>().map { group ->
group.state.map {
if (it.isIncluded()) includeTags.add(it.id)
if (it.isExcluded()) excludeTags.add(it.id)
}
}
// Searching with just one tag usually gives wrong results
if (query.isEmpty()) {
when {
excludeTags.size == 1 && includeTags.isEmpty() -> excludeTags.addAll(excludeTags)
includeTags.size == 1 && excludeTags.isEmpty() -> {
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("browse")
addPathSegment("tags")
addPathSegment("content")
addPathSegment(includeTags[0].first)
addQueryParameter("sort", sortBy)
addQueryParameter("start_page", pagesMin.toString())
addQueryParameter("last_page", pagesMax.toString())
if (page > 1) addQueryParameter("page", page.toString())
}.build()
return GET(url, headers)
}
}
filters.find<PagesGroup>().range.let {
pagesMin = it.first
pagesMax = it.last
}
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("search")
addQueryParameter("q", query)
addQueryParameter("sort", sortBy)
addQueryParameter("start_page", pagesMin.toString())
addQueryParameter("last_page", pagesMax.toString())
if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
if (page > 1) addQueryParameter("page", page.toString())
}.build()
return GET(url, headers)
}
return GET(url.build().toString(), headers)
}
override fun searchMangaSelector(): String = popularMangaSelector()
@ -203,13 +107,8 @@ abstract class Pururin(
document.select(".box-gallery").let { e ->
initialized = true
title = e.select(".title").text()
author = e.select("a[href*=/circle/]").text().ifEmpty { e.select("[itemprop=author]").text() }
artist = e.select("[itemprop=author]").text()
genre = e.select("a[href*=/content/]").text()
author = e.select("[itemprop=author]").text()
description = e.select(".box-gallery .table-info tr")
.filter { tr ->
tr.select("td").none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
}
.joinToString("\n") { tr ->
tr.select("td")
.joinToString(": ") { it.text() }
@ -257,8 +156,8 @@ abstract class Pururin(
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
override fun getFilterList() = getFilters()
override fun getFilterList() = FilterList(
CategoryGroup(),
PagesGroup(),
)
}

View File

@ -14,11 +14,11 @@ class PururinFactory : SourceFactory {
class PururinAll : Pururin()
class PururinEN : Pururin(
"en",
Pair("13010", "english"),
"{\"id\":13010,\"name\":\"English [Language]\"}",
"/tags/language/13010/english",
)
class PururinJA : Pururin(
"ja",
Pair("13011", "japanese"),
"{\"id\":13011,\"name\":\"Japanese [Language]\"}",
"/tags/language/13011/japanese",
)

View File

@ -1,57 +1,57 @@
package eu.kanade.tachiyomi.extension.all.pururin
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
SelectFilter("Sort by", getSortsList),
TypeFilter("Types"),
Filter.Separator(),
Filter.Header("Separate tags with commas (,)"),
Filter.Header("Prepend with dash (-) to exclude"),
TextFilter("Tags", "[Content]"),
TextFilter("Artists", "[Artist]"),
TextFilter("Circles", "[Circle]"),
TextFilter("Parodies", "[Parody]"),
TextFilter("Languages", "[Language]"),
TextFilter("Scanlators", "[Scanlator]"),
TextFilter("Conventions", "[Convention]"),
TextFilter("Collections", "[Collections]"),
TextFilter("Categories", "[Category]"),
TextFilter("Uploaders", "[Uploader]"),
Filter.Separator(),
Filter.Header("Filter by pages, for example: (>20)"),
PageFilter("Pages"),
)
sealed class TagFilter(
name: String,
val id: String,
) : Filter.TriState(name)
sealed class TagGroup<T : TagFilter>(
name: String,
values: List<T>,
) : Filter.Group<T>(name, values)
class Category(name: String, id: String) : TagFilter(name, id)
class CategoryGroup(
values: List<Category> = categories,
) : TagGroup<Category>("Categories", values) {
companion object {
private val categories get() = listOf(
Category("Doujinshi", "{\"id\":13003,\"name\":\"Doujinshi [Category]\"}"),
Category("Manga", "{\"id\":13004,\"name\":\"Manga [Category]\"}"),
Category("Artist CG", "{\"id\":13006,\"name\":\"Artist CG [Category]\"}"),
Category("Game CG", "{\"id\":13008,\"name\":\"Game CG [Category]\"}"),
Category("Artbook", "{\"id\":17783,\"name\":\"Artbook [Category]\"}"),
Category("Webtoon", "{\"id\":27939,\"name\":\"Webtoon [Category]\"}"),
)
}
}
internal class TypeFilter(name: String) :
Filter.Group<CheckBoxFilter>(
name,
listOf(
Pair("Artbook", "17783"),
Pair("Artist CG", "13004"),
Pair("Doujinshi", "13003"),
Pair("Game CG", "13008"),
Pair("Manga", "13004"),
Pair("Webtoon", "27939"),
).map { CheckBoxFilter(it.first, it.second, true) },
)
internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state)
internal open class PageFilter(name: String) : Filter.Text(name)
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
internal open class SelectFilter(name: String, val vals: List<Pair<String, String>>, state: Int = 0) :
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
fun getValue() = vals[state].second
class PagesFilter(
name: String,
default: Int,
values: Array<Int> = range,
) : Filter.Select<Int>(name, values, default) {
companion object {
private val range get() = Array(301) { it }
}
}
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Newest", "newest"),
Pair("Most Popular", "most-popular"),
Pair("Highest Rated", "highest-rated"),
Pair("Most Viewed", "most-viewed"),
Pair("Title", "title"),
)
class PagesGroup(
values: List<PagesFilter> = minmax,
) : Filter.Group<PagesFilter>("Pages", values) {
inline val range get() = IntRange(state[0].state, state[1].state).also {
require(it.first <= it.last) { "'Minimum' cannot exceed 'Maximum'" }
}
companion object {
private val minmax get() = listOf(
PagesFilter("Minimum", 0),
PagesFilter("Maximum", 300),
)
}
}
inline fun <reified T> List<Filter<*>>.find() = find { it is T } as T

View File

@ -1,7 +1,7 @@
ext {
extName = 'Union Mangas'
extClass = '.UnionMangasFactory'
extVersionCode = 5
extVersionCode = 3
isNsfw = true
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.all.unionmangas
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
@ -9,16 +10,22 @@ 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.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.TimeUnit
class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
override val lang = langOption.lang
@ -31,12 +38,39 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
private val json: Json by injectLazy()
val langApiInfix = when (lang) {
"it" -> langOption.infix
else -> "v3/po"
}
override val client = network.client.newBuilder()
.rateLimit(2)
.rateLimit(5, 2, TimeUnit.SECONDS)
.build()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.set("Referer", "$baseUrl/")
private fun apiHeaders(url: String): Headers {
val date = apiDateFormat.format(Date())
val path = url.toUrlWithoutDomain()
return headersBuilder()
.add("_hash", authorization(apiSeed, domain, date))
.add("_tranId", authorization(apiSeed, domain, date, path))
.add("_date", date)
.add("_domain", domain)
.add("_path", path)
.add("Origin", baseUrl)
.add("Host", apiUrl.removeProtocol())
.add("Referer", "$baseUrl/")
.build()
}
private fun authorization(vararg payloads: String): String {
val md = MessageDigest.getInstance("MD5")
val bytes = payloads.joinToString("").toByteArray()
val digest = md.digest(bytes)
return digest
.fold("") { str, byte -> str + "%02x".format(byte) }
.padStart(32, '0')
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
@ -45,103 +79,97 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
var currentPage = 0
do {
val chaptersDto = fetchChapterListPageable(manga, currentPage)
chapters += chaptersDto.data.map { chapter ->
SChapter.create().apply {
name = chapter.name
date_upload = chapter.date.toDate()
url = chapter.toChapterUrl(langOption.infix)
}
}
chapters += chaptersDto.toSChapter(langOption)
currentPage++
} while (chaptersDto.hasNextPage())
return Observable.just(chapters)
return Observable.just(chapters.reversed())
}
private fun fetchChapterListPageable(manga: SManga, page: Int): Pageable<ChapterDto> {
manga.apply {
url = getURLCompatibility(url)
}
private fun fetchChapterListPageable(manga: SManga, page: Int): ChapterPageDto {
val maxResult = 16
val url = "$apiUrl/${langOption.infix}/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/ASC"
return client.newCall(GET(url, headers)).execute()
.parseAs<Pageable<ChapterDto>>()
val url = "$apiUrl/api/$langApiInfix/GetChapterListFilter/${manga.slug()}/$maxResult/$page/all/ASC"
return client.newCall(GET(url, apiHeaders(url))).execute()
.parseAs<ChapterPageDto>()
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun latestUpdatesParse(response: Response): MangasPage {
val nextData = response.parseNextData<LatestUpdateProps>()
val dto = nextData.data.latestUpdateDto
val mangas = dto.mangas.map { mangaParse(it, nextData.query) }
override fun latestUpdatesRequest(page: Int): Request {
val maxResult = 24
val url = "$apiUrl/${langOption.infix}/HomeLastUpdate".toHttpUrl().newBuilder()
.addPathSegment("$maxResult")
.addPathSegment("${page - 1}")
.build()
return GET(url, headers)
}
override fun getMangaUrl(manga: SManga): String {
manga.apply {
url = getURLCompatibility(url)
}
return baseUrl + manga.url.replace(langOption.infix, langOption.mangaSubstring)
}
override fun mangaDetailsRequest(manga: SManga): Request {
manga.apply {
url = getURLCompatibility(url)
}
val url = "$apiUrl/${langOption.infix}/getInfoManga".toHttpUrl().newBuilder()
.addPathSegment(manga.slug())
.build()
return GET(url, headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val dto = response.parseAs<MangaDetailsDto>()
return mangaParse(dto.details)
}
override fun pageListRequest(chapter: SChapter): Request {
val chapterSlug = getURLCompatibility(chapter.url)
.substringAfter(langOption.infix)
val url = "$apiUrl/${langOption.infix}/GetImageChapter$chapterSlug"
return GET(url, headers)
}
override fun pageListParse(response: Response): List<Page> {
val location = response.request.url.toString()
val dto = response.parseAs<PageDto>()
return dto.pages.mapIndexed { index, url ->
Page(index, location, imageUrl = url)
}
}
override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseAs<Pageable<MangaDto>>()
val mangas = dto.data.map(::mangaParse)
return MangasPage(
mangas = mangas,
hasNextPage = dto.hasNextPage(),
)
}
override fun popularMangaRequest(page: Int): Request {
val maxResult = 24
return GET("$apiUrl/${langOption.infix}/HomeTopFllow/$maxResult/${page - 1}")
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val maxResult = 20
val url = "$apiUrl/${langOption.infix}/QuickSearch/".toHttpUrl().newBuilder()
.addPathSegment(query)
.addPathSegment("$maxResult")
override fun latestUpdatesRequest(page: Int): Request {
val url = "$baseUrl/${langOption.infix}/latest-releases".toHttpUrl().newBuilder()
.addQueryParameter("page", "$page")
.build()
return GET(url, headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val nextData = response.parseNextData<MangaDetailsProps>()
val dto = nextData.data.mangaDetailsDto
return SManga.create().apply {
title = dto.title
genre = dto.genres
thumbnail_url = dto.thumbnailUrl
url = mangaUrlParse(dto.slug, nextData.query.type)
status = dto.status
}
}
override fun pageListParse(response: Response): List<Page> {
val chaptersDto = decryptChapters(response)
return chaptersDto.images.mapIndexed { index, imageUrl ->
Page(index, imageUrl = imageUrl)
}
}
private fun decryptChapters(response: Response): ChaptersDto {
val document = response.asJsoup()
val password = findChapterPassword(document)
val pageListData = document.parseNextData<ChaptersProps>().data.pageListData
val decodedData = CryptoAES.decrypt(pageListData, password)
return ChaptersDto(
data = json.decodeFromString<ChaptersDto>(decodedData).data,
delimiter = langOption.pageDelimiter,
)
}
private fun findChapterPassword(document: Document): String {
val regxPasswordUrl = """\/pages\/%5Btype%5D\/%5Bidmanga%5D\/%5Biddetail%5D-.+\.js""".toRegex()
val regxFindPassword = """AES\.decrypt\(\w+,"(?<password>[^"]+)"\)""".toRegex(RegexOption.MULTILINE)
val jsDecryptUrl = document.select("script")
.map { it.absUrl("src") }
.first { regxPasswordUrl.find(it) != null }
val jsDecrypt = client.newCall(GET(jsDecryptUrl, headers)).execute().asJsoup().html()
return regxFindPassword.find(jsDecrypt)?.groups?.get("password")!!.value.trim()
}
override fun popularMangaParse(response: Response): MangasPage {
val dto = response.parseNextData<PopularMangaProps>()
val mangas = dto.data.mangas.map { it.details }.map { mangaParse(it, dto.query) }
return MangasPage(
mangas = mangas,
hasNextPage = false,
)
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/${langOption.infix}")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val maxResult = 6
val url = "$apiUrl/api/$langApiInfix/searchforms/$maxResult/".toHttpUrl().newBuilder()
.addPathSegment(query)
.addPathSegment("${page - 1}")
.build()
return GET(url, apiHeaders(url.toString()))
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(SEARCH_PREFIX)) {
val url = "$baseUrl/${langOption.infix}/${query.substringAfter(SEARCH_PREFIX)}"
@ -157,54 +185,52 @@ class UnionMangas(private val langOption: LanguageOption) : HttpSource() {
override fun imageUrlParse(response: Response): String = ""
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<SearchDto>()
val mangasDto = response.parseAs<MangaListDto>().apply {
currentPage = response.request.url.pathSegments.last()
}
return MangasPage(
dto.mangas.map(::mangaParse),
false,
mangas = mangasDto.toSManga(langOption.infix),
hasNextPage = mangasDto.hasNextPage(),
)
}
/*
* Keeps compatibility with pt-BR previous version
* */
private fun getURLCompatibility(url: String): String {
val slugSuffix = "-br"
val mangaSubString = "manga-br"
private inline fun <reified T> Response.parseNextData() = asJsoup().parseNextData<T>()
val oldSlug = url.substringAfter(mangaSubString)
.substring(1)
.split("/")
.first()
val newSlug = oldSlug.substringBeforeLast(slugSuffix)
return url.replace(oldSlug, newSlug)
private inline fun <reified T> Document.parseNextData(): NextData<T> {
val jsonContent = selectFirst("script#__NEXT_DATA__")!!.html()
return json.decodeFromString<NextData<T>>(jsonContent)
}
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private fun String.removeProtocol() = trim().replace("https://", "")
private fun SManga.slug() = this.url.split("/").last()
private fun mangaParse(dto: MangaDto): SManga {
private fun String.toUrlWithoutDomain() = trim().replace(apiUrl, "")
private fun mangaParse(dto: MangaDto, query: QueryDto): SManga {
return SManga.create().apply {
title = dto.title
thumbnail_url = dto.thumbnailUrl
status = dto.status
url = "/${langOption.infix}/${dto.slug}"
url = mangaUrlParse(dto.slug, query.type)
genre = dto.genres
initialized = true
}
}
private fun String.toDate(): Long =
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
private fun mangaUrlParse(slug: String, pathSegment: String) = "/$pathSegment/$slug"
companion object {
const val SEARCH_PREFIX = "slug:"
val apiUrl = "https://app.unionmanga.xyz/api"
val oldApiUrl = "https://api.unionmanga.xyz"
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", Locale.ENGLISH)
val apiUrl = "https://api.unionmanga.xyz"
val apiSeed = "8e0550790c94d6abc71d738959a88d209690dc86"
val domain = "yaoi-chan.xyz"
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
val apiDateFormat = SimpleDateFormat("EE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH)
.apply { timeZone = TimeZone.getTimeZone("GMT") }
}
}

View File

@ -1,68 +1,149 @@
package eu.kanade.tachiyomi.extension.all.unionmangas
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class MangaDetailsDto(private val data: Props) {
val details: MangaDto get() = data.details
@Serializable
class Props(
@SerialName("infoDoc") val details: MangaDto,
)
class NextData<T>(val props: Props<T>, val query: QueryDto) {
val data get() = props.pageProps
}
@Serializable
open class Pageable<T>(
var currentPage: Int,
var totalPage: Int,
val data: List<T>,
) {
fun hasNextPage() = (currentPage + 1) <= totalPage
class Props<T>(val pageProps: T)
@Serializable
class PopularMangaProps(@SerialName("data_popular") val mangas: List<PopularMangaDto>)
@Serializable
class LatestUpdateProps(@SerialName("data_lastuppdate") val latestUpdateDto: MangaListDto)
@Serializable
class MangaDetailsProps(@SerialName("dataManga") val mangaDetailsDto: MangaDetailsDto)
@Serializable
class ChaptersProps(@SerialName("data") val pageListData: String)
@Serializable
abstract class Pageable {
abstract var currentPage: String?
abstract var totalPage: Int
fun hasNextPage() =
try { (currentPage!!.toInt() + 1) < totalPage } catch (_: Exception) { false }
}
@Serializable
class ChapterPageDto(
val totalRecode: Int = 0,
override var currentPage: String?,
override var totalPage: Int,
@SerialName("data") val chapters: List<ChapterDto> = emptyList(),
) : Pageable() {
fun toSChapter(langOption: LanguageOption): List<SChapter> =
chapters.map { chapter ->
SChapter.create().apply {
name = chapter.name
date_upload = chapter.date.toDate()
url = "/${langOption.infix}${chapter.toChapterUrl(langOption.chpPrefix)}"
}
}
private fun String.toDate(): Long =
try { UnionMangas.dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
private fun ChapterDto.toChapterUrl(prefix: String) = "/${this.slugManga}/$prefix-${this.id}"
}
@Serializable
class ChapterDto(
val date: String,
val slug: String,
@SerialName("idDoc") val slugManga: String,
@SerialName("idDetail") val id: String,
@SerialName("nameChapter") val name: String,
) {
fun toChapterUrl(lang: String) = "/$lang/${this.slugManga}/$id"
)
@Serializable
class QueryDto(
val type: String,
)
@Serializable
class MangaListDto(
override var currentPage: String?,
override var totalPage: Int,
@SerialName("data") val mangas: List<MangaDto>,
) : Pageable() {
fun toSManga(siteLang: String) = mangas.map { dto ->
SManga.create().apply {
title = dto.title
thumbnail_url = dto.thumbnailUrl
status = dto.status
url = mangaUrlParse(dto.slug, siteLang)
genre = dto.genres
}
}
}
@Serializable
class PopularMangaDto(
@SerialName("document") val details: MangaDto,
)
@Serializable
class MangaDto(
@SerialName("name") val title: String,
@SerialName("image") private val _thumbnailUrl: String,
@SerialName("idDoc") val slug: String,
@SerialName("genresName") val genres: String,
@SerialName("genres") private val _genres: String,
@SerialName("status") val _status: String,
) {
val thumbnailUrl get() = "${UnionMangas.oldApiUrl}$_thumbnailUrl"
val thumbnailUrl get() = "${UnionMangas.apiUrl}$_thumbnailUrl"
val genres get() = _genres.split(",").joinToString { it.trim() }
val status get() = toSMangaStatus(_status)
}
val status get() = when (_status) {
@Serializable
class MangaDetailsDto(
@SerialName("name") val title: String,
@SerialName("image") private val _thumbnailUrl: String,
@SerialName("idDoc") val slug: String,
@SerialName("lsgenres") private val _genres: List<Prop>,
@SerialName("lsstatus") private val _status: List<Prop>,
) {
val thumbnailUrl get() = "${UnionMangas.apiUrl}$_thumbnailUrl"
val genres get() = _genres.joinToString { it.name }
val status get() = toSMangaStatus(_status.firstOrNull()?.name ?: "")
@Serializable
class Prop(
val name: String,
)
}
@Serializable
class ChaptersDto(
@SerialName("dataManga") val data: PageDto,
private var delimiter: String = "",
) {
val images get() = data.getImages(delimiter)
}
@Serializable
class PageDto(
@SerialName("source") private val imgData: String,
) {
fun getImages(delimiter: String): List<String> = imgData.split(delimiter)
}
private fun mangaUrlParse(slug: String, pathSegment: String) = "/$pathSegment/$slug"
private fun toSMangaStatus(status: String) =
when (status.lowercase()) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
@Serializable
class SearchDto(
@SerialName("data")
val mangas: List<MangaDto>,
)
@Serializable
class PageDto(val `data`: Data) {
val pages: List<String> get() = `data`.detailDocuments.source.split("#")
@Serializable
class Data(@SerialName("detail_documents") val detailDocuments: DetailDocuments)
@Serializable
class DetailDocuments(val source: String)
}

View File

@ -7,9 +7,9 @@ class UnionMangasFactory : SourceFactory {
override fun createSources(): List<Source> = languages.map { UnionMangas(it) }
}
class LanguageOption(val lang: String, val infix: String = lang, val mangaSubstring: String = infix)
class LanguageOption(val lang: String, val infix: String = lang, val chpPrefix: String, val pageDelimiter: String)
val languages = listOf(
LanguageOption("pt-BR", "manga-br"),
LanguageOption("ru", "manga-ru", "mangas"),
LanguageOption("it", "italy", "leer", ","),
LanguageOption("pt-BR", "manga-br", "cap", "#"),
)

Some files were not shown because too many files have changed in this diff Show More