Compare commits

..

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

1701 changed files with 4378 additions and 11621 deletions

View File

@ -1,4 +1,3 @@
import html
import json
import os
import re
@ -108,10 +107,3 @@ with (REPO_DIR / "index.json").open("w", encoding="utf-8") as f:
with (REPO_DIR / "index.min.json").open("w", encoding="utf-8") as f:
json.dump(index_min_data, f, ensure_ascii=False, separators=(",", ":"))
with (REPO_DIR / "index.html").open("w", encoding="utf-8") as f:
f.write('<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<title>apks</title>\n</head>\n<body>\n<pre>\n')
for entry in index_data:
apk_escaped = 'apk/' + html.escape(entry["apk"])
name_escaped = html.escape(entry["name"])
f.write(f'<a href="{apk_escaped}">{name_escaped}</a>\n')
f.write('</pre>\n</body>\n</html>\n')

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

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

View File

@ -84,7 +84,7 @@ open class ComicGamma(
}
}
override fun chapterListSelector() = ".read__area .read__outer > a:not([href=#comics])"
override fun chapterListSelector() = ".read__area > .read__outer > a:not([href=#comics])"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
url = element.attr("href").toOldChapterUrl()
val number = url.removeSuffix("/").substringAfterLast('/').replace('_', '.')

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.galleryadults.GalleryAdultsUrlActivity"
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="/g.*/..*/"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>
</manifest>

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,941 +0,0 @@
package eu.kanade.tachiyomi.multisrc.galleryadults
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource
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.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import okhttp3.FormBody
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
abstract class GalleryAdults(
override val name: String,
override val baseUrl: String,
override val lang: String = "all",
protected open val mangaLang: String = LANGUAGE_MULTI,
protected val simpleDateFormat: SimpleDateFormat? = null,
) : ConfigurableSource, ParsedHttpSource() {
override val client: OkHttpClient = network.cloudflareClient
protected open val xhrHeaders = headers.newBuilder()
.add("X-Requested-With", "XMLHttpRequest")
.build()
/* Preferences */
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
protected open val useShortTitlePreference = true
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = PREF_SHORT_TITLE
title = "Display Short Titles"
summaryOff = "Showing Long Titles"
summaryOn = "Showing short Titles"
setDefaultValue(false)
setVisible(useShortTitlePreference)
}.also(screen::addPreference)
}
protected val SharedPreferences.shortTitle
get() = getBoolean(PREF_SHORT_TITLE, false)
/* List detail */
protected class SMangaDto(
val title: String,
val url: String,
val thumbnail: String?,
val lang: String,
)
protected open fun Element.mangaTitle(selector: String = ".caption"): String? =
mangaFullTitle(selector).let {
if (preferences.shortTitle) it?.shortenTitle() else it
}
protected open fun Element.mangaFullTitle(selector: String) =
selectFirst(selector)?.text()
protected open fun String.shortenTitle() = this.replace(shortenTitleRegex, "").trim()
protected open val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""")
protected open fun Element.mangaUrl() =
selectFirst(".inner_thumb a")?.attr("abs:href")
protected open fun Element.mangaThumbnail() =
selectFirst(".inner_thumb img")?.imgAttr()
// Overwrite this to filter other languages' manga from search result.
// Default to [mangaLang] won't filter anything
protected open fun Element.mangaLang() = mangaLang
protected open fun HttpUrl.Builder.addPageUri(page: Int): HttpUrl.Builder {
val url = toString()
if (!url.endsWith('/') && !url.contains('?')) {
addPathSegment("") // trailing slash (/)
}
addQueryParameter("page", page.toString())
return this
}
/* Popular */
override fun popularMangaRequest(page: Int): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (mangaLang.isNotBlank()) addPathSegments("language/$mangaLang")
if (supportsLatest) addPathSegment("popular")
addPageUri(page)
}
return GET(url.build(), headers)
}
override fun popularMangaSelector() = "div.thumb"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.mangaTitle()!!
setUrlWithoutDomain(element.mangaUrl()!!)
thumbnail_url = element.mangaThumbnail()
}
}
override fun popularMangaNextPageSelector() = ".pagination li.active + li:not(.disabled)"
/* Latest */
override fun latestUpdatesRequest(page: Int): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (mangaLang.isNotBlank()) addPathSegments("language/$mangaLang")
addPageUri(page)
}
return GET(url.build(), headers)
}
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
/* Search */
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val randomEntryFilter = filters.filterIsInstance<RandomEntryFilter>().firstOrNull()
return when {
randomEntryFilter?.state == true -> {
client.newCall(randomEntryRequest())
.asObservableSuccess()
.map { response -> randomEntryParse(response) }
}
query.startsWith(PREFIX_ID_SEARCH) -> {
val id = query.removePrefix(PREFIX_ID_SEARCH)
client.newCall(searchMangaByIdRequest(id))
.asObservableSuccess()
.map { response -> searchMangaByIdParse(response, id) }
}
query.toIntOrNull() != null -> {
client.newCall(searchMangaByIdRequest(query))
.asObservableSuccess()
.map { response -> searchMangaByIdParse(response, query) }
}
else -> {
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response -> searchMangaParse(response) }
}
}
}
protected open fun randomEntryRequest(): Request = GET("$baseUrl/random/", headers)
protected open fun randomEntryParse(response: Response): MangasPage {
val document = response.asJsoup()
val url = response.request.url.toString()
val id = url.removeSuffix("/").substringAfterLast('/')
return MangasPage(
listOf(
SManga.create().apply {
title = document.mangaTitle("h1")!!
setUrlWithoutDomain("$baseUrl/$idPrefixUri/$id/")
thumbnail_url = document.getCover()
},
),
false,
)
}
/**
* Manga URL: $baseUrl/$idPrefixUri/<id>/
*/
protected open val idPrefixUri = "gallery"
protected open fun searchMangaByIdRequest(id: String): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment(idPrefixUri)
addPathSegments("$id/")
}
return GET(url.build(), headers)
}
protected open fun searchMangaByIdParse(response: Response, id: String): MangasPage {
val details = mangaDetailsParse(response.asJsoup())
details.url = "/$idPrefixUri/$id/"
return MangasPage(listOf(details), false)
}
protected open val useIntermediateSearch: Boolean = false
protected open val supportAdvancedSearch: Boolean = false
protected open val supportSpeechless: Boolean = false
protected open val useBasicSearch: Boolean
get() = !useIntermediateSearch
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// Basic search
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
val genresFilter = filters.filterIsInstance<GenresFilter>().firstOrNull()
val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList()
val favoriteFilter = filters.filterIsInstance<FavoriteFilter>().firstOrNull()
// Speechless
val speechlessFilter = filters.filterIsInstance<SpeechlessFilter>().firstOrNull()
// Advanced search
val advancedSearchFilters = filters.filterIsInstance<AdvancedTextFilter>()
return when {
favoriteFilter?.state == true ->
favoriteFilterSearchRequest(page, query, filters)
supportSpeechless && speechlessFilter?.state == true ->
speechlessFilterSearchRequest(page, query, filters)
supportAdvancedSearch && advancedSearchFilters.any { it.state.isNotBlank() } ->
advancedSearchRequest(page, query, filters)
selectedGenres.size == 1 && query.isBlank() ->
tagBrowsingSearchRequest(page, query, filters)
useIntermediateSearch ->
intermediateSearchRequest(page, query, filters)
useBasicSearch && (selectedGenres.size > 1 || query.isNotBlank()) ->
basicSearchRequest(page, query, filters)
sortOrderFilter?.state == 1 ->
latestUpdatesRequest(page)
else ->
popularMangaRequest(page)
}
}
protected open val basicSearchKey = "q"
/**
* Basic Search: support query string with multiple-genres filter by adding genres to query string.
*/
protected open fun basicSearchRequest(page: Int, query: String, filters: FilterList): Request {
// Basic search
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
val genresFilter = filters.filterIsInstance<GenresFilter>().firstOrNull()
val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList()
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegments("search/")
addEncodedQueryParameter(basicSearchKey, buildQueryString(selectedGenres.map { it.name }, query))
if (sortOrderFilter?.state == 0) addQueryParameter("sort", "popular")
addPageUri(page)
}
return GET(url.build(), headers)
}
protected open val intermediateSearchKey = "key"
/**
* This supports filter query search with languages, categories (manga, doujinshi...)
* with additional sort orders.
*/
protected open fun intermediateSearchRequest(page: Int, query: String, filters: FilterList): Request {
// Basic search
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
val genresFilter = filters.filterIsInstance<GenresFilter>().firstOrNull()
val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList()
// Intermediate search
val categoryFilters = filters.filterIsInstance<CategoryFilters>().firstOrNull()
// Only for query string or multiple tags
val url = "$baseUrl/search".toHttpUrl().newBuilder().apply {
getSortOrderURIs().forEachIndexed { index, pair ->
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
}
categoryFilters?.state?.forEach {
addQueryParameter(it.uri, toBinary(it.state))
}
getLanguageURIs().forEach { pair ->
addQueryParameter(
pair.second,
toBinary(mangaLang == pair.first || mangaLang == LANGUAGE_MULTI),
)
}
addEncodedQueryParameter(intermediateSearchKey, buildQueryString(selectedGenres.map { it.name }, query))
addPageUri(page)
}
return GET(url.build())
}
protected open val advancedSearchKey = "key"
protected open val advancedSearchUri = "advsearch"
/**
* Advanced Search normally won't support search for string but allow include/exclude specific
* tags/artists/groups/parodies/characters
*/
protected open fun advancedSearchRequest(page: Int, query: String, filters: FilterList): Request {
// Basic search
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
val genresFilter = filters.filterIsInstance<GenresFilter>().firstOrNull()
val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList()
// Intermediate search
val categoryFilters = filters.filterIsInstance<CategoryFilters>().firstOrNull()
// Advanced search
val advancedSearchFilters = filters.filterIsInstance<AdvancedTextFilter>()
val url = "$baseUrl/$advancedSearchUri".toHttpUrl().newBuilder().apply {
getSortOrderURIs().forEachIndexed { index, pair ->
addQueryParameter(pair.second, toBinary(sortOrderFilter?.state == index))
}
categoryFilters?.state?.forEach {
addQueryParameter(it.uri, toBinary(it.state))
}
getLanguageURIs().forEach { pair ->
addQueryParameter(
pair.second,
toBinary(
mangaLang == pair.first ||
mangaLang == LANGUAGE_MULTI,
),
)
}
// Build this query string: +tag:"bat+man"+-tag:"cat"+artist:"Joe"...
// +tag must be encoded into %2Btag while the rest are not needed to encode
val keys = emptyList<String>().toMutableList()
keys.addAll(selectedGenres.map { "%2Btag:\"${it.name}\"" })
advancedSearchFilters.forEach { filter ->
val key = when (filter) {
is TagsFilter -> "tag"
is ParodiesFilter -> "parody"
is ArtistsFilter -> "artist"
is CharactersFilter -> "character"
is GroupsFilter -> "group"
else -> null
}
if (key != null) {
keys.addAll(
filter.state.trim()
.replace(regexSpaceNotAfterComma, "+")
.replace(" ", "")
.split(',')
.mapNotNull {
val match = regexExcludeTerm.find(it)
match?.groupValues?.let { groups ->
"${if (groups[1].isNotBlank()) "-" else "%2B"}$key:\"${groups[2]}\""
}
},
)
}
}
addEncodedQueryParameter(advancedSearchKey, keys.joinToString("+"))
addPageUri(page)
}
return GET(url.build())
}
/**
* Convert space( ) typed in search-box into plus(+) in URL. Then:
* - uses plus(+) to search for exact match
* - use comma(,) for separate terms, as AND condition.
* Plus(+) after comma(,) doesn't have any effect.
*/
protected open fun buildQueryString(tags: List<String>, query: String): String {
return (tags + query).filterNot { it.isBlank() }.joinToString(",") {
// any space except after a comma (we're going to replace spaces only between words)
it.trim()
.replace(regexSpaceNotAfterComma, "+")
.replace(" ", "")
}
}
protected open fun tagBrowsingSearchRequest(page: Int, query: String, filters: FilterList): Request {
// Basic search
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
val genresFilter = filters.filterIsInstance<GenresFilter>().firstOrNull()
val selectedGenres = genresFilter?.state?.filter { it.state } ?: emptyList()
// Browsing single tag's catalog
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("tag")
addPathSegment(selectedGenres.single().uri)
if (sortOrderFilter?.state == 0) addPathSegment("popular")
addPageUri(page)
}
return GET(url.build(), headers)
}
/**
* Browsing speechless titles. Some sites exclude speechless titles from normal search and
* allow browsing separately.
*/
protected open fun speechlessFilterSearchRequest(page: Int, query: String, filters: FilterList): Request {
// Basic search
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("language")
addPathSegment(LANGUAGE_SPEECHLESS)
if (sortOrderFilter?.state == 0) addPathSegment("popular")
addPageUri(page)
}
return GET(url.build(), headers)
}
/**
* Browsing user's personal favorites saved on site. This requires login in view WebView.
*/
protected open fun favoriteFilterSearchRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/$favoritePath".toHttpUrl().newBuilder()
return POST(
url.build().toString(),
xhrHeaders,
FormBody.Builder()
.add("page", page.toString())
.build(),
)
}
protected open val favoritePath = "user/fav_pags.php"
protected open fun loginRequired(document: Document, url: String): Boolean {
return (
url.contains("/login/") &&
document.select("input[value=Login]").isNotEmpty()
)
}
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
if (loginRequired(document, response.request.url.toString())) {
throw Exception("Log in via WebView to view favorites")
} else {
val hasNextPage = document.select(searchMangaNextPageSelector()).isNotEmpty()
val mangas = document.select(searchMangaSelector())
.map {
SMangaDto(
title = it.mangaTitle()!!,
url = it.mangaUrl()!!,
thumbnail = it.mangaThumbnail(),
lang = it.mangaLang(),
)
}
.let { unfiltered ->
val results = unfiltered.filter { mangaLang.isBlank() || it.lang == mangaLang }
// return at least 1 title if all mangas in current page is of other languages
if (results.isEmpty() && hasNextPage) listOf(unfiltered[0]) else results
}
.map {
SManga.create().apply {
title = it.title
setUrlWithoutDomain(it.url)
thumbnail_url = it.thumbnail
}
}
return MangasPage(mangas, hasNextPage)
}
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
/* Details */
protected open val mangaDetailInfoSelector = ".gallery_top"
override fun mangaDetailsParse(document: Document): SManga {
return document.selectFirst(mangaDetailInfoSelector)!!.run {
SManga.create().apply {
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.COMPLETED
title = mangaTitle("h1")!!
thumbnail_url = getCover()
genre = getInfo("Tags")
author = getInfo("Artists")
description = getDescription(document)
}
}
}
protected open fun Element.getCover() =
selectFirst(".cover img")?.imgAttr()
protected val regexTag = Regex("Tags?")
/**
* Parsing document to extract info related to [tag].
*/
protected abstract fun Element.getInfo(tag: String): String
protected open fun Element.getDescription(document: Document? = null): String = (
listOf("Parodies", "Characters", "Languages", "Categories", "Category")
.mapNotNull { tag ->
getInfo(tag)
.takeIf { it.isNotBlank() }
?.let { "$tag: $it" }
} +
listOfNotNull(
getInfoPages(document),
getInfoAlternativeTitle(),
getInfoFullTitle(),
)
)
.joinToString("\n\n")
protected open fun Element.getInfoPages(document: Document? = null): String? =
document?.inputIdValueOf(totalPagesSelector)
?.takeIf { it.isNotBlank() }
?.let { "Pages: $it" }
protected open fun Element.getInfoAlternativeTitle(): String? =
selectFirst("h1 + h2, .subtitle")?.ownText()
.takeIf { !it.isNullOrBlank() }
?.let { "Alternative title: $it" }
protected open fun Element.getInfoFullTitle(): String? =
if (preferences.shortTitle) "Full title: ${mangaFullTitle("h1")}" else null
protected open fun Element.getTime(): Long =
selectFirst(".uploaded")
?.ownText()
.toDate(simpleDateFormat)
/* Chapters */
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return listOf(
SChapter.create().apply {
name = "Chapter"
scanlator = document.selectFirst(mangaDetailInfoSelector)
?.getInfo("Groups")
date_upload = document.getTime()
setUrlWithoutDomain(response.request.url.encodedPath)
},
)
}
override fun chapterListSelector() = throw UnsupportedOperationException()
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
/* Pages */
protected open fun Element.inputIdValueOf(string: String): String {
return select("input[id=$string]").attr("value")
}
protected open val pagesRequest = "inc/thumbs_loader.php"
protected open val galleryIdSelector = "gallery_id"
protected open val loadIdSelector = "load_id"
protected open val loadDirSelector = "load_dir"
protected open val totalPagesSelector = "load_pages"
protected open val serverSelector = "load_server"
protected open fun pageRequestForm(document: Document, totalPages: String, loadedPages: Int): FormBody {
val token = document.select("[name=csrf-token]").attr("content")
val serverNumber = document.serverNumber()
return FormBody.Builder()
.add("u_id", document.inputIdValueOf(galleryIdSelector))
.add("g_id", document.inputIdValueOf(loadIdSelector))
.add("img_dir", document.inputIdValueOf(loadDirSelector))
.add("visible_pages", loadedPages.toString())
.add("total_pages", totalPages)
.add("type", "2") // 1 would be "more", 2 is "all remaining"
.apply {
if (token.isNotBlank()) add("_token", token)
if (serverNumber != null) add("server", serverNumber)
}
.build()
}
protected open val thumbnailSelector = ".gallery_thumb"
private val jsonFormat: Json by injectLazy()
protected open fun Element.getServer(): String {
val domain = baseUrl.toHttpUrl().host
return serverNumber()
?.let { "m$it.$domain" }
?: getCover()!!.toHttpUrl().host
}
protected open fun Element.serverNumber(): String? =
inputIdValueOf(serverSelector)
.takeIf { it.isNotBlank() }
protected open fun Element.parseJson(): String? =
selectFirst("script:containsData(parseJSON)")?.data()
?.substringAfter("$.parseJSON('")
?.substringBefore("');")?.trim()
/**
* Page URL: $baseUrl/$pageUri/<id>/<page>
*/
protected open val pageUri = "g"
override fun pageListParse(document: Document): List<Page> {
val json = document.parseJson()
if (json != null) {
val loadDir = document.inputIdValueOf(loadDirSelector)
val loadId = document.inputIdValueOf(loadIdSelector)
val galleryId = document.inputIdValueOf(galleryIdSelector)
val pageUrl = "$baseUrl/$pageUri/$galleryId"
val server = document.getServer()
val imagesUri = "https://$server/$loadDir/$loadId"
try {
val pages = mutableListOf<Page>()
val images = jsonFormat.parseToJsonElement(json).jsonObject
// JSON string in this form: {"1":"j,1100,1148","2":"j,728,689",...
for (image in images) {
val ext = image.value.toString().replace("\"", "").split(",")[0]
val imageExt = when (ext) {
"p" -> "png"
"b" -> "bmp"
"g" -> "gif"
else -> "jpg"
}
val idx = image.key.toInt()
pages.add(
Page(
index = idx,
imageUrl = "$imagesUri/${image.key}.$imageExt",
url = "$pageUrl/$idx/",
),
)
}
return pages
} catch (e: SerializationException) {
Log.e("GalleryAdults", "Failed to decode JSON")
return this.pageListParseAlternative(document)
}
} else {
return this.pageListParseAlternative(document)
}
}
/**
* Overwrite this to force extension not blindly converting thumbnails to full image
* by simply removing the trailing "t" from file name. Instead, it will open each page,
* one by one, then parsing for actual image's URL.
* This will be much slower but guaranteed work.
*
* This only apply if site doesn't provide 'parseJSON'.
*/
protected open val parsingImagePageByPage: Boolean = false
/**
* Either:
* - Load all thumbnails then convert thumbnails to full images.
* - Or request then parse for a list of manga's page's URL,
* which will then request one by one to parse for page's image's URL using [imageUrlParse].
*/
protected open fun pageListParseAlternative(document: Document): List<Page> {
val totalPages = document.inputIdValueOf(totalPagesSelector)
val galleryId = document.inputIdValueOf(galleryIdSelector)
val pageUrl = "$baseUrl/$pageUri/$galleryId"
val pages = document.select("$thumbnailSelector a")
.map {
if (parsingImagePageByPage) {
it.absUrl("href")
} else {
it.selectFirst("img")!!.imgAttr()
}
}
.toMutableList()
if (totalPages.isNotBlank() && totalPages.toInt() > pages.size) {
val form = pageRequestForm(document, totalPages, pages.size)
val morePages = client.newCall(POST("$baseUrl/$pagesRequest", xhrHeaders, form))
.execute()
.asJsoup()
.select("a")
.map {
if (parsingImagePageByPage) {
it.absUrl("href")
} else {
it.selectFirst("img")!!.imgAttr()
}
}
if (morePages.isNotEmpty()) {
pages.addAll(morePages)
} else {
return pageListParseDummy(document)
}
}
return pages.mapIndexed { idx, url ->
if (parsingImagePageByPage) {
Page(idx, url)
} else {
Page(
index = idx,
imageUrl = url.thumbnailToFull(),
url = "$pageUrl/$idx/",
)
}
}
}
/**
* Generate all images using `totalPages`. Supposedly they are sequential.
* Use in case any extension doesn't know how to request for "All thumbnails"
*/
protected open fun pageListParseDummy(document: Document): List<Page> {
val loadDir = document.inputIdValueOf(loadDirSelector)
val loadId = document.inputIdValueOf(loadIdSelector)
val galleryId = document.inputIdValueOf(galleryIdSelector)
val pageUrl = "$baseUrl/$pageUri/$galleryId"
val server = document.getServer()
val imagesUri = "https://$server/$loadDir/$loadId"
val images = document.select("$thumbnailSelector img")
val thumbUrls = images.map { it.imgAttr() }.toMutableList()
val totalPages = document.inputIdValueOf(totalPagesSelector)
if (totalPages.isNotBlank() && totalPages.toInt() > thumbUrls.size) {
val imagesExt = images.first()?.imgAttr()!!
.substringAfterLast('.')
thumbUrls.addAll(
listOf((images.size + 1)..totalPages.toInt()).flatten().map {
"$imagesUri/${it}t.$imagesExt"
},
)
}
return thumbUrls.mapIndexed { idx, url ->
Page(
index = idx,
imageUrl = url.thumbnailToFull(),
url = "$pageUrl/$idx/",
)
}
}
override fun imageUrlParse(document: Document): String {
return document.selectFirst("img#gimg, img#fimg")?.imgAttr()!!
}
/* Filters */
private val scope = CoroutineScope(Dispatchers.IO)
private fun launchIO(block: () -> Unit) = scope.launch { block() }
private var tagsFetched = false
private var tagsFetchAttempt = 0
/**
* List of tags in <name, uri> pairs
*/
protected var genres: MutableMap<String, String> = mutableMapOf()
protected open fun tagsRequest(page: Int): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegments("tags/popular")
addPageUri(page)
}
return GET(url.build(), headers)
}
/**
* Parsing [document] to return a list of tags in <name, uri> pairs.
*/
protected open fun tagsParser(document: Document): List<Genre> {
return document.select("a.tag_btn")
.mapNotNull {
Genre(
it.select(".list_tag, .tag_name").text(),
it.attr("href")
.removeSuffix("/").substringAfterLast('/'),
)
}
}
protected open fun requestTags() {
if (!tagsFetched && tagsFetchAttempt < 3) {
launchIO {
val tags = mutableListOf<Genre>()
runBlocking {
val jobsPool = mutableListOf<Job>()
// Get first 3 pages
(1..3).forEach { page ->
jobsPool.add(
launchIO {
runCatching {
tags.addAll(
client.newCall(tagsRequest(page))
.execute().asJsoup().let { tagsParser(it) },
)
}
},
)
}
jobsPool.joinAll()
tags.sortedWith(compareBy { it.name })
.forEach {
genres[it.name] = it.uri
}
tagsFetched = true
}
tagsFetchAttempt++
}
}
}
override fun getFilterList(): FilterList {
requestTags()
val filters = emptyList<Filter<*>>().toMutableList()
if (useIntermediateSearch) {
filters.add(Filter.Header("HINT: Separate search term with comma (,)"))
}
filters.add(SortOrderFilter(getSortOrderURIs()))
if (genres.isEmpty()) {
filters.add(Filter.Header("Press 'reset' to attempt to load tags"))
} else {
filters.add(GenresFilter(genres))
}
if (useIntermediateSearch || supportAdvancedSearch) {
filters.addAll(
listOf(
Filter.Separator(),
CategoryFilters(getCategoryURIs()),
),
)
}
if (supportAdvancedSearch) {
filters.addAll(
listOf(
Filter.Separator(),
Filter.Header("Advanced filters will ignore query search. Separate terms by comma (,) and precede term with minus (-) to exclude."),
TagsFilter(),
ParodiesFilter(),
ArtistsFilter(),
CharactersFilter(),
GroupsFilter(),
),
)
}
filters.add(Filter.Separator())
if (supportSpeechless) {
filters.add(SpeechlessFilter())
}
filters.add(FavoriteFilter())
filters.add(RandomEntryFilter())
return FilterList(filters)
}
protected open fun getSortOrderURIs() = listOf(
Pair("Popular", "pp"),
Pair("Latest", "lt"),
) + if (useIntermediateSearch || supportAdvancedSearch) {
listOf(
Pair("Downloads", "dl"),
Pair("Top Rated", "tr"),
)
} else {
emptyList()
}
protected open fun getCategoryURIs() = listOf(
SearchFlagFilter("Manga", "m"),
SearchFlagFilter("Doujinshi", "d"),
SearchFlagFilter("Western", "w"),
SearchFlagFilter("Image Set", "i"),
SearchFlagFilter("Artist CG", "a"),
SearchFlagFilter("Game CG", "g"),
)
protected open fun getLanguageURIs() = listOf(
Pair(LANGUAGE_ENGLISH, "en"),
Pair(LANGUAGE_JAPANESE, "jp"),
Pair(LANGUAGE_SPANISH, "es"),
Pair(LANGUAGE_FRENCH, "fr"),
Pair(LANGUAGE_KOREAN, "kr"),
Pair(LANGUAGE_GERMAN, "de"),
Pair(LANGUAGE_RUSSIAN, "ru"),
)
companion object {
const val PREFIX_ID_SEARCH = "id:"
private const val PREF_SHORT_TITLE = "pref_short_title"
// references to be used in factory
const val LANGUAGE_MULTI = ""
const val LANGUAGE_ENGLISH = "english"
const val LANGUAGE_JAPANESE = "japanese"
const val LANGUAGE_CHINESE = "chinese"
const val LANGUAGE_KOREAN = "korean"
const val LANGUAGE_SPANISH = "spanish"
const val LANGUAGE_FRENCH = "french"
const val LANGUAGE_GERMAN = "german"
const val LANGUAGE_RUSSIAN = "russian"
const val LANGUAGE_SPEECHLESS = "speechless"
const val LANGUAGE_TRANSLATED = "translated"
}
}

View File

@ -1,31 +0,0 @@
package eu.kanade.tachiyomi.multisrc.galleryadults
import eu.kanade.tachiyomi.source.model.Filter
class Genre(name: String, val uri: String) : Filter.CheckBox(name)
class GenresFilter(genres: Map<String, String>) : Filter.Group<Genre>(
"Tags",
genres.map { Genre(it.key, it.value) },
)
class SortOrderFilter(sortOrderURIs: List<Pair<String, String>>) :
Filter.Select<String>("Sort By", sortOrderURIs.map { it.first }.toTypedArray())
class FavoriteFilter : Filter.CheckBox("Show favorites only (login via WebView)", false)
class RandomEntryFilter : Filter.CheckBox("Random manga", false)
// Speechless
class SpeechlessFilter : Filter.CheckBox("Show speechless items only", false)
// Intermediate search
class SearchFlagFilter(name: String, val uri: String, state: Boolean = true) : Filter.CheckBox(name, state)
class CategoryFilters(flags: List<SearchFlagFilter>) : Filter.Group<SearchFlagFilter>("Categories", flags)
// Advance search
abstract class AdvancedTextFilter(name: String) : Filter.Text(name)
class TagsFilter : AdvancedTextFilter("Tags")
class ParodiesFilter : AdvancedTextFilter("Parodies")
class ArtistsFilter : AdvancedTextFilter("Artists")
class CharactersFilter : AdvancedTextFilter("Characters")
class GroupsFilter : AdvancedTextFilter("Groups")

View File

@ -1,144 +0,0 @@
package eu.kanade.tachiyomi.multisrc.galleryadults
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
// any space except after a comma (we're going to replace spaces only between words)
val regexSpaceNotAfterComma = Regex("""(?<!,)\s+""")
// extract preceding minus (-) and term
val regexExcludeTerm = Regex("""^(-?)"?(.+)"?""")
val regexTagCountNumber = Regex("\\([0-9,]*\\)")
val regexDateSuffix = Regex("""\d(st|nd|rd|th)""")
val regexDate = Regex("""\d\D\D""")
val regexNotNumber = Regex("""\D""")
val regexRelativeDateTime = Regex("""\d*[^0-9]*(\d+)""")
fun Element.imgAttr() = when {
hasAttr("data-cfsrc") -> absUrl("data-cfsrc")
hasAttr("data-src") -> absUrl("data-src")
hasAttr("data-lazy-src") -> absUrl("data-lazy-src")
hasAttr("srcset") -> absUrl("srcset").substringBefore(" ")
else -> absUrl("src")
}
fun Element.cleanTag(): String = text().cleanTag()
fun String.cleanTag(): String = replace(regexTagCountNumber, "").trim()
// convert thumbnail URLs to full image URLs
fun String.thumbnailToFull(): String {
val ext = substringAfterLast(".")
return replace("t.$ext", ".$ext")
}
fun String?.toDate(simpleDateFormat: SimpleDateFormat?): Long {
this ?: return 0L
return if (simpleDateFormat != null) {
if (contains(regexDateSuffix)) {
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
split(" ").map {
if (it.contains(regexDate)) {
it.replace(regexNotNumber, "")
} else {
it
}
}
.let { simpleDateFormat.tryParse(it.joinToString(" ")) }
} else {
simpleDateFormat.tryParse(this)
}
} else {
parseDate(this)
}
}
private fun parseDate(date: String?): Long {
date ?: return 0L
return when {
// Handle 'yesterday' and 'today', using midnight
WordSet("yesterday", "يوم واحد").startsWith(date) -> {
Calendar.getInstance().apply {
add(Calendar.DAY_OF_MONTH, -1) // yesterday
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
}
WordSet("today", "just now").startsWith(date) -> {
Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
}
WordSet("يومين").startsWith(date) -> {
Calendar.getInstance().apply {
add(Calendar.DAY_OF_MONTH, -2) // day before yesterday
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}.timeInMillis
}
WordSet("ago", "atrás", "önce", "قبل").endsWith(date) -> {
parseRelativeDate(date)
}
WordSet("hace").startsWith(date) -> {
parseRelativeDate(date)
}
else -> 0L
}
}
// Parses dates in this form: 21 hours ago OR "2 days ago (Updated 19 hours ago)"
private fun parseRelativeDate(date: String): Long {
val number = regexRelativeDateTime.find(date)?.value?.toIntOrNull()
?: date.split(" ").firstOrNull()
?.replace("one", "1")
?.replace("a", "1")
?.toIntOrNull()
?: return 0L
val now = Calendar.getInstance()
// Sort by order
return when {
WordSet("detik", "segundo", "second", "วินาที").anyWordIn(date) ->
now.apply { add(Calendar.SECOND, -number) }.timeInMillis
WordSet("menit", "dakika", "min", "minute", "minuto", "นาที", "دقائق").anyWordIn(date) ->
now.apply { add(Calendar.MINUTE, -number) }.timeInMillis
WordSet("jam", "saat", "heure", "hora", "hour", "ชั่วโมง", "giờ", "ore", "ساعة", "小时").anyWordIn(date) ->
now.apply { add(Calendar.HOUR, -number) }.timeInMillis
WordSet("hari", "gün", "jour", "día", "dia", "day", "วัน", "ngày", "giorni", "أيام", "").anyWordIn(date) ->
now.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
WordSet("week", "semana").anyWordIn(date) ->
now.apply { add(Calendar.DAY_OF_MONTH, -number * 7) }.timeInMillis
WordSet("month", "mes").anyWordIn(date) ->
now.apply { add(Calendar.MONTH, -number) }.timeInMillis
WordSet("year", "año").anyWordIn(date) ->
now.apply { add(Calendar.YEAR, -number) }.timeInMillis
else -> 0L
}
}
private fun SimpleDateFormat.tryParse(string: String): Long {
return try {
parse(string)?.time ?: 0L
} catch (_: ParseException) {
0L
}
}
class WordSet(private vararg val words: String) {
fun anyWordIn(dateString: String): Boolean = words.any { dateString.contains(it, ignoreCase = true) }
fun startsWith(dateString: String): Boolean = words.any { dateString.startsWith(it, ignoreCase = true) }
fun endsWith(dateString: String): Boolean = words.any { dateString.endsWith(it, ignoreCase = true) }
}
fun toBinary(boolean: Boolean) = if (boolean) "1" else "0"

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,178 +0,0 @@
package eu.kanade.tachiyomi.multisrc.goda
import eu.kanade.tachiyomi.network.GET
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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.Entities
import rx.Observable
open class GoDa(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : HttpSource() {
override val supportsLatest get() = true
private val enableGenres = true
override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/")
override val client = network.cloudflareClient
private fun getKey(link: String): String {
return link.substringAfter("/manga/").removeSuffix("/")
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/hots/page/$page", headers)
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup().also(::parseGenres)
val mangas = document.select(".cardlist .pb-2 a").map { element ->
SManga.create().apply {
val imgSrc = element.selectFirst("img")!!.attr("src")
url = getKey(element.attr("href"))
title = element.selectFirst("h3")!!.ownText()
thumbnail_url = if ("url=" in imgSrc) imgSrc.toHttpUrl().queryParameter("url")!! else imgSrc
}
}
val nextPage = if (lang == "zh") "下一頁" else "NEXT"
val hasNextPage = document.selectFirst("a[aria-label=$nextPage] button") != null
return MangasPage(mangas, hasNextPage)
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/newss/page/$page", headers)
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = "$baseUrl/s".toHttpUrl().newBuilder()
.addPathSegment(query)
.addEncodedQueryParameter("page", "$page")
.build()
return GET(url, headers)
}
for (filter in filters) {
if (filter is UriPartFilter) return GET(baseUrl + filter.toUriPart() + "/page/$page", headers)
}
return popularMangaRequest(page)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun getMangaUrl(manga: SManga) = "$baseUrl/manga/${manga.url}"
override fun mangaDetailsRequest(manga: SManga): Request {
return GET(getMangaUrl(manga), headers)
}
private fun Element.getMangaId() = selectFirst("#mangachapters")!!.attr("data-mid")
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val document = response.asJsoup().selectFirst("main")!!
val titleElement = document.selectFirst("h1")!!
val elements = titleElement.parent()!!.parent()!!.children()
check(elements.size == 6)
title = titleElement.ownText()
status = when (titleElement.child(0).text()) {
"連載中", "Ongoing" -> SManga.ONGOING
"完結" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
author = Entities.unescape(elements[1].children().drop(1).joinToString { it.text().removeSuffix(" ,") })
genre = buildList {
elements[2].children().drop(1).mapTo(this) { it.text().removeSuffix(" ,") }
elements[3].children().mapTo(this) { it.text().removePrefix("#") }
}.joinToString()
description = (elements[4].text() + "\n\nID: ${document.getMangaId()}").trim()
thumbnail_url = document.selectFirst("img.object-cover")!!.attr("src")
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
val mangaId = manga.description
?.substringAfterLast("ID: ", "")
?.takeIf { it.toIntOrNull() != null }
?: client.newCall(mangaDetailsRequest(manga)).execute().asJsoup().getMangaId()
fetchChapterList(mangaId)
}
override fun chapterListParse(response: Response): List<SChapter> {
throw UnsupportedOperationException()
}
open fun fetchChapterList(mangaId: String): List<SChapter> {
val response = client.newCall(GET("$baseUrl/manga/get?mid=$mangaId&mode=all", headers)).execute()
return response.asJsoup().select(".chapteritem").asReversed().map { element ->
val anchor = element.selectFirst("a")!!
SChapter.create().apply {
url = getKey(anchor.attr("href")) + "#$mangaId/" + anchor.attr("data-cs")
name = anchor.attr("data-ct")
}
}
}
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/manga/" + chapter.url.substringBeforeLast('#')
override fun pageListRequest(chapter: SChapter): Request {
val id = chapter.url.substringAfterLast('#', "")
val mangaId = id.substringBefore('/', "")
val chapterId = id.substringAfter('/', "")
return pageListRequest(mangaId, chapterId)
}
open fun pageListRequest(mangaId: String, chapterId: String) = GET("$baseUrl/chapter/getcontent?m=$mangaId&c=$chapterId", headers)
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return document.select("#chapcontent > div > img").mapIndexed { index, element ->
Page(index, imageUrl = element.attr("data-src").ifEmpty { element.attr("src") })
}
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
private var genres: Array<Pair<String, String>> = emptyArray()
private fun parseGenres(document: Document) {
if (!enableGenres || genres.isNotEmpty()) return
val box = document.selectFirst("h2")?.parent()?.parent() ?: return
val items = box.select("a")
genres = Array(items.size) { i ->
val item = items[i]
Pair(item.text().removePrefix("#"), item.attr("href"))
}
}
override fun getFilterList(): FilterList =
if (!enableGenres) {
FilterList()
} else if (genres.isEmpty()) {
FilterList(listOf(Filter.Header(if (lang == "zh") "点击“重置”刷新分类" else "Tap 'Reset' to load genres")))
} else {
val list = listOf(
Filter.Header(if (lang == "zh") "分类(搜索文本时无效)" else "Filters are ignored when using text search."),
UriPartFilter(if (lang == "zh") "分类" else "Genre", genres),
)
FilterList(list)
}
class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
}

View File

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

View File

@ -76,7 +76,7 @@ abstract class HeanCms(
protected open val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ", Locale.US)
override fun headersBuilder() = super.headersBuilder()
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")

View File

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

View File

@ -223,38 +223,42 @@ abstract class Keyoapp(
// Image list
override fun pageListParse(document: Document): List<Page> {
return document.select("#pages > img")
.map { it.imgAttr() }
.filter { it.contains(imgCdnRegex) }
.mapIndexed { index, img ->
Page(index, document.location(), img)
return document.select("#pages > img").map {
val index = it.attr("count").toInt()
Page(index, document.location(), it.imgAttr("150"))
}
}
private val imgCdnRegex = Regex("""^(https?:)?//cdn\d*\.keyoapp\.com""")
override fun imageUrlParse(document: Document) = ""
// Utilities
// From mangathemesia
private fun Element.imgAttr(): String {
private fun Element.imgAttr(width: String): String {
val url = when {
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("data-src") -> attr("abs:data-src")
else -> attr("abs:src")
}
return url
return url.toHttpUrl()
.newBuilder()
.addQueryParameter("w", width)
.build()
.toString()
}
private fun Element.getImageUrl(selector: String): String? {
return this.selectFirst(selector)?.let { element ->
element.attr("style")
return this.selectFirst(selector)?.let {
it.attr("style")
.substringAfter(":url(", "")
.substringBefore(")", "")
.takeIf { it.isNotEmpty() }
?.toHttpUrlOrNull()?.newBuilder()?.setQueryParameter("w", "480")?.build()
?.toString()
?.toHttpUrlOrNull()?.let {
it.newBuilder()
.setQueryParameter("w", "480")
.build()
.toString()
}
}
}

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 36
baseVersionCode = 35
dependencies {
api(project(":lib:cryptoaes"))

View File

@ -612,9 +612,6 @@ abstract class Madara(
"مكتمل",
"已完结",
"Tamamlandı",
"Đã hoàn thành",
"Завершено",
"Tamamlanan",
)
protected val ongoingStatusList: Array<String> = arrayOf(
@ -622,7 +619,6 @@ 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",
)
protected val hiatusStatusList: Array<String> = arrayOf(
@ -630,22 +626,12 @@ abstract class Madara(
"Pausado",
"En espera",
"Durduruldu",
"Beklemede",
"Đang chờ",
"متوقف",
"En Pause",
"Заморожено",
)
protected val canceledStatusList: Array<String> = arrayOf(
"Canceled",
"Cancelado",
"İptal Edildi",
"Güncel",
"Đã hủy",
"ملغي",
"Abandonné",
"Заброшено",
)
override fun mangaDetailsParse(document: Document): SManga {
@ -960,11 +946,7 @@ abstract class Madara(
val imageUrl = element.selectFirst("img")?.let { imageFromElement(it) }
Page(index, document.location(), imageUrl)
}
val chapterProtectorHtml = chapterProtector.attr("src")
.takeIf { it.startsWith("data:text/javascript;base64,") }
?.substringAfter("data:text/javascript;base64,")
?.let { Base64.decode(it, Base64.DEFAULT).toString(Charsets.UTF_8) }
?: chapterProtector.html()
val chapterProtectorHtml = chapterProtector.html()
val password = chapterProtectorHtml
.substringAfter("wpmangaprotectornonce='")
.substringBefore("';")

View File

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

View File

@ -21,7 +21,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
@ -30,25 +29,24 @@ import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
abstract class MadTheme(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH),
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM dd, yyy", Locale.US),
) : ParsedHttpSource() {
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(1, 1, TimeUnit.SECONDS)
.rateLimit(1, 1)
.build()
// TODO: better cookie sharing
// TODO: don't count cached responses against rate limit
private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(1, 12, TimeUnit.SECONDS)
.rateLimit(1, 12)
.build()
override fun headersBuilder() = Headers.Builder().apply {
@ -57,8 +55,6 @@ abstract class MadTheme(
private val json: Json by injectLazy()
private var genreKey = "genre[]"
// Popular
override fun popularMangaRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList(OrderFilter(0)))
@ -104,7 +100,7 @@ abstract class MadTheme(
.filter { it.state }
.let { list ->
if (list.isNotEmpty()) {
list.forEach { genre -> url.addQueryParameter(genreKey, genre.id) }
list.forEach { genre -> url.addQueryParameter("genre[]", genre.id) }
}
}
}
@ -124,11 +120,11 @@ abstract class MadTheme(
override fun searchMangaSelector(): String = ".book-detailed-item"
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("abs:href"))
title = element.selectFirst("a")!!.attr("title")
element.selectFirst(".summary")?.text()?.let { description = it }
element.select(".genres > *").joinToString { it.text() }.takeIf { it.isNotEmpty() }?.let { genre = it }
thumbnail_url = element.selectFirst("img")!!.attr("abs:data-src")
setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href"))
title = element.select("a").first()!!.attr("title")
description = element.select(".summary").first()?.text()
genre = element.select(".genres > *").joinToString { it.text() }
thumbnail_url = element.select("img").first()!!.attr("abs:data-src")
}
/*
@ -139,25 +135,23 @@ abstract class MadTheme(
// Details
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
title = document.selectFirst(".detail h1")!!.text()
title = document.select(".detail h1").first()!!.text()
author = document.select(".detail .meta > p > strong:contains(Authors) ~ a").joinToString { it.text().trim(',', ' ') }
genre = document.select(".detail .meta > p > strong:contains(Genres) ~ a").joinToString { it.text().trim(',', ' ') }
thumbnail_url = document.selectFirst("#cover img")!!.attr("abs:data-src")
thumbnail_url = document.select("#cover img").first()!!.attr("abs:data-src")
val altNames = document.selectFirst(".detail h2")?.text()
val altNames = document.select(".detail h2").first()?.text()
?.split(',', ';')
?.mapNotNull { it.trim().takeIf { it != title } }
?: listOf()
description = document.select(".summary .content, .summary .content ~ p").text() +
description = document.select(".summary .content").first()?.text() +
(altNames.takeIf { it.isNotEmpty() }?.let { "\n\nAlt name(s): ${it.joinToString()}" } ?: "")
val statusText = document.selectFirst(".detail .meta > p > strong:contains(Status) ~ a")!!.text()
status = when (statusText.lowercase(Locale.ENGLISH)) {
val statusText = document.select(".detail .meta > p > strong:contains(Status) ~ a").first()!!.text()
status = when (statusText.lowercase(Locale.US)) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
"on-hold" -> SManga.ON_HIATUS
"canceled" -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
@ -193,14 +187,7 @@ abstract class MadTheme(
}
override fun chapterListRequest(manga: SManga): Request =
MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)?.let { mangaId ->
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", mangaId)
.addQueryParameter("manga_name", manga.title)
.build()
GET(url, headers)
} ?: GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers)
GET("$baseUrl/api/manga${manga.url}/chapters?source=detail", headers)
override fun searchMangaParse(response: Response): MangasPage {
if (genresList == null) {
@ -217,25 +204,16 @@ abstract class MadTheme(
.absUrl("href")
.removePrefix(baseUrl)
name = element.selectFirst(".chapter-title")!!.text()
date_upload = parseChapterDate(element.selectFirst(".chapter-update")?.text())
name = element.select(".chapter-title").first()!!.text()
date_upload = parseChapterDate(element.select(".chapter-update").first()?.text())
}
// Pages
override fun pageListParse(document: Document): List<Page> {
val mangaId = MANGA_ID_REGEX.find(document.location())?.groupValues?.get(1)
val chapterId = CHAPTER_ID_REGEX.find(document.html())?.groupValues?.get(1)
val html = if (mangaId != null && chapterId != null) {
val url = GET("$baseUrl/service/backend/chapterServer/?server_id=1&chapter_id=$chapterId", headers)
client.newCall(url).execute().body.string()
} else {
document.html()
}
val realDocument = Jsoup.parse(html, document.location())
val html = document.html()
if (!html.contains("var mainServer = \"")) {
val chapterImagesFromHtml = realDocument.select("#chapter-images img, .chapter-image[data-src]")
val chapterImagesFromHtml = document.select("#chapter-images img")
// 17/03/2023: Certain hosts only embed two pages in their "#chapter-images" and leave
// the rest to be lazily(?) loaded by javascript. Let's extract `chapImages` and compare
@ -314,7 +292,7 @@ abstract class MadTheme(
}
return when {
" ago" in date -> {
"ago".endsWith(date) -> {
parseRelativeDate(date)
}
else -> dateFormat.tryParse(date)
@ -322,12 +300,10 @@ abstract class MadTheme(
}
private fun parseRelativeDate(date: String): Long {
val number = NUMBER_REGEX.find(date)?.groupValues?.getOrNull(0)?.toIntOrNull() ?: return 0
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
val cal = Calendar.getInstance()
return when {
date.contains("year") -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
date.contains("month") -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
date.contains("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
@ -338,21 +314,13 @@ abstract class MadTheme(
// Dynamic genres
private fun parseGenres(document: Document): List<Genre>? {
return document.selectFirst(".checkbox-group.genres")?.select(".checkbox-wrapper")?.run {
firstOrNull()?.selectFirst("input")?.attr("name")?.takeIf { it.isNotEmpty() }?.let { genreKey = it }
map {
Genre(it.selectFirst(".radio__label")!!.text(), it.selectFirst("input")!!.`val`())
}
return document.select(".checkbox-group.genres").first()?.select("label")?.map {
Genre(it.select(".radio__label").first()!!.text(), it.select("input").`val`())
}
}
// Filters
override fun getFilterList() = FilterList(
// TODO: Filters for sites that support it:
// excluded genres
// genre inclusion mode
// bookmarks
// author
GenreFilter(getGenreList()),
StatusFilter(),
OrderFilter(),
@ -384,7 +352,6 @@ abstract class MadTheme(
Pair("Updated", "updated_at"),
Pair("Created", "created_at"),
Pair("Name A-Z", "name"),
// Pair("Number of Chapters", "total_chapters"),
Pair("Rating", "rating"),
),
state,
@ -398,10 +365,4 @@ abstract class MadTheme(
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray(), state) {
fun toUriPart() = vals[state].second
}
companion object {
private val MANGA_ID_REGEX = """/manga/(\d+)-""".toRegex()
private val CHAPTER_ID_REGEX = """chapterId\s*=\s*(\d+)""".toRegex()
private val NUMBER_REGEX = """\d+""".toRegex()
}
}

View File

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

View File

@ -73,7 +73,7 @@ abstract class MangaEsp(
return MangasPage(mangas, false)
}
protected var comicsList = mutableListOf<SeriesDto>()
private var comicsList = mutableListOf<SeriesDto>()
override fun fetchSearchManga(
page: Int,
@ -93,7 +93,7 @@ abstract class MangaEsp(
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
protected open fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
private fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
val document = response.asJsoup()
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
@ -105,7 +105,7 @@ abstract class MangaEsp(
private var filteredList = mutableListOf<SeriesDto>()
protected open fun parseComicsList(page: Int, query: String, filterList: FilterList): MangasPage {
private fun parseComicsList(page: Int, query: String, filterList: FilterList): MangasPage {
if (page == 1) {
filteredList.clear()
@ -228,21 +228,21 @@ abstract class MangaEsp(
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
protected open fun Element.imgAttr(): String = when {
private fun Element.imgAttr(): String = when {
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
hasAttr("data-src") -> attr("abs:data-src")
hasAttr("data-cfsrc") -> attr("abs:data-cfsrc")
else -> attr("abs:src")
}
fun String.unescape(): String {
private fun String.unescape(): String {
return UNESCAPE_REGEX.replace(this, "$1")
}
companion object {
private val UNESCAPE_REGEX = """\\(.)""".toRegex()
val MANGA_LIST_REGEX = """self\.__next_f\.push\(.*data\\":(\[.*trending.*])\}""".toRegex()
private val MANGA_LIST_REGEX = """self\.__next_f\.push\(.*data\\":(\[.*trending.*])\}""".toRegex()
private val MANGA_DETAILS_REGEX = """self\.__next_f\.push\(.*data\\":(\{.*lastChapters.*\}).*\\"numFollow""".toRegex()
const val MANGAS_PER_PAGE = 15
private const val MANGAS_PER_PAGE = 15
}
}

View File

@ -44,9 +44,7 @@ class SeriesDto(
val trending: TrendingDto? = null,
@SerialName("autors") private val authors: List<AuthorDto> = emptyList(),
private val artists: List<ArtistDto> = emptyList(),
@Suppress("unused") // Used in some sources
@SerialName("idioma")
val language: String? = null,
) {
fun toSManga(): SManga {
return SManga.create().apply {

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 9
baseVersionCode = 4
dependencies {
compileOnly("com.github.tachiyomiorg:image-decoder:e08e9be535")

View File

@ -8,7 +8,6 @@ import android.graphics.Canvas
import android.graphics.Rect
import android.util.Base64
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@ -29,7 +28,6 @@ import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@ -84,18 +82,6 @@ abstract class PeachScan(
override fun latestUpdatesNextPageSelector() = null
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(URL_SEARCH_PREFIX)) {
val manga = SManga.create().apply { url = query.substringAfter(URL_SEARCH_PREFIX) }
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.map {
MangasPage(listOf(mangaDetailsParse(it).apply { url = manga.url }), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegments("auto-complete/")
@ -167,31 +153,21 @@ abstract class PeachScan(
}.getOrDefault(0L)
}
private val urlsRegex = """const\s+urls\s*=\s*\[(.*?)]\s*;""".toRegex()
override fun pageListParse(document: Document): List<Page> {
val scriptElement = document.selectFirst("script:containsData(const urls)")
val scriptElement = document.selectFirst("script:containsData(const urls =[)")
?: return document.select("#imageContainer img").mapIndexed { i, it ->
Page(i, document.location(), it.attr("abs:src"))
Page(i, imageUrl = it.attr("abs:src"))
}
val urls = urlsRegex.find(scriptElement.data())?.groupValues?.get(1)
?: throw Exception("Could not find image URLs")
val urls = scriptElement.html().substringAfter("const urls =[").substringBefore("];")
return urls.split(",").mapIndexed { i, it ->
Page(i, document.location(), baseUrl + it.trim().removeSurrounding("'") + "#page")
Page(i, imageUrl = baseUrl + it.trim().removeSurrounding("'") + "#page")
}
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
override fun imageRequest(page: Page): Request {
val imgHeaders = headersBuilder()
.add("Referer", page.url)
.build()
return GET(page.imageUrl!!, imgHeaders)
}
private val dataUriRegex = Regex("""base64,([0-9a-zA-Z/+=\s]+)""")
private fun zipImageInterceptor(chain: Interceptor.Chain): Response {
@ -212,7 +188,7 @@ abstract class PeachScan(
val entryIndex = splitEntryName.first().toInt()
val entryType = splitEntryName.last()
val imageData = if (entryType == "avif" || splitEntryName.size == 1) {
val imageData = if (entryType == "avif") {
zis.readBytes()
} else {
val svgBytes = zis.readBytes()
@ -275,8 +251,4 @@ abstract class PeachScan(
memInfo.totalMem < 3L * 1024 * 1024 * 1024
}
companion object {
const val URL_SEARCH_PREFIX = "slug:"
}
}

View File

@ -1,32 +0,0 @@
package eu.kanade.tachiyomi.multisrc.peachscan
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class PeachScanUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val path = intent?.data?.path
if (path != null) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${PeachScan.URL_SEARCH_PREFIX}$path")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("PeachScanUrlActivity", e.toString())
}
} else {
Log.e("PeachScanUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -212,8 +212,8 @@ abstract class ZeistManga(
protected open val useNewChapterFeed = false
protected open val useOldChapterFeed = false
protected open val chapterFeedRegex = """clwd\.run\(["'](.*?)["']\)""".toRegex()
protected open val scriptSelector = "#clwd > script"
private val chapterFeedRegex = """clwd\.run\(["'](.*?)["']\)""".toRegex()
private val scriptSelector = "#clwd > script"
open fun getChapterFeedUrl(doc: Document): String {
if (useNewChapterFeed) return newChapterFeedUrl(doc)
@ -434,6 +434,6 @@ abstract class ZeistManga(
companion object {
private const val maxMangaResults = 20
const val maxChapterResults = 999999
private const val maxChapterResults = 999999
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,122 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ahottie
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
class AHottie() : ParsedHttpSource() {
override val baseUrl = "https://ahottie.net"
override val lang = "all"
override val name = "AHottie"
override val supportsLatest = false
// Popular
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
thumbnail_url = element.select(".relative img").attr("src")
genre = element.select(".flex a").joinToString(", ") {
it.text()
}
title = element.select("h2").text()
setUrlWithoutDomain(element.select("a").attr("href"))
initialized = true
}
override fun popularMangaNextPageSelector() = "a[rel=next]"
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/?page=$page", headers)
}
override fun popularMangaSelector() = "#main > div > div"
// Search
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET(
baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("search")
addQueryParameter("kw", query)
addQueryParameter("page", page.toString())
}.build(),
headers,
)
}
override fun searchMangaSelector() = popularMangaSelector()
// Details
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.select("h1").text()
genre = document.select("div.pl-3 > a").joinToString(", ") {
it.text()
}
}
override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
var doc = document
while (true) {
doc.select("#main img.block").map {
pages.add(Page(pages.size, imageUrl = it.attr("src")))
}
val nextPageUrl = doc.select("a[rel=next]").attr("abs:href")
if (nextPageUrl.isEmpty()) break
doc = client.newCall(GET(nextPageUrl, headers)).execute().asJsoup()
}
return pages
}
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.select("link[rel=canonical]").attr("abs:href"))
chapter_number = 0F
name = "GALLERY"
date_upload = getDate(element.select("time").text())
}
override fun chapterListSelector() = "html"
// Pages
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SManga {
throw UnsupportedOperationException()
}
override fun latestUpdatesNextPageSelector(): String? {
throw UnsupportedOperationException()
}
override fun latestUpdatesRequest(page: Int): Request {
throw UnsupportedOperationException()
}
override fun latestUpdatesSelector(): String {
throw UnsupportedOperationException()
}
private fun getDate(str: String): Long {
return try {
DATE_FORMAT.parse(str)?.time ?: 0L
} catch (e: ParseException) {
0L
}
}
companion object {
private val DATE_FORMAT by lazy {
SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
}
}
}

View File

@ -2,19 +2,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.brasilhentai.BrasilHentaiUrlActivity"
android:name=".all.asmhentai.ASMHUrlActivity"
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="brasilhentai.com"
android:pathPattern="/..*"
android:host="asmhentai.com"
android:pathPattern="/g/..*"
android:scheme="https" />
</intent-filter>
</activity>

View File

@ -1,9 +1,7 @@
ext {
extName = 'AsmHentai'
extClass = '.ASMHFactory'
themePkg = 'galleryadults'
baseUrl = 'https://asmhentai.com'
overrideVersionCode = 2
extVersionCode = 1
isNsfw = true
}

View File

@ -1,14 +1,13 @@
package eu.kanade.tachiyomi.extension.all.asmhentai
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class ASMHFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
AsmHentai("en", GalleryAdults.LANGUAGE_ENGLISH),
AsmHentai("ja", GalleryAdults.LANGUAGE_JAPANESE),
AsmHentai("zh", GalleryAdults.LANGUAGE_CHINESE),
AsmHentai("all", GalleryAdults.LANGUAGE_MULTI),
AsmHentai("en", "english"),
AsmHentai("ja", "japanese"),
AsmHentai("zh", "chinese"),
AsmHentai("all", ""),
)
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.tr.hattorimanga
package eu.kanade.tachiyomi.extension.all.asmhentai
import android.app.Activity
import android.content.ActivityNotFoundException
@ -7,28 +7,28 @@ import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class HattoriMangaUrlActivity : Activity() {
private val tag = javaClass.simpleName
/**
* Springboard that accepts https://asmhentai.com/g/xxxxxx intents and redirects them to
* the main Tachiyomi process.
*/
class ASMHUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${HattoriManga.SEARCH_PREFIX}$item")
putExtra("query", "${AsmHentai.PREFIX_ID_SEARCH}${pathSegments[1]}")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
Log.e("ASMHUrlActivity", e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
Log.e("ASMHUrlActivity", "could not parse uri from intent $intent")
}
finish()

View File

@ -1,103 +1,274 @@
package eu.kanade.tachiyomi.extension.all.asmhentai
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
import eu.kanade.tachiyomi.multisrc.galleryadults.Genre
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
class AsmHentai(
lang: String = "all",
override val mangaLang: String = LANGUAGE_MULTI,
) : GalleryAdults(
"AsmHentai",
"https://asmhentai.com",
lang = lang,
) {
override val supportsLatest = mangaLang.isNotBlank()
open class AsmHentai(override val lang: String, private val tlTag: String) : ParsedHttpSource() {
override fun Element.mangaUrl() =
selectFirst(".image a")?.attr("abs:href")
override val client: OkHttpClient = network.cloudflareClient
override fun Element.mangaThumbnail() =
selectFirst(".image img")?.imgAttr()
override val baseUrl = "https://asmhentai.com"
override fun Element.mangaLang() =
select("a:has(.flag)").attr("href")
.removeSuffix("/").substringAfterLast("/")
override val name = "AsmHentai"
override fun popularMangaSelector() = ".preview_item"
override val supportsLatest = false
override val favoritePath = "inc/user.php?act=favs"
// Popular
override fun Element.getInfo(tag: String): String {
return select(".tags:contains($tag:) .tag_list a")
.joinToString {
val name = it.selectFirst(".tag")?.ownText() ?: ""
if (tag.contains(regexTag)) {
genres[name] = it.attr("href")
.removeSuffix("/").substringAfterLast('/')
override fun popularMangaRequest(page: Int): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (tlTag.isNotEmpty()) addPathSegments("language/$tlTag/")
if (page > 1) addQueryParameter("page", page.toString())
}
listOf(
name,
it.select(".split_tag").text()
.removePrefix("| ")
.trim(),
return GET(url.build(), headers)
}
override fun popularMangaSelector(): String = ".preview_item"
private fun Element.mangaTitle() = select("h2").text()
private fun Element.mangaUrl() = select(".image a").attr("abs:href")
private fun Element.mangaThumbnail() = select(".image img").attr("abs:src")
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
title = element.mangaTitle()
setUrlWithoutDomain(element.mangaUrl())
thumbnail_url = element.mangaThumbnail()
}
}
override fun popularMangaNextPageSelector(): String = "li.active + li:not(.disabled)"
// Latest
override fun latestUpdatesNextPageSelector(): String? {
throw UnsupportedOperationException()
}
override fun latestUpdatesRequest(page: Int): Request {
throw UnsupportedOperationException()
}
override fun latestUpdatesFromElement(element: Element): SManga {
throw UnsupportedOperationException()
}
override fun latestUpdatesSelector(): String {
throw UnsupportedOperationException()
}
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PREFIX_ID_SEARCH) -> {
val id = query.removePrefix(PREFIX_ID_SEARCH)
client.newCall(searchMangaByIdRequest(id))
.asObservableSuccess()
.map { response -> searchMangaByIdParse(response, id) }
}
query.toIntOrNull() != null -> {
client.newCall(searchMangaByIdRequest(query))
.asObservableSuccess()
.map { response -> searchMangaByIdParse(response, query) }
}
else -> super.fetchSearchManga(page, query, filters)
}
}
// any space except after a comma (we're going to replace spaces only between words)
private val spaceRegex = Regex("""(?<!,)\s+""")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val tags = (filters.last() as TagFilter).state
val q = when {
tags.isBlank() -> query
query.isBlank() -> tags
else -> "$query,$tags"
}.replace(spaceRegex, "+")
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegments("search/")
addEncodedQueryParameter("q", q)
if (page > 1) addQueryParameter("page", page.toString())
}
return GET(url.build(), headers)
}
private class SMangaDto(
val title: String,
val url: String,
val thumbnail: String,
val lang: String,
)
.filter { s -> s.isNotBlank() }
.joinToString()
override fun searchMangaParse(response: Response): MangasPage {
val doc = response.asJsoup()
val mangas = doc.select(searchMangaSelector())
.map {
SMangaDto(
title = it.mangaTitle(),
url = it.mangaUrl(),
thumbnail = it.mangaThumbnail(),
lang = it.select("a:has(.flag)").attr("href").removeSuffix("/").substringAfterLast("/"),
)
}
.let { unfiltered ->
if (tlTag.isNotEmpty()) unfiltered.filter { it.lang == tlTag } else unfiltered
}
.map {
SManga.create().apply {
title = it.title
setUrlWithoutDomain(it.url)
thumbnail_url = it.thumbnail
}
}
override fun Element.getInfoPages(document: Document?) =
selectFirst(".book_page .pages h3")?.ownText()
return MangasPage(mangas, doc.select(searchMangaNextPageSelector()).isNotEmpty())
}
override val mangaDetailInfoSelector = ".book_page"
private fun searchMangaByIdRequest(id: String) = GET("$baseUrl/g/$id/", headers)
/**
* [totalPagesSelector] only exists if pages > 10
*/
override val totalPagesSelector = "t_pages"
private fun searchMangaByIdParse(response: Response, id: String): MangasPage {
val details = mangaDetailsParse(response)
details.url = "/g/$id/"
return MangasPage(listOf(details), false)
}
override val galleryIdSelector = "load_id"
override val thumbnailSelector = ".preview_thumb"
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override val idPrefixUri = "g"
override val pageUri = "gallery"
override fun searchMangaSelector() = popularMangaSelector()
override fun pageRequestForm(document: Document, totalPages: String, loadedPages: Int): FormBody {
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
// Details
private fun Element.get(tag: String): String {
return select(".tags:contains($tag) .tag").joinToString { it.ownText() }
}
override fun mangaDetailsParse(document: Document): SManga {
return SManga.create().apply {
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
document.select(".book_page").first()!!.let { element ->
thumbnail_url = element.select(".cover img").attr("abs:src")
title = element.select("h1").text()
genre = element.get("Tags")
artist = element.get("Artists")
author = artist
description = listOf("Parodies", "Groups", "Languages", "Category")
.mapNotNull { tag ->
element.get(tag).let { if (it.isNotEmpty()) "$tag: $it" else null }
}
.joinToString("\n", postfix = "\n") +
element.select(".pages h3").text() +
element.select("h1 + h2").text()
.let { altTitle -> if (altTitle.isNotEmpty()) "\nAlternate Title: $altTitle" else "" }
}
}
}
// Chapters
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.just(
listOf(
SChapter.create().apply {
name = "Chapter"
url = manga.url
},
),
)
}
override fun chapterListSelector(): String {
throw UnsupportedOperationException()
}
override fun chapterFromElement(element: Element): SChapter {
throw UnsupportedOperationException()
}
// Pages
// convert thumbnail URLs to full image URLs
private fun String.full(): String {
val fType = substringAfterLast("t")
return replace("t$fType", fType)
}
private fun Document.inputIdValueOf(string: String): String {
return select("input[id=$string]").attr("value")
}
override fun pageListParse(document: Document): List<Page> {
val thumbUrls = document.select(".preview_thumb img")
.map { it.attr("abs:data-src") }
.toMutableList()
// input only exists if pages > 10 and have to make a request to get the other thumbnails
val totalPages = document.inputIdValueOf("t_pages")
if (totalPages.isNotEmpty()) {
val token = document.select("[name=csrf-token]").attr("content")
return FormBody.Builder()
.add("id", document.inputIdValueOf(loadIdSelector))
.add("dir", document.inputIdValueOf(loadDirSelector))
.add("visible_pages", loadedPages.toString())
val form = FormBody.Builder()
.add("_token", token)
.add("id", document.inputIdValueOf("load_id"))
.add("dir", document.inputIdValueOf("load_dir"))
.add("visible_pages", "10")
.add("t_pages", totalPages)
.add("type", "2") // 1 would be "more", 2 is "all remaining"
.apply {
if (token.isNotBlank()) add("_token", token)
}
.build()
val xhrHeaders = headers.newBuilder()
.add("X-Requested-With", "XMLHttpRequest")
.build()
client.newCall(POST("$baseUrl/inc/thumbs_loader.php", xhrHeaders, form))
.execute()
.asJsoup()
.select("img")
.mapTo(thumbUrls) { it.attr("abs:data-src") }
}
return thumbUrls.mapIndexed { i, url -> Page(i, "", url.full()) }
}
override fun tagsParser(document: Document): List<Genre> {
return document.select(".tags_page .tags a.tag")
.mapNotNull {
Genre(
it.ownText(),
it.attr("href")
.removeSuffix("/").substringAfterLast('/'),
)
}
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
override fun getFilterList() = FilterList(
listOf(
Filter.Header("HINT: Separate search term with comma (,)"),
Filter.Header("String query search doesn't support Sort"),
) + super.getFilterList().list,
// Filters
override fun getFilterList(): FilterList = FilterList(
Filter.Header("Separate tags with commas (,)"),
TagFilter(),
)
class TagFilter : Filter.Text("Tags")
companion object {
const val PREFIX_ID_SEARCH = "id:"
}
}

View File

@ -47,9 +47,6 @@
<data
android:pathPattern="/subject-overview/..*"
android:scheme="https" />
<data
android:pathPattern="/title/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>

View File

@ -1,7 +1,7 @@
ext {
extName = 'Bato.to'
extClass = '.BatoToFactory'
extVersionCode = 36
extVersionCode = 35
isNsfw = true
}

View File

@ -42,27 +42,10 @@ class BatoToUrlActivity : Activity() {
private fun fromBatoTo(pathSegments: MutableList<String>): String? {
return if (pathSegments.size >= 2) {
val path = pathSegments[1] as java.lang.String?
if (path != null) {
var index = -1
for (i in path.indices) {
if (path[i] == '-') {
index = i
break
}
}
val id = if (index == -1) {
path
} else {
path.substring(0, index)
}
val id = pathSegments[1]
"ID:$id"
} else {
null
}
} else {
null
}
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,175 +0,0 @@
package eu.kanade.tachiyomi.extension.all.beauty3600000
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
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.ParsedHttpSource
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class Beauty3600000 : ParsedHttpSource() {
override val baseUrl = "https://3600000.xyz"
override val lang = "all"
override val name = "3600000 Beauty"
override val supportsLatest = false
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
// Latest
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
// Popular
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
thumbnail_url = element.select("a img.ls_lazyimg").attr("file")
title = element.select(".entry-title").text()
setUrlWithoutDomain(element.select(".entry-title > a").attr("abs:href"))
status = SManga.COMPLETED
}
override fun popularMangaNextPageSelector() = ".next"
override fun popularMangaRequest(page: Int) = GET("$baseUrl/page/$page/", headers)
override fun popularMangaSelector() = "#blog-entries > article"
// Search
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = if (filters.isEmpty()) getFilterList() else filters
val tagFilter = filterList.findInstance<TagFilter>()!!
val categoryFilter = filterList.findInstance<CategoryFilter>()!!
var searchQuery = query
val searchPath: String = when {
tagFilter.state.isNotEmpty() -> "$baseUrl/tag/${tagFilter.state}/page/$page/"
categoryFilter.state != 0 -> "$baseUrl/category/${categoryFilter.toUriPart()}/page/$page/"
query.startsWith("tag:") -> {
tagFilter.state = searchQuery.substringAfter("tag:")
searchQuery = ""
"$baseUrl/tag/${tagFilter.state}/page/$page/"
}
else -> "$baseUrl/page/$page/"
}
return when {
searchQuery.isNotEmpty() -> GET(
searchPath.toHttpUrl().newBuilder().apply {
addQueryParameter("s", searchQuery)
}.build(),
headers,
)
else -> GET(searchPath, headers)
}
}
override fun searchMangaSelector() = popularMangaSelector()
// Details
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val main = document.selectFirst("#main")!!
title = main.select(".entry-title").text()
description = main.select(".entry-title").text()
genre = getGenres(document).joinToString(", ")
thumbnail_url = main.select(".entry-content img.ls_lazyimg").attr("file")
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
}
private fun getGenres(element: Element): List<String> {
val genres = mutableListOf<String>()
element.select(".cat-links a").forEach {
genres.add(it.text())
}
element.select(".tags-links a").forEach {
val tag = it.attr("href").toHttpUrl().pathSegments[1]
genres.add("tag:$tag")
}
return genres
}
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.select("link[rel=\"shortlink\"]").attr("href"))
name = "Gallery"
date_upload = getDate(element.select("#main time").attr("datetime"))
}
override fun chapterListSelector() = "html"
// Pages
override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
document.select("noscript").remove()
document.select(".entry-content img").forEachIndexed { i, it ->
val itUrl = it.select("img.ls_lazyimg").attr("file")
pages.add(Page(i, imageUrl = itUrl))
}
return pages
}
override fun imageUrlParse(document: Document): String =
throw UnsupportedOperationException()
// Filters
override fun getFilterList(): FilterList = FilterList(
Filter.Header("NOTE: Only one filter will be applied!"),
Filter.Separator(),
CategoryFilter(),
TagFilter(),
)
open class UriPartFilter(
displayName: String,
private val valuePair: Array<Pair<String, String>>,
) : Filter.Select<String>(displayName, valuePair.map { it.first }.toTypedArray()) {
fun toUriPart() = valuePair[state].second
}
class CategoryFilter : UriPartFilter(
"Category",
arrayOf(
Pair("Any", ""),
Pair("Gravure", "gravure"),
Pair("Aidol", "aidol"),
Pair("Magazine", "magazine"),
Pair("Korea", "korea"),
Pair("Thailand", "thailand"),
Pair("Chinese", "chinese"),
Pair("Japan", "japan"),
Pair("China", "china"),
Pair("Uncategorized", "uncategorized"),
Pair("Magazine", "magazine"),
Pair("Photobook", "photobook"),
Pair("Western", "western"),
),
)
class TagFilter : Filter.Text("Tag")
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
private fun getDate(str: String): Long {
return try {
DATE_FORMAT.parse(str).time
} catch (e: ParseException) {
0L
}
}
companion object {
private val DATE_FORMAT by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
.apply { timeZone = TimeZone.getTimeZone("UTC") }
}
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Comick'
extClass = '.ComickFactory'
extVersionCode = 46
extVersionCode = 42
isNsfw = true
}

View File

@ -21,17 +21,11 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.TimeUnit
import kotlin.math.min
abstract class Comick(
@ -161,31 +155,9 @@ abstract class Comick(
}
override val client = network.client.newBuilder()
.addNetworkInterceptor(::errorInterceptor)
.rateLimit(3, 1, TimeUnit.SECONDS)
.rateLimit(3, 1)
.build()
private fun errorInterceptor(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (
response.isSuccessful ||
"application/json" !in response.header("Content-Type").orEmpty()
) {
return response
}
val error = try {
response.parseAs<Error>()
} catch (_: Exception) {
null
}
error?.run {
throw Exception("$name error $statusCode: $message")
} ?: throw Exception("HTTP error ${response.code}")
}
/** Popular Manga **/
override fun popularMangaRequest(page: Int): Request {
val url = "$apiUrl/v1.0/search?sort=follow&limit=$LIMIT&page=$page&tachiyomi=true"
@ -329,7 +301,7 @@ abstract class Comick(
is TagFilter -> {
if (it.state.isNotEmpty()) {
it.state.split(",").forEach {
addQueryParameter("tags", it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-").replace("'-", "-and-039-").replace("'", "-and-039-"))
addQueryParameter("tags", it.trim())
}
}
}
@ -426,31 +398,13 @@ abstract class Comick(
.substringBefore("/chapters")
.substringAfter(apiUrl)
val currentTimestamp = System.currentTimeMillis()
return chapterListResponse.chapters
.filter {
val publishTime = try {
publishedDateFormat.parse(it.publishedAt)!!.time
} catch (_: ParseException) {
0L
}
val publishedChapter = publishTime <= currentTimestamp
val noGroupBlock = it.groups.map { g -> g.lowercase() }
.intersect(preferences.ignoredGroups)
.isEmpty()
publishedChapter && noGroupBlock
it.groups.map { g -> g.lowercase() }.intersect(preferences.ignoredGroups).isEmpty()
}
.map { it.toSChapter(mangaUrl) }
}
private val publishedDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
override fun getChapterUrl(chapter: SChapter): String {
return "$baseUrl${chapter.url}"
}
@ -499,7 +453,6 @@ abstract class Comick(
companion object {
const val SLUG_SEARCH_PREFIX = "id:"
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
private const val INCLUDE_MU_TAGS_DEFAULT = false

View File

@ -170,8 +170,7 @@ class Chapter(
private val hid: String,
private val lang: String = "",
private val title: String = "",
@SerialName("created_at") private val createdAt: String = "",
@SerialName("publish_at") val publishedAt: String = "",
@SerialName("created_at") val createdAt: String = "",
private val chap: String = "",
private val vol: String = "",
@SerialName("group_name") val groups: List<String> = emptyList(),
@ -198,9 +197,3 @@ class ChapterPageData(
class Page(
val url: String? = null,
)
@Serializable
class Error(
val statusCode: Int,
val message: String,
)

View File

@ -7,7 +7,7 @@ import java.util.Locale
import java.util.TimeZone
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.ENGLISH).apply {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Danbooru'
extClass = '.Danbooru'
extVersionCode = 2
extVersionCode = 1
isNsfw = true
}

View File

@ -47,52 +47,54 @@ class Danbooru : ParsedHttpSource() {
override fun popularMangaSelector(): String =
searchMangaSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/pools/gallery".toHttpUrl().newBuilder()
url.setEncodedQueryParameter("search[category]", "series")
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = Request(
url = "$baseUrl/pools/gallery".toHttpUrl().newBuilder().run {
setEncodedQueryParameter("search[category]", "series")
filters.forEach {
when (it) {
is FilterTags -> if (it.state.isNotBlank()) {
url.addQueryParameter("search[post_tags_match]", it.state)
addQueryParameter("search[post_tags_match]", it.state)
}
is FilterDescription -> if (it.state.isNotBlank()) {
url.addQueryParameter("search[description_matches]", it.state)
addQueryParameter("search[description_matches]", it.state)
}
is FilterIsDeleted -> if (it.state) {
url.addEncodedQueryParameter("search[is_deleted]", "true")
addEncodedQueryParameter("search[is_deleted]", "true")
}
is FilterCategory -> {
url.setEncodedQueryParameter("search[category]", it.selected)
setEncodedQueryParameter("search[category]", it.selected)
}
is FilterOrder -> if (it.selected != null) {
url.addEncodedQueryParameter("search[order]", it.selected)
addEncodedQueryParameter("search[order]", it.selected)
}
else -> throw IllegalStateException("Unrecognized filter")
}
}
url.addEncodedQueryParameter("page", page.toString())
addEncodedQueryParameter("page", page.toString())
if (query.isNotBlank()) {
url.addQueryParameter("search[name_contains]", query)
addQueryParameter("search[name_contains]", query)
}
return GET(url.build(), headers)
}
build()
},
headers = headers,
)
override fun searchMangaSelector(): String =
"article.post-preview"
".post-preview"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
url = element.selectFirst(".post-preview-link")?.attr("href")!!
title = element.selectFirst("div.text-center")?.text() ?: ""
title = element.selectFirst(".desc")?.text() ?: ""
thumbnail_url = element.selectFirst("source")?.attr("srcset")
?.substringAfterLast(',')?.trim()

View File

@ -1,82 +0,0 @@
package eu.kanade.tachiyomi.extension.all.eternalmangas
import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp
import eu.kanade.tachiyomi.multisrc.mangaesp.SeriesDto
import eu.kanade.tachiyomi.multisrc.mangaesp.TopSeriesDto
import eu.kanade.tachiyomi.network.POST
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.util.asJsoup
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import okhttp3.FormBody
import okhttp3.Response
open class EternalMangas(
lang: String,
private val internalLang: String,
) : MangaEsp(
"EternalMangas",
"https://eternalmangas.com",
lang,
) {
override fun popularMangaParse(response: Response): MangasPage {
val body = response.body.string()
val responseData = json.decodeFromString<TopSeriesDto>(body)
val topDaily = responseData.response.topDaily.flatten().map { it.data }
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }
.filter { it.language == internalLang }
.map { it.toSManga() }
return MangasPage(mangas, false)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val responseData = json.decodeFromString<LatestUpdatesDto>(response.body.string())
val mangas = responseData.updates[internalLang]?.flatten()?.map { it.toSManga() } ?: emptyList()
return MangasPage(mangas, false)
}
override fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
val document = response.asJsoup()
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
?: throw Exception(intl["comics_list_error"])
val unescapedJson = jsonString.unescape()
comicsList = json.decodeFromString<List<SeriesDto>>(unescapedJson)
.filter { it.language == internalLang }
.toMutableList()
return parseComicsList(page, query, filters)
}
override fun pageListParse(response: Response): List<Page> {
var document = response.asJsoup()
document.selectFirst("body > form[method=post]")?.let {
val action = it.attr("action")
val inputs = it.select("input")
val form = FormBody.Builder()
inputs.forEach { input ->
form.add(input.attr("name"), input.attr("value"))
}
document = client.newCall(POST(action, headers, form.build())).execute().asJsoup()
}
return document.select("main > img").mapIndexed { i, img ->
Page(i, imageUrl = img.imgAttr())
}
}
@Serializable
class LatestUpdatesDto(
val updates: Map<String, List<List<SeriesDto>>>,
)
}

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.extension.all.eternalmangas
import eu.kanade.tachiyomi.source.SourceFactory
class EternalMangasFactory : SourceFactory {
override fun createSources() = listOf(
EternalMangasES(),
EternalMangasEN(),
EternalMangasPTBR(),
)
}
class EternalMangasES : EternalMangas("es", "es")
class EternalMangasEN : EternalMangas("en", "en")
class EternalMangasPTBR : EternalMangas("pt-BR", "pt")

View File

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

View File

@ -1,103 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hentaifox
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
import eu.kanade.tachiyomi.multisrc.galleryadults.toDate
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.HttpUrl
import org.jsoup.nodes.Element
class HentaiFox(
lang: String = "all",
override val mangaLang: String = LANGUAGE_MULTI,
) : GalleryAdults(
"HentaiFox",
"https://hentaifox.com",
lang = lang,
) {
override val supportsLatest = mangaLang.isNotBlank()
private val languages: List<Pair<String, String>> = listOf(
Pair(LANGUAGE_ENGLISH, "1"),
Pair(LANGUAGE_TRANSLATED, "2"),
Pair(LANGUAGE_JAPANESE, "5"),
Pair(LANGUAGE_CHINESE, "6"),
Pair(LANGUAGE_KOREAN, "11"),
)
private val langCode = languages.firstOrNull { lang -> lang.first == mangaLang }?.second
override fun Element.mangaLang() = attr("data-languages")
.split(' ').let {
when {
it.contains(langCode) -> mangaLang
// search result doesn't have "data-languages" which will return a list with 1 blank element
it.size > 1 || (it.size == 1 && it.first().isNotBlank()) -> "other"
// if we don't know which language to filter then set to mangaLang to not filter at all
else -> mangaLang
}
}
override val useShortTitlePreference = false
override fun Element.mangaTitle(selector: String): String? = mangaFullTitle(selector)
override fun Element.getInfo(tag: String): String {
return select("ul.${tag.lowercase()} a")
.joinToString {
val name = it.ownText()
if (tag.contains(regexTag)) {
genres[name] = it.attr("href")
.removeSuffix("/").substringAfterLast('/')
}
listOf(
name,
it.select(".split_tag").text()
.removePrefix("| ")
.trim(),
)
.filter { s -> s.isNotBlank() }
.joinToString()
}
}
override fun Element.getTime(): Long =
selectFirst(".pages:contains(Posted:)")?.ownText()
?.removePrefix("Posted: ")
.toDate(simpleDateFormat)
override fun HttpUrl.Builder.addPageUri(page: Int): HttpUrl.Builder {
val url = toString()
when {
url == "$baseUrl/" && page == 2 ->
addPathSegments("page/$page")
url.contains('?') ->
addQueryParameter("page", page.toString())
else ->
addPathSegments("pag/$page")
}
addPathSegment("") // trailing slash (/)
return this
}
/**
* Convert space( ) typed in search-box into plus(+) in URL. Then:
* - ignore the word preceding by a special character (e.g. 'school-girl' will ignore 'girl')
* => replace to plus(+),
* - use plus(+) for separate terms, as AND condition.
* - use double quote(") to search for exact match.
*/
override fun buildQueryString(tags: List<String>, query: String): String {
val regexSpecialCharacters = Regex("""[^a-zA-Z0-9"]+(?=[a-zA-Z0-9"])""")
return (tags + query + mangaLang).filterNot { it.isBlank() }.joinToString("+") {
it.trim().replace(regexSpecialCharacters, "+")
}
}
override val favoritePath = "includes/user_favs.php"
override val pagesRequest = "includes/thumbs_loader.php"
override fun getFilterList() = FilterList(
listOf(
Filter.Header("HINT: Use double quote (\") for exact match"),
) + super.getFilterList().list,
)
}

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hentaifox
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class HentaiFoxFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
HentaiFox("en", GalleryAdults.LANGUAGE_ENGLISH),
HentaiFox("ja", GalleryAdults.LANGUAGE_JAPANESE),
HentaiFox("zh", GalleryAdults.LANGUAGE_CHINESE),
HentaiFox("ko", GalleryAdults.LANGUAGE_KOREAN),
HentaiFox("all", GalleryAdults.LANGUAGE_MULTI),
)
}

View File

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

View File

@ -1,81 +0,0 @@
package eu.kanade.tachiyomi.extension.all.hitomi
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
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(" ", "_")}")
}
}
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)
}
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,12 +1,8 @@
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.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@ -26,8 +22,6 @@ 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
@ -41,7 +35,7 @@ import kotlin.math.min
class Hitomi(
override val lang: String,
private val nozomiLang: String,
) : ConfigurableSource, HttpSource() {
) : HttpSource() {
override val name = "Hitomi"
@ -57,12 +51,6 @@ 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)
@ -88,13 +76,11 @@ 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${parsedFilter.first}".trim(),
parsedFilter.second,
query.trim(),
filters.filterIsInstance<SortFilter>().firstOrNull()?.state == 1,
nozomiLang,
).toList()
}
@ -107,7 +93,11 @@ class Hitomi(
}
}
override fun getFilterList(): FilterList = getFilterListInternal()
private class SortFilter : Filter.Select<String>("Sort By", arrayOf("Updated", "Popularity"))
override fun getFilterList(): FilterList {
return FilterList(SortFilter())
}
private fun Int.nextPageRange(): LongRange {
val byteOffset = ((this - 1) * 25) * 4L
@ -127,7 +117,7 @@ class Hitomi(
private suspend fun hitomiSearch(
query: String,
order: OrderType,
sortByPopularity: Boolean = false,
language: String = "all",
): Set<Int> =
coroutineScope {
@ -136,6 +126,9 @@ class Hitomi(
.replace(Regex("""^\?"""), "")
.lowercase()
.split(Regex("\\s+"))
.map {
it.replace('_', ' ')
}
val positiveTerms = LinkedList<String>()
val negativeTerms = LinkedList<String>()
@ -151,7 +144,7 @@ class Hitomi(
val positiveResults = positiveTerms.map {
async {
runCatching {
getGalleryIDsForQuery(it, language, order)
getGalleryIDsForQuery(it, language)
}.getOrDefault(emptySet())
}
}
@ -159,13 +152,14 @@ class Hitomi(
val negativeResults = negativeTerms.map {
async {
runCatching {
getGalleryIDsForQuery(it, language, order)
getGalleryIDsForQuery(it, language)
}.getOrDefault(emptySet())
}
}
val results = when {
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(order.first, order.second, language)
sortByPopularity -> getGalleryIDsFromNozomi(null, "popular", language)
positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(null, "index", language)
else -> emptySet()
}.toMutableSet()
@ -197,7 +191,6 @@ class Hitomi(
private suspend fun getGalleryIDsForQuery(
query: String,
language: String = "all",
order: OrderType,
): Set<Int> {
query.replace("_", " ").let {
if (it.indexOf(':') > -1) {
@ -220,20 +213,6 @@ 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)
}
@ -450,7 +429,7 @@ class Hitomi(
url = galleryurl
author = groups?.joinToString { it.formatted }
artist = artists?.joinToString { it.formatted }
genre = tags?.joinToString { it.getFormatted(iconified) }
genre = tags?.joinToString { it.formatted }
thumbnail_url = files.first().let {
val hash = it.hash
val imageId = imageIdFromHash(hash)
@ -465,8 +444,7 @@ class Hitomi(
parodys?.joinToString { it.formatted }?.let {
append("Parodies: ", it, "\n")
}
append("Pages: ", files.size, "\n")
append("Language: ", language)
append("Pages: ", files.size)
}
status = SManga.COMPLETED
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
@ -620,28 +598,9 @@ 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

@ -9,7 +9,6 @@ data class Gallery(
val title: String,
val date: String,
val type: String,
val language: String,
val tags: List<Tag>?,
val artists: List<Artist>?,
val groups: List<Group>?,
@ -29,10 +28,10 @@ data class Tag(
val male: JsonPrimitive?,
val tag: String,
) {
fun getFormatted(iconified: Boolean) = if (female?.content == "1") {
tag.toCamelCase() + if (iconified) "" else " (Female)"
val formatted get() = if (female?.content == "1") {
"${tag.toCamelCase()} (Female)"
} else if (male?.content == "1") {
tag.toCamelCase() + if (iconified) "" else " (Male)"
"${tag.toCamelCase()} (Male)"
} else {
tag.toCamelCase()
}

View File

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".es.tumanhwas.TuManhwasUrlActivity"
android:name=".all.imhentai.IMHentaiUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
@ -13,8 +14,8 @@
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="tumanhwas.com"
android:pathPattern="/manga/..*"
android:host="imhentai.xxx"
android:pathPattern="/gallery/..*"
android:scheme="https" />
</intent-filter>
</activity>

View File

@ -1,9 +1,7 @@
ext {
extName = 'IMHentai'
extClass = '.IMHentaiFactory'
themePkg = 'galleryadults'
baseUrl = 'https://imhentai.xxx'
overrideVersionCode = 15
extVersionCode = 14
isNsfw = true
}

View File

@ -1,33 +1,33 @@
package eu.kanade.tachiyomi.extension.all.imhentai
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
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 kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.IOException
class IMHentai(
lang: String = "all",
override val mangaLang: String = LANGUAGE_MULTI,
) : GalleryAdults(
"IMHentai",
"https://imhentai.xxx",
lang = lang,
) {
override val supportsLatest = true
override val useIntermediateSearch: Boolean = true
override val supportAdvancedSearch: Boolean = true
override val supportSpeechless: Boolean = true
class IMHentai(override val lang: String, private val imhLang: String) : ParsedHttpSource() {
override fun Element.mangaLang() =
select("a:has(.thumb_flag)").attr("href")
.removeSuffix("/").substringAfterLast("/")
.let {
// Include Speechless in search results
if (it == LANGUAGE_SPEECHLESS) mangaLang else it
}
override val baseUrl: String = "https://imhentai.xxx"
override val name: String = "IMHentai"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
.newBuilder()
@ -57,32 +57,271 @@ class IMHentai(
},
).build()
/* Details */
override fun Element.getInfo(tag: String): String {
return select("li:has(.tags_text:contains($tag:)) a.tag")
.joinToString {
val name = it.ownText()
if (tag.contains(regexTag)) {
genres[name] = it.attr("href")
.removeSuffix("/").substringAfterLast('/')
// Popular
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
thumbnail_url = element.selectFirst(".inner_thumb img")?.let {
it.absUrl(if (it.hasAttr("data-src")) "data-src" else "src")
}
with(element.select(".caption a")) {
url = this.attr("href")
title = this.text()
}
}
}
override fun popularMangaNextPageSelector(): String = ".pagination li a:contains(Next):not([tabindex])"
override fun popularMangaSelector(): String = ".thumbs_container .thumb"
override fun popularMangaRequest(page: Int): Request = searchMangaRequest(page, "", getFilterList(SORT_ORDER_POPULAR))
// Latest
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest(page, "", getFilterList(SORT_ORDER_LATEST))
override fun latestUpdatesSelector(): String = popularMangaSelector()
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith("id:")) {
val id = query.substringAfter("id:")
return client.newCall(GET("$baseUrl/gallery/$id/"))
.asObservableSuccess()
.map { response ->
val manga = mangaDetailsParse(response)
manga.url = "/gallery/$id/"
MangasPage(listOf(manga), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
private fun toBinary(boolean: Boolean) = if (boolean) "1" else "0"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (filters.any { it is LanguageFilters && it.state.any { it.name == LANGUAGE_SPEECHLESS && it.state } }) { // edge case for language = speechless
val url = "$baseUrl/language/speechless/".toHttpUrl().newBuilder()
if ((if (filters.isEmpty()) getFilterList() else filters).filterIsInstance<SortOrderFilter>()[0].state == 0) {
url.addPathSegment("popular")
}
return GET(url.build())
} else {
val url = "$baseUrl/search".toHttpUrl().newBuilder()
.addQueryParameter("key", query)
.addQueryParameter("page", page.toString())
.addQueryParameter(getLanguageURIByName(imhLang).uri, toBinary(true)) // main language always enabled
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
is LanguageFilters -> {
filter.state.forEach {
url.addQueryParameter(it.uri, toBinary(it.state))
}
}
is CategoryFilters -> {
filter.state.forEach {
url.addQueryParameter(it.uri, toBinary(it.state))
}
}
is SortOrderFilter -> {
getSortOrderURIs().forEachIndexed { index, pair ->
url.addQueryParameter(pair.second, toBinary(filter.state == index))
}
}
else -> {}
}
}
return GET(url.build())
}
}
override fun searchMangaSelector(): String = popularMangaSelector()
// Details
private fun Elements.csvText(splitTagSeparator: String = ", "): String {
return this.joinToString {
listOf(
name,
it.ownText(),
it.select(".split_tag").text()
.trim()
.removePrefix("| "),
)
.filter { s -> s.isNotBlank() }
.joinToString()
.filter { s -> !s.isNullOrBlank() }
.joinToString(splitTagSeparator)
}
}
override fun Element.getCover() =
selectFirst(".left_cover img")?.imgAttr()
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
title = document.selectFirst("div.right_details > h1")!!.text()
override val mangaDetailInfoSelector = ".gallery_first"
thumbnail_url = document.selectFirst("div.left_cover img")?.let {
it.absUrl(if (it.hasAttr("data-src")) "data-src" else "src")
}
/* Pages */
override val thumbnailSelector = ".gthumb"
override val pageUri = "view"
val mangaInfoElement = document.select(".galleries_info")
val infoMap = mangaInfoElement.select("li:not(.pages)").associate {
it.select("span.tags_text").text().removeSuffix(":") to it.select(".tag")
}
artist = infoMap["Artists"]?.csvText(" | ")
author = artist
genre = infoMap["Tags"]?.csvText()
status = SManga.COMPLETED
val pages = mangaInfoElement.select("li.pages").text().substringAfter("Pages: ")
val altTitle = document.select(".subtitle").text().ifBlank { null }
description = listOf(
"Parodies",
"Characters",
"Groups",
"Languages",
"Category",
).map { it to infoMap[it]?.csvText() }
.let { listOf(Pair("Alternate Title", altTitle)) + it + listOf(Pair("Pages", pages)) }
.filter { !it.second.isNullOrEmpty() }
.joinToString("\n\n") { "${it.first}:\n${it.second}" }
}
// Chapters
override fun chapterListParse(response: Response): List<SChapter> {
return listOf(
SChapter.create().apply {
setUrlWithoutDomain(response.request.url.toString().replace("gallery", "view") + "1")
name = "Chapter"
chapter_number = 1f
},
)
}
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
override fun chapterListSelector(): String = throw UnsupportedOperationException()
// Pages
private val json: Json by injectLazy()
override fun pageListParse(document: Document): List<Page> {
val imageDir = document.select("#image_dir").`val`()
val galleryId = document.select("#gallery_id").`val`()
val uId = document.select("#u_id").`val`().toInt()
val randomServer = when (uId) {
in 1..274825 -> "m1.imhentai.xxx"
in 274826..403818 -> "m2.imhentai.xxx"
in 403819..527143 -> "m3.imhentai.xxx"
in 527144..632481 -> "m4.imhentai.xxx"
in 632482..816010 -> "m5.imhentai.xxx"
in 816011..970098 -> "m6.imhentai.xxx"
in 970099..1121113 -> "m7.imhentai.xxx"
else -> "m8.imhentai.xxx"
}
val images = json.parseToJsonElement(
document.selectFirst("script:containsData(var g_th)")!!.data()
.substringAfter("$.parseJSON('").substringBefore("');").trim(),
).jsonObject
val pages = mutableListOf<Page>()
for (image in images) {
val iext = image.value.toString().replace("\"", "").split(",")[0]
val iextPr = when (iext) {
"p" -> "png"
"b" -> "bmp"
"g" -> "gif"
else -> "jpg"
}
pages.add(Page(image.key.toInt() - 1, "", "https://$randomServer/$imageDir/$galleryId/${image.key}.$iextPr"))
}
return pages
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
// Filters
private class SortOrderFilter(sortOrderURIs: List<Pair<String, String>>, state: Int) :
Filter.Select<String>("Sort By", sortOrderURIs.map { it.first }.toTypedArray(), state)
private open class SearchFlagFilter(name: String, val uri: String, state: Boolean = true) : Filter.CheckBox(name, state)
private class LanguageFilter(name: String, uri: String = name) : SearchFlagFilter(name, uri, false)
private class LanguageFilters(flags: List<LanguageFilter>) : Filter.Group<LanguageFilter>("Other Languages", flags)
private class CategoryFilters(flags: List<SearchFlagFilter>) : Filter.Group<SearchFlagFilter>("Categories", flags)
override fun getFilterList() = getFilterList(SORT_ORDER_DEFAULT)
private fun getFilterList(sortOrderState: Int) = FilterList(
SortOrderFilter(getSortOrderURIs(), sortOrderState),
CategoryFilters(getCategoryURIs()),
LanguageFilters(getLanguageURIs().filter { it.name != imhLang }), // exclude main lang
Filter.Header("Speechless language: ignores all filters except \"Popular\" and \"Latest\" in Sorting Filter"),
)
private fun getCategoryURIs() = listOf(
SearchFlagFilter("Manga", "manga"),
SearchFlagFilter("Doujinshi", "doujinshi"),
SearchFlagFilter("Western", "western"),
SearchFlagFilter("Image Set", "imageset"),
SearchFlagFilter("Artist CG", "artistcg"),
SearchFlagFilter("Game CG", "gamecg"),
)
// update sort order indices in companion object if order is changed
private fun getSortOrderURIs() = listOf(
Pair("Popular", "pp"),
Pair("Latest", "lt"),
Pair("Downloads", "dl"),
Pair("Top Rated", "tr"),
)
private fun getLanguageURIs() = listOf(
LanguageFilter(LANGUAGE_ENGLISH, "en"),
LanguageFilter(LANGUAGE_JAPANESE, "jp"),
LanguageFilter(LANGUAGE_SPANISH, "es"),
LanguageFilter(LANGUAGE_FRENCH, "fr"),
LanguageFilter(LANGUAGE_KOREAN, "kr"),
LanguageFilter(LANGUAGE_GERMAN, "de"),
LanguageFilter(LANGUAGE_RUSSIAN, "ru"),
LanguageFilter(LANGUAGE_SPEECHLESS, ""),
)
private fun getLanguageURIByName(name: String): LanguageFilter {
return getLanguageURIs().first { it.name == name }
}
companion object {
// references to sort order indices
private const val SORT_ORDER_POPULAR = 0
private const val SORT_ORDER_LATEST = 1
private const val SORT_ORDER_DEFAULT = SORT_ORDER_POPULAR
// references to be used in factory
const val LANGUAGE_ENGLISH = "English"
const val LANGUAGE_JAPANESE = "Japanese"
const val LANGUAGE_SPANISH = "Spanish"
const val LANGUAGE_FRENCH = "French"
const val LANGUAGE_KOREAN = "Korean"
const val LANGUAGE_GERMAN = "German"
const val LANGUAGE_RUSSIAN = "Russian"
const val LANGUAGE_SPEECHLESS = "Speechless"
}
}

View File

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

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.multisrc.galleryadults
package eu.kanade.tachiyomi.extension.all.imhentai
import android.app.Activity
import android.content.ActivityNotFoundException
@ -8,9 +8,10 @@ import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://<domain>/g/xxxxxx intents and redirects them to main app process.
* Springboard that accepts https://imhentai.xxx/gallery/xxxxxx intents and redirects them to
* the main Tachiyomi process.
*/
class GalleryAdultsUrlActivity : Activity() {
class IMHentaiUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
@ -18,17 +19,17 @@ class GalleryAdultsUrlActivity : Activity() {
val id = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${GalleryAdults.PREFIX_ID_SEARCH}$id")
putExtra("query", "id:$id")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("GalleryAdultsUrl", e.toString())
Log.e("IMHentaiUrlActivity", e.toString())
}
} else {
Log.e("GalleryAdultsUrl", "could not parse uri from intent $intent")
Log.e("IMHentaiUrlActivity", "could not parse uri from intent $intent")
}
finish()

View File

@ -1,9 +0,0 @@
ext {
extName = 'Manga18Me'
extClass = '.M18MFactory'
extVersionCode = 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.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,84 +0,0 @@
package eu.kanade.tachiyomi.extension.all.manga18me
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
fun getFilters(): FilterList {
return FilterList(
Filter.Header(name = "The filter is ignored when using text search."),
GenreFilter("Genre", getGenresList),
SortFilter("Sort", getSortsList),
RawFilter("Raw"),
CompletedFilter("Completed"),
)
}
/** Filters **/
internal class GenreFilter(name: String, genreList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, genreList, state)
internal class SortFilter(name: String, sortList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, sortList, state)
internal class CompletedFilter(name: String) : CheckBoxFilter(name)
internal class RawFilter(name: String) : CheckBoxFilter(name)
internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(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
}
/** Filters Data **/
private val getGenresList: List<Pair<String, String>> = listOf(
Pair("Manga", "manga"),
Pair("Drama", "drama"),
Pair("Mature", "mature"),
Pair("Romance", "romance"),
Pair("Adult", "adult"),
Pair("Hentai", "hentai"),
Pair("Comedy", "comedy"),
Pair("Ecchi", "ecchi"),
Pair("School Life", "school-life"),
Pair("Shounen", "shounen"),
Pair("Slice of Life", "slice-of-life"),
Pair("Seinen", "seinen"),
Pair("Yuri", "yuri"),
Pair("Action", "action"),
Pair("Fantasy", "fantasy"),
Pair("Harem", "harem"),
Pair("Supernatural", "supernatural"),
Pair("Sci-Fi", "sci-fi"),
Pair("Isekai", "isekai"),
Pair("Shoujo", "shoujo"),
Pair("Horror", "horror"),
Pair("Psychological", "psychological"),
Pair("Smut", "smut"),
Pair("Tragedy", "tragedy"),
Pair("Raw", "raw"),
Pair("Historical", "historical"),
Pair("Adventure", "adventure"),
Pair("Martial Arts", "martial-arts"),
Pair("Manhwa", "manhwa"),
Pair("Manhua", "manhua"),
Pair("Mystery", "mystery"),
Pair("BL", "bl"),
Pair("Yaoi", "yaoi"),
Pair("Gender Bender", "gender-bender"),
Pair("Thriller", "thriller"),
Pair("Josei", "josei"),
Pair("Sports", "sports"),
Pair("GL", "gl"),
Pair("Family", "family"),
Pair("Magic", "magic"),
)
private val getSortsList: List<Pair<String, String>> = listOf(
Pair("Latest", "latest"),
Pair("A-Z", "alphabet"),
Pair("Rating", "rating"),
Pair("Trending", "trending"),
)

View File

@ -1,12 +0,0 @@
package eu.kanade.tachiyomi.extension.all.manga18me
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class M18MFactory : SourceFactory {
override fun createSources(): List<Source> =
listOf(
Manga18Me("all"),
Manga18Me("en"),
)
}

View File

@ -1,194 +0,0 @@
package eu.kanade.tachiyomi.extension.all.manga18me
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.lang.Exception
import java.text.SimpleDateFormat
import java.util.Locale
open class Manga18Me(override val lang: String) : ParsedHttpSource() {
override val name = "Manga18.me"
override val baseUrl = "https://manga18.me"
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = Headers.Builder().apply {
add("Referer", "$baseUrl/")
}
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/manga/$page?orderby=trending", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val entries = document.select(popularMangaSelector())
val hasNextPage = document.selectFirst(popularMangaNextPageSelector()) != null
if (lang == "en") {
val searchText = document.selectFirst("div.section-heading h1")?.text() ?: ""
val raw = document.selectFirst("div.canonical")?.attr("href") ?: ""
return MangasPage(
entries
.filter { it ->
val title = it.selectFirst("div.item-thumb.wleft a")?.attr("href") ?: ""
searchText.lowercase().contains("raw") ||
raw.contains("raw") ||
!title.contains("raw")
}
.map(::popularMangaFromElement),
hasNextPage,
)
}
return MangasPage(entries.map(::popularMangaFromElement), hasNextPage)
}
override fun popularMangaSelector() = "div.page-item-detail"
override fun popularMangaNextPageSelector() = ".next"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
title = element.selectFirst("div.item-thumb.wleft a")!!.attr("title")
thumbnail_url = element.selectFirst("img")?.absUrl("src")
}
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/manga/$page?orderby=latest", headers)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder().apply {
if (query.isEmpty()) {
var completed = false
var raw = false
var genre = ""
filters.forEach {
when (it) {
is GenreFilter -> {
genre = it.getValue()
}
is CompletedFilter -> {
completed = it.state
}
is RawFilter -> {
raw = it.state
}
is SortFilter -> {
addQueryParameter("orderby", it.getValue())
}
else -> {}
}
}
if (raw) {
addPathSegment("raw")
} else if (completed) {
addPathSegment("completed")
} else {
if (genre != "manga") addPathSegment("genre")
addPathSegment(genre)
}
addPathSegment(page.toString())
} else {
addPathSegment("search")
addQueryParameter("q", query)
addQueryParameter("page", page.toString())
}
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun getFilterList() = getFilters()
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val info = document.selectFirst("div.post_content")!!
title = document.select("div.post-title.wleft > h1").text()
description = buildString {
document.select("div.ss-manga > p")
.eachText().onEach {
append(it.trim())
append("\n\n")
}
info.selectFirst("div.post-content_item.wleft:contains(Alternative) div.summary-content")
?.text()
?.takeIf { it != "Updating" && it.isNotEmpty() }
?.let {
append("Alternative Names:\n")
append(it.trim())
}
}
status = when (info.select("div.post-content_item.wleft:contains(Status) div.summary-content").text()) {
"Ongoing" -> SManga.ONGOING
"Completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
author = info.selectFirst("div.href-content.artist-content > a")?.text()?.takeIf { it != "Updating" }
artist = info.selectFirst("div.href-content.artist-content > a")?.text()?.takeIf { it != "Updating" }
genre = info.select("div.href-content.genres-content > a[href*=/manga-list/]").eachText().joinToString()
thumbnail_url = document.selectFirst("div.summary_image > img")?.absUrl("src")
}
override fun chapterListSelector() = "ul.row-content-chapter.wleft .a-h.wleft"
private val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale.ENGLISH)
override fun chapterFromElement(element: Element) = SChapter.create().apply {
element.selectFirst("a")!!.run {
setUrlWithoutDomain(absUrl("href"))
name = text()
}
date_upload = try {
dateFormat.parse(element.selectFirst("span")!!.text())!!.time
} catch (_: Exception) {
0L
}
}
override fun pageListParse(document: Document): List<Page> {
val contents = document.select("div.read-content.wleft img")
if (contents.isEmpty()) {
throw Exception("Unable to find script with image data")
}
return contents.mapIndexed { idx, image ->
val imageUrl = image.attr("src")
Page(idx, imageUrl = imageUrl)
}
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'MangaDex'
extClass = '.MangaDexFactory'
extVersionCode = 193
extVersionCode = 192
isNsfw = true
}

View File

@ -10,7 +10,6 @@ import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.AppInfo
import eu.kanade.tachiyomi.extension.BuildConfig
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateDto
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AggregateVolume
import eu.kanade.tachiyomi.extension.all.mangadex.dto.AtHomeDto
@ -63,12 +62,10 @@ abstract class MangaDex(final override val lang: String, private val dexLang: St
final override fun headersBuilder(): Headers.Builder {
val extraHeader = "Android/${Build.VERSION.RELEASE} " +
"Tachiyomi/${AppInfo.getVersionName()} " +
"MangaDex/1.4.${BuildConfig.VERSION_CODE} " +
"Keiyoushi"
"MangaDex/1.4.190"
val builder = super.headersBuilder().apply {
set("Referer", "$baseUrl/")
set("Origin", baseUrl)
set("Extra", extraHeader)
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.Manhwa18CcFactory'
themePkg = 'madara'
baseUrl = 'https://manhwa18.cc'
overrideVersionCode = 5
overrideVersionCode = 4
isNsfw = true
}

View File

@ -47,7 +47,7 @@ abstract class Manhwa18Cc(
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/webtoons/$page?orderby=trending")
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/webtoons/$page")
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/webtoons/$page?orderby=latest")
override fun searchMangaSelector() = popularMangaSelector()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,24 +0,0 @@
package eu.kanade.tachiyomi.extension.all.manhwaclubnet
import eu.kanade.tachiyomi.multisrc.madara.Madara
import eu.kanade.tachiyomi.source.model.SChapter
import okhttp3.Response
class ManhwaClubNet(lang: String) : Madara(
"ManhwaClub.net",
"https://manhwaclub.net",
lang,
) {
override val useLoadMoreRequest = LoadMoreStrategy.Never
override val useNewChapterEndpoint = false
override fun chapterListParse(response: Response): List<SChapter> {
val chapters = super.chapterListParse(response)
return when (lang) {
"en" -> chapters.filterNot { it.name.endsWith(" raw") }
"ko" -> chapters.filter { it.name.endsWith(" raw") }
else -> emptyList()
}
}
}

View File

@ -1,11 +0,0 @@
package eu.kanade.tachiyomi.extension.all.manhwaclubnet
import eu.kanade.tachiyomi.source.SourceFactory
class ManhwaClubNetFactory : SourceFactory {
// Font for icon: Cooper BT Std Black Headline
override fun createSources() = listOf(
ManhwaClubNet("en"),
ManhwaClubNet("ko"),
)
}

View File

@ -2,8 +2,8 @@ ext {
extName = 'Miau Scan'
extClass = '.MiauScanFactory'
themePkg = 'mangathemesia'
baseUrl = 'https://lectormiau.com'
overrideVersionCode = 4
baseUrl = 'https://miauvisor.org'
overrideVersionCode = 3
}
apply from: "$rootDir/common.gradle"

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