diff --git a/src/en/anchira/AndroidManifest.xml b/src/en/anchira/AndroidManifest.xml deleted file mode 100644 index 34d34709a..000000000 --- a/src/en/anchira/AndroidManifest.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/src/en/anchira/build.gradle b/src/en/anchira/build.gradle deleted file mode 100644 index 18ccc6457..000000000 --- a/src/en/anchira/build.gradle +++ /dev/null @@ -1,8 +0,0 @@ -ext { - extName = 'Anchira' - extClass = '.Anchira' - extVersionCode = 14 - isNsfw = true -} - -apply from: "$rootDir/common.gradle" diff --git a/src/en/anchira/res/mipmap-hdpi/ic_launcher.png b/src/en/anchira/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 62a1189fb..000000000 Binary files a/src/en/anchira/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/anchira/res/mipmap-mdpi/ic_launcher.png b/src/en/anchira/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index d8705d271..000000000 Binary files a/src/en/anchira/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/anchira/res/mipmap-xhdpi/ic_launcher.png b/src/en/anchira/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index b557c6b9d..000000000 Binary files a/src/en/anchira/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/anchira/res/mipmap-xxhdpi/ic_launcher.png b/src/en/anchira/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 8de0cb92e..000000000 Binary files a/src/en/anchira/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/anchira/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/anchira/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 63f8dd15f..000000000 Binary files a/src/en/anchira/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/Anchira.kt b/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/Anchira.kt deleted file mode 100644 index 785203e6e..000000000 --- a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/Anchira.kt +++ /dev/null @@ -1,477 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.anchira - -import android.annotation.SuppressLint -import android.app.Application -import android.content.SharedPreferences -import androidx.preference.ListPreference -import androidx.preference.PreferenceScreen -import androidx.preference.SwitchPreferenceCompat -import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.createChapter -import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.getCdn -import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.getPathFromUrl -import eu.kanade.tachiyomi.extension.en.anchira.AnchiraHelper.prepareTags -import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.asObservableSuccess -import eu.kanade.tachiyomi.network.interceptor.rateLimit -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.HttpSource -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromStream -import okhttp3.HttpUrl -import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get -import java.io.IOException -import java.util.concurrent.TimeUnit -import kotlin.math.ceil -import kotlin.math.min - -class Anchira : HttpSource(), ConfigurableSource { - override val name = "Anchira" - - override val baseUrl = "https://anchira.to" - - private val apiUrl = baseUrl.replace("://", "://api.") - - private val libraryUrl = "$apiUrl/library" - - private val cdnUrl = "https://kisakisexo.xyz" - - override val lang = "en" - - override val supportsLatest = true - - override val client: OkHttpClient = network.cloudflareClient.newBuilder() - .rateLimit(3, 1, TimeUnit.SECONDS) - .addInterceptor { resampledInterceptor(it) } - .build() - - private val json = Json { ignoreUnknownKeys = true } - - private val preferences: SharedPreferences by lazy { - Injekt.get().getSharedPreferences("source_$id", 0x0000) - } - - override fun headersBuilder() = super.headersBuilder() - .add("Referer", "$baseUrl/") - .add("Origin", baseUrl) - - // Latest - - override fun latestUpdatesRequest(page: Int) = GET("$libraryUrl?page=$page", headers) - - override fun latestUpdatesParse(response: Response): MangasPage { - // Ugly but it works - anchiraData.isNotEmpty() - - val data = json.decodeFromString(response.body.string()) - - return MangasPage( - data.entries.map { - SManga.create().apply { - url = "/g/${it.id}/${it.key}" - title = it.title - thumbnail_url = "$cdnUrl/${it.id}/${it.key}/m/${it.cover?.name}" - val art = it.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name } - .ifEmpty { null } - artist = art - author = it.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name } - .ifEmpty { art } - genre = prepareTags(it.tags, preferences.useTagGrouping) - update_strategy = UpdateStrategy.ONLY_FETCH_ONCE - status = SManga.COMPLETED - } - }.toList(), - data.page * data.limit < data.total, - ) - } - - // Popular - - override fun popularMangaRequest(page: Int) = GET("$libraryUrl?sort=32&page=$page", headers) - - override fun popularMangaParse(response: Response) = latestUpdatesParse(response) - - // Search - - override fun fetchSearchManga( - page: Int, - query: String, - filters: FilterList, - ): Observable { - return if (query.startsWith(SLUG_SEARCH_PREFIX)) { - // url deep link - val idKey = query.substringAfter(SLUG_SEARCH_PREFIX) - val manga = SManga.create().apply { this.url = "/g/$idKey" } - fetchMangaDetails(manga).map { - MangasPage(listOf(it), false) - } - } else if (query.startsWith(SLUG_BUNDLE_PREFIX)) { - // bundle entries as chapters - val url = applyFilters( - page, - query.substringAfter(SLUG_BUNDLE_PREFIX), - filters, - ).removeAllQueryParameters("page") - val manga = SManga.create() - .apply { this.url = "?${url.build().query}" } - fetchMangaDetails(manga).map { - MangasPage(listOf(it), false) - } - } else { - // regular filtering without text search - client.newCall(searchMangaRequest(page, query, filters)) - .asObservableSuccess() - .map(::searchMangaParse) - } - } - - override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = - GET(applyFilters(page, query, filters).build(), headers) - - private fun applyFilters(page: Int, query: String, filters: FilterList): HttpUrl.Builder { - val filterList = if (filters.isEmpty()) getFilterList() else filters - val trendingFilter = filterList.findInstance() - val sortTrendingFilter = filters.findInstance() - var url = libraryUrl.toHttpUrl().newBuilder() - - if (trendingFilter?.state == true) { - val interval = when (sortTrendingFilter?.state) { - 1 -> "3" - else -> "" - } - - if (interval.isNotBlank()) url.setQueryParameter("interval", interval) - - url = url.toString().replace("library", "trending").toHttpUrl() - .newBuilder() - } else { - if (query.isNotBlank()) { - url.setQueryParameter("s", query) - } - - filters.forEach { filter -> - when (filter) { - is CategoryGroup -> { - var sum = 0 - - filter.state.forEach { category -> - when (category.name) { - "Manga" -> if (category.state) sum = sum or 1 - "Doujinshi" -> if (category.state) sum = sum or 2 - "Illustration" -> if (category.state) sum = sum or 4 - } - } - - if (sum > 0) url.setQueryParameter("cat", sum.toString()) - } - - is SortFilter -> { - val sort = when (filter.state?.index) { - 0 -> "1" - 1 -> "2" - 2 -> "4" - 4 -> "32" - else -> "" - } - - if (sort.isNotEmpty()) url.setQueryParameter("sort", sort) - if (filter.state?.ascending == true) url.setQueryParameter("order", "1") - } - - is FavoritesFilter -> { - if (filter.state) { - if (!isLoggedIn()) { - throw IOException("No login cookie found") - } - - url = url.toString().replace("library", "user/favorites").toHttpUrl() - .newBuilder() - } - } - - else -> {} - } - } - } - - if (page > 1) { - url.setQueryParameter("page", page.toString()) - } - - return url - } - - override fun searchMangaParse(response: Response) = latestUpdatesParse(response) - - // Details - - override fun mangaDetailsRequest(manga: SManga): Request { - return if (manga.url.startsWith("?")) { - GET(libraryUrl + manga.url, headers) - } else { - GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers) - } - } - - override fun mangaDetailsParse(response: Response): SManga { - return if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) { - val manga = latestUpdatesParse(response).mangas.first() - val query = response.request.url.queryParameter("s") - val cleanTitle = CHAPTER_SUFFIX_RE.replace(manga.title, "").trim() - manga.apply { - url = "?${response.request.url.query}" - description = "Bundled from $query" - title = "[Bundle] $cleanTitle" - update_strategy = UpdateStrategy.ALWAYS_UPDATE - } - } else { - val data = json.decodeFromString(response.body.string()) - - SManga.create().apply { - url = "/g/${data.id}/${data.key}" - title = data.title - thumbnail_url = - "$cdnUrl/${data.id}/${data.key}/l/${data.images[data.thumbnailIndex].name}" - val art = data.tags.filter { it.namespace == 1 }.joinToString(", ") { it.name } - .ifEmpty { null } - artist = art - author = data.tags.filter { it.namespace == 2 }.joinToString(", ") { it.name } - .ifEmpty { art } - genre = prepareTags(data.tags, preferences.useTagGrouping) - update_strategy = UpdateStrategy.ONLY_FETCH_ONCE - status = SManga.COMPLETED - } - } - } - - override fun getMangaUrl(manga: SManga) = - if (preferences.openSource && !manga.url.startsWith("?")) { - val id = manga.url.split("/").reversed()[1].toInt() - anchiraData.find { it.id == id }?.url ?: "$baseUrl${manga.url}" - } else { - "$baseUrl${manga.url}" - } - - // Chapter - - override fun chapterListRequest(manga: SManga): Request { - return if (manga.url.startsWith("?")) { - GET(libraryUrl + manga.url, headers) - } else { - GET("$libraryUrl/${getPathFromUrl(manga.url)}", headers) - } - } - - override fun chapterListParse(response: Response): List { - val chapterList = mutableListOf() - if (response.request.url.pathSegments.count() == libraryUrl.toHttpUrl().pathSegments.count()) { - var results = json.decodeFromString(response.body.string()) - val pages = min(5, ceil((results.total.toFloat() / results.limit)).toInt()) - for (page in 1..pages) { - results.entries.forEach { data -> - chapterList.add( - createChapter(data, anchiraData), - ) - } - if (page < pages) { - results = json.decodeFromString( - client.newCall( - GET( - response.request.url.newBuilder() - .setQueryParameter("page", (page + 1).toString()).build(), - headers, - ), - ).execute().body.string(), - ) - } - } - } else { - val data = json.decodeFromString(response.body.string()) - chapterList.add( - createChapter(data, anchiraData), - ) - } - return chapterList - } - - override fun getChapterUrl(chapter: SChapter) = "$baseUrl/g/${getPathFromUrl(chapter.url)}" - - // Page List - - override fun pageListRequest(chapter: SChapter) = - GET("$libraryUrl/${getPathFromUrl(chapter.url)}", headers) - - override fun pageListParse(response: Response): List { - val data = json.decodeFromString(response.body.string()) - val imageData = getImageData(data) - - return data.images.mapIndexed { i, image -> - Page( - i, - imageUrl = "${getCdn(i)}/${imageData.id}/${imageData.key}/${imageData.hash}/b/${image.name}", - ) - } - } - - private fun getImageData(entry: Entry): ImageData { - val keys = anchiraData.find { it.id == entry.id } - - if (keys?.key != null && keys.hash != null) { - return ImageData(keys.id, keys.key, keys.hash) - } - - try { - val response = - client.newCall(GET("$libraryUrl/${entry.id}/${entry.key}/data", headers)).execute() - - return json.decodeFromString(response.body.string()) - } catch (_: IOException) { - throw IOException("Complete a Captcha in the site to continue") - } - } - - override fun imageRequest(page: Page): Request { - return GET(page.imageUrl!!.replace("/b/", "/${preferences.imageQuality}/"), headers) - } - - override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() - - // Settings - - @SuppressLint("SetTextI18n") - override fun setupPreferenceScreen(screen: PreferenceScreen) { - val imageQualityPref = ListPreference(screen.context).apply { - key = IMAGE_QUALITY_PREF - title = "Image quality" - entries = arrayOf("Original", "Resampled") - entryValues = arrayOf("a", "b") - setDefaultValue("b") - summary = "%s" - } - - val openSourcePref = SwitchPreferenceCompat(screen.context).apply { - key = OPEN_SOURCE_PREF - title = "Open source website in WebView" - summary = - "Enable to open the original source website of the gallery (if available) instead of Anchira." - setDefaultValue(false) - } - - val useTagGrouping = SwitchPreferenceCompat(screen.context).apply { - key = USE_TAG_GROUPING - title = "Group tags" - summary = - "Enable to group tags together by artist, circle, parody, magazine and general tags" - setDefaultValue(false) - } - - screen.addPreference(imageQualityPref) - screen.addPreference(openSourcePref) - screen.addPreference(useTagGrouping) - } - - override fun getFilterList() = FilterList( - CategoryGroup(), - SortFilter(), - FavoritesFilter(), - Filter.Separator(), - Filter.Header("Others are ignored if trending only"), - TrendingFilter(), - SortTrendingFilter(), - ) - - private class CategoryFilter(name: String) : Filter.CheckBox(name, false) - - private class FavoritesFilter : Filter.CheckBox( - "Show only my favorites", - ) - - private class CategoryGroup : Filter.Group( - "Categories", - listOf("Manga", "Doujinshi", "Illustration").map { CategoryFilter(it) }, - ) - - private class SortFilter : Filter.Sort( - "Sort", - arrayOf("Title", "Pages", "Date uploaded", "Date published", "Popularity"), - Selection(2, false), - ) - - private class TrendingFilter : Filter.CheckBox( - "Show only trending", - ) - - private class SortTrendingFilter : PartFilter( - "Sort By", - arrayOf("Trending: Weekly", "Trending: Monthly"), - ) - - private open class PartFilter(displayName: String, value: Array) : - Filter.Select(displayName, value) - - private val SharedPreferences.imageQuality - get() = getString(IMAGE_QUALITY_PREF, "b")!! - - private val SharedPreferences.openSource - get() = getBoolean(OPEN_SOURCE_PREF, false) - - private val SharedPreferences.useTagGrouping - get() = getBoolean(USE_TAG_GROUPING, false) - - private fun resampledInterceptor(chain: Interceptor.Chain): Response { - val request = chain.request() - val url = request.url.toString() - - return if (url.contains("sexo.xyz")) { - val response = chain.proceed(request) - - if (response.isSuccessful) { - return response - } else if (url.contains("/b/")) { - return chain.proceed(request.newBuilder().url(url.replace("/b/", "/a/")).build()) - } - - throw IOException("An error occurred while loading the image - ${response.code}") - } else { - chain.proceed(request) - } - } - - private fun isLoggedIn() = client.cookieJar.loadForRequest(baseUrl.toHttpUrl()).any { - it.name == "session" - } - - private val anchiraData by lazy { - client.newCall(GET(DATA_JSON, headers)).execute() - .use { json.decodeFromStream>(it.body.byteStream()) } - } - - private inline fun Iterable<*>.findInstance() = find { it is T } as? T - - companion object { - const val SLUG_SEARCH_PREFIX = "id:" - private const val SLUG_BUNDLE_PREFIX = "bundle:" - private const val IMAGE_QUALITY_PREF = "image_quality" - private const val OPEN_SOURCE_PREF = "use_manga_source" - private const val USE_TAG_GROUPING = "use_tag_grouping" - private const val DATA_JSON = - "https://raw.githubusercontent.com/LetrixZ/gallery-data/main/extension_data.min.json" - } -} - -val CHAPTER_SUFFIX_RE = - Regex("\\W*(?:Ch\\.?|Chapter|Part|Vol\\.?|Volume|#)?\\W?(? = emptyList(), - val total: Int, - val page: Int, - val limit: Int, -) - -@Serializable -class Entry( - val id: Int, - val key: String, - @SerialName("published_at") val publishedAt: Long = 0L, - val title: String, - @SerialName("thumb_index") val thumbnailIndex: Int = 0, - val tags: List = emptyList(), - val url: String? = null, - val pages: Int = 1, - val cover: Image? = null, - @SerialName("data") - val images: List = emptyList(), -) - -@Serializable -class ImageData( - val id: Int, - val key: String, - val hash: String, -) - -@Serializable -class EntryKey( - val id: Int, - val key: String? = null, - val hash: String? = null, - val url: String? = null, -) - -@Serializable -class Image( - @SerialName("n") val name: String, -) diff --git a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraHelper.kt b/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraHelper.kt deleted file mode 100644 index 4f83dbe5e..000000000 --- a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraHelper.kt +++ /dev/null @@ -1,62 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.anchira - -import eu.kanade.tachiyomi.source.model.SChapter -import java.util.Locale - -object AnchiraHelper { - fun getPathFromUrl(url: String) = "${url.split("/").reversed()[1]}/${url.split("/").last()}" - - fun prepareTags(tags: List, group: Boolean) = tags.map { - if (it.namespace == null) { - it.namespace = 6 - } - it - } - .sortedBy { it.name } - .sortedBy { it.namespace } - .map { - val tag = it.name.lowercase() - return@map if (group) { - when (it.namespace) { - 1 -> "artist:$tag" - 2 -> "circle:$tag" - 3 -> "parody:$tag" - 4 -> "magazine:$tag" - else -> "tag:$tag" - } - } else { - tag - } - } - .joinToString(", ") { it } - - fun createChapter(entry: Entry, anchiraData: List) = - SChapter.create().apply { - val chSuffix = CHAPTER_SUFFIX_RE.find(entry.title)?.value.orEmpty() - val chNumber = - chSuffix.replace(Regex("[^.\\d]"), "").trim('.').takeUnless { it.isEmpty() } ?: "1" - val source = Regex("fakku|irodori").find( - anchiraData.find { it.id == entry.id }?.url.orEmpty(), - )?.value.orEmpty().titleCase() - url = "/g/${entry.id}/${entry.key}" - name = "$chNumber. ${entry.title.removeSuffix(chSuffix)}" - date_upload = entry.publishedAt * 1000 - chapter_number = chNumber.toFloat() - scanlator = buildString { - if (source.isNotEmpty()) { - append("$source - ") - } - append("${entry.pages} pages") - } - } - - fun getCdn(page: Int) = if (page % 2 == 0) "https://kisakisexo.xyz" else "https://aronasexo.xyz" - - private fun String.titleCase() = replaceFirstChar { - if (it.isLowerCase()) { - it.titlecase(Locale.getDefault()) - } else { - it.toString() - } - } -} diff --git a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraUrlActivity.kt b/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraUrlActivity.kt deleted file mode 100644 index 7b4bdaaeb..000000000 --- a/src/en/anchira/src/eu/kanade/tachiyomi/extension/en/anchira/AnchiraUrlActivity.kt +++ /dev/null @@ -1,35 +0,0 @@ -package eu.kanade.tachiyomi.extension.en.anchira - -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 AnchiraUrlActivity : Activity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val pathSegments = intent?.data?.pathSegments - if (pathSegments != null && pathSegments.size > 2) { - val id = pathSegments[1] - val key = pathSegments[2] - val mainIntent = Intent().apply { - action = "eu.kanade.tachiyomi.SEARCH" - putExtra("query", "${Anchira.SLUG_SEARCH_PREFIX}$id/$key") - putExtra("filter", packageName) - } - - try { - startActivity(mainIntent) - } catch (e: ActivityNotFoundException) { - Log.e("AnchiraUrlActivity", e.toString()) - } - } else { - Log.e("AnchiraUrlActivity", "could not parse uri from intent $intent") - } - - finish() - exitProcess(0) - } -}