Simply Cosplay (#17291)

This commit is contained in:
AwkwardPeak7 2023-07-29 07:31:24 +05:00 committed by GitHub
parent 15d527bac9
commit ffd04a5671
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 570 additions and 0 deletions

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".all.simplycosplay.SimplyCosplayUrlActivity"
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="www.simply-cosplay.com" />
<data android:host="simply-cosplay.com" />
<data android:scheme="https" />
<data android:pathPattern="/gallery/..*" />
<data android:pathPattern="/image/..*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,13 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Simply Cosplay'
pkgNameSuffix = 'all.simplycosplay'
extClass = '.SimplyCosplay'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -0,0 +1,382 @@
package eu.kanade.tachiyomi.extension.all.simplycosplay
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
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.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl
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 uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
class SimplyCosplay : HttpSource(), ConfigurableSource {
override val name = "Simply Cosplay"
override val lang = "all"
override val baseUrl = "https://www.simply-cosplay.com"
private val apiUrl = "https://api.simply-porn.com/v2".toHttpUrl()
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::tokenIntercept)
.rateLimit(2)
.build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", baseUrl)
private val json: Json by injectLazy()
private val preference by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private fun tokenIntercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.host != apiUrl.host) {
return chain.proceed(request)
}
val url = request.url.newBuilder()
.setQueryParameter("token", preference.getToken())
.build()
val response = chain.proceed(
request.newBuilder()
.url(url)
.build(),
)
if (response.isSuccessful.not() && response.code == 403) {
response.close()
val newToken = fetchNewToken()
preference.putToken(newToken)
val newUrl = request.url.newBuilder()
.setQueryParameter("token", newToken)
.build()
return chain.proceed(
request.newBuilder()
.url(newUrl)
.build(),
)
}
return response
}
private fun fetchNewToken(): String {
val document = client.newCall(GET(baseUrl, headers)).execute().asJsoup()
val scriptUrl = document.selectFirst("script[src*=main]")
?.attr("abs:src")
?: throw IOException(TOKEN_EXCEPTION)
val scriptContent = client.newCall(GET(scriptUrl, headers)).execute()
.use { it.body.string() }
.replace("\'", "\"")
return TokenRegex.find(scriptContent)?.groupValues?.get(1)
?: throw IOException(TOKEN_EXCEPTION)
}
private fun browseUrlBuilder(endPoint: String, sort: String, page: Int): HttpUrl.Builder {
return apiUrl.newBuilder().apply {
addPathSegment(endPoint)
addQueryParameter("sort", sort)
addQueryParameter("limit", limit.toString())
addQueryParameter("page", page.toString())
}
}
override fun popularMangaRequest(page: Int): Request {
val url = browseUrlBuilder(preference.getDefaultBrowse(), "hot", page)
return GET(url.build(), headers)
}
override fun popularMangaParse(response: Response): MangasPage {
runCatching { fetchTags() }
val result = response.parseAs<browseResponse>()
val entries = result.data.map(BrowseItem::toSManga)
val hasNextPage = result.data.size >= limit
return MangasPage(entries, hasNextPage)
}
override fun latestUpdatesRequest(page: Int): Request {
val url = browseUrlBuilder(preference.getDefaultBrowse(), "new", page)
return GET(url.build(), headers)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(SEARCH_PREFIX)) {
val url = query.substringAfter(SEARCH_PREFIX)
val manga = SManga.create().apply { this.url = url }
fetchMangaDetails(manga).map {
MangasPage(listOf(it), false)
}
} else {
super.fetchSearchManga(page, query, filters)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val sort = filters.filterIsInstance<SortFilter>().firstOrNull()?.getSort() ?: "new"
val url = browseUrlBuilder("search", sort, page).apply {
if (query.isNotEmpty()) {
addQueryParameter("query", query)
}
filters.map { filter ->
when (filter) {
is TagFilter -> {
filter.getSelected().forEachIndexed { index, tag ->
addQueryParameter(
"filter[tag_names][$index]",
tag.name.replace(" ", "+"),
)
}
}
is TypeFilter -> {
filter.getValue().let {
if (it.isNotEmpty()) {
addQueryParameter("filter[type][0]", it)
}
}
}
else -> { }
}
}
}
return GET(url.build(), headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
private var tagList: List<String> = emptyList()
private var tagsFetchAttempt = 0
private var tagsFetchFailed = false
private fun fetchTags() {
if (tagsFetchAttempt < 3 && (tagList.isEmpty() || tagsFetchFailed)) {
val tags = runCatching {
client.newCall(tagsRequest())
.execute().use(::tagsParse)
}
tagsFetchFailed = tags.isFailure
tagList = tags.getOrElse {
Log.e("SimplyHentaiTags", it.stackTraceToString())
emptyList()
}
tagsFetchAttempt++
}
}
private fun tagsRequest(): Request {
val url = apiUrl.newBuilder()
.addPathSegment("search")
.build()
return GET(url, headers)
}
private fun tagsParse(response: Response): List<String> {
val result = response.parseAs<TagsResponse>()
return result.aggs.tag_names.buckets.map {
it.key.trim()
}
}
class Tag(name: String) : Filter.CheckBox(name)
class TagFilter(title: String, tags: List<String>) :
Filter.Group<Tag>(title, tags.map(::Tag)) {
fun getSelected() = state.filter { it.state }
}
class TypeFilter(title: String, private val types: List<String>) :
Filter.Select<String>(title, types.toTypedArray()) {
fun getValue() = types[state].lowercase()
}
class SortFilter(title: String, private val sorts: List<String>) :
Filter.Select<String>(title, sorts.toTypedArray()) {
fun getSort() = sorts[state].lowercase()
}
override fun getFilterList(): FilterList {
val filters: MutableList<Filter<*>> = mutableListOf(
SortFilter("Sort", listOf("New", "Hot")),
TypeFilter("Type", listOf("", "Image", "Gallery")),
)
if (tagList.isNotEmpty()) {
filters += TagFilter("Tags", tagList)
} else {
filters += listOf(
Filter.Separator(),
Filter.Header("Press 'Reset' to attempt to show tags"),
)
}
return FilterList(filters)
}
private fun mangaUrlBuilder(dbUrl: String): HttpUrl.Builder {
val pathSegments = dbUrl.split("/")
val type = pathSegments[1]
val slug = pathSegments[3]
return apiUrl.newBuilder().apply {
addPathSegment(type)
addPathSegments(slug)
}
}
override fun mangaDetailsRequest(manga: SManga): Request {
val url = mangaUrlBuilder(manga.url)
return GET(url.build(), headers)
}
override fun mangaDetailsParse(response: Response): SManga {
val result = response.parseAs<detailsResponse>()
return result.data.toSManga()
}
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.just(
listOf(
SChapter.create().apply {
url = manga.url
name = manga.url.split("/")[1].replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(
Locale.ROOT,
)
} else {
it.toString()
}
}
date_upload = manga.description?.substringAfterLast("Date: ").parseDate()
},
),
)
}
override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url
override fun pageListRequest(chapter: SChapter): Request {
val url = mangaUrlBuilder(chapter.url)
return GET(url.build(), headers)
}
override fun pageListParse(response: Response): List<Page> {
val result = response.parseAs<pageResponse>()
return result.data.images?.mapIndexedNotNull { index, image ->
if (image.urls.url.isNullOrEmpty()) {
null
} else {
Page(index, "", image.urls.url)
}
}
?: Page(1, "", result.data.preview.urls.url).let(::listOf)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = BROWSE_TYPE_PREF_KEY
title = BROWSE_TYPE_TITLE
entries = arrayOf("Gallery", "Image")
entryValues = arrayOf("gallery", "image")
summary = "%s"
setDefaultValue("gallery")
}.also(screen::addPreference)
}
private fun SharedPreferences.getDefaultBrowse() =
getString(BROWSE_TYPE_PREF_KEY, "gallery")!!
private fun SharedPreferences.getToken() =
getString(DEFAULT_TOKEN_PREF, DEFAULT_FALLBACK_TOKEN) ?: DEFAULT_FALLBACK_TOKEN
private fun SharedPreferences.putToken(token: String) =
edit().putString(DEFAULT_TOKEN_PREF, token).commit()
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private fun String?.parseDate(): Long {
return runCatching { dateFormat.parse(this!!)!!.time }
.getOrDefault(0L)
}
companion object {
private const val limit = 20
const val SEARCH_PREFIX = "url:"
private const val DEFAULT_TOKEN_PREF = "default_token_pref"
private const val DEFAULT_FALLBACK_TOKEN = "01730876"
private const val TOKEN_EXCEPTION = "Unable to fetch new Token"
private val TokenRegex = Regex("""token\s*:\s*"([^\"]+)""")
private val dateFormat by lazy { SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss.SSS", Locale.ENGLISH) }
private const val BROWSE_TYPE_PREF_KEY = "default_browse_type_key"
private const val BROWSE_TYPE_TITLE = "Default Browse List"
}
override fun chapterListParse(response: Response) =
throw UnsupportedOperationException("Not implemented")
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException("Not implemented")
}

View File

@ -0,0 +1,116 @@
package eu.kanade.tachiyomi.extension.all.simplycosplay
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import kotlinx.serialization.Serializable
import java.util.Locale
typealias browseResponse = Data<List<BrowseItem>>
typealias detailsResponse = Data<DetailsResponse>
typealias pageResponse = Data<PageResponse>
@Serializable
data class Data<T>(val data: T)
@Serializable
data class BrowseItem(
val title: String? = null,
val slug: String,
val type: String,
val preview: Images,
) {
fun toSManga() = SManga.create().apply {
title = this@BrowseItem.title ?: ""
url = "/${type.lowercase().trim()}/new/$slug"
thumbnail_url = preview.urls.thumb.url
description = preview.publish_date?.let { "Date: $it" }
}
}
@Serializable
data class TagsResponse(
val aggs: Agg,
)
@Serializable
data class Agg(
val tag_names: TagNames,
)
@Serializable
data class TagNames(
val buckets: List<GenreItem>,
)
@Serializable
data class GenreItem(
val key: String,
)
@Serializable
data class DetailsResponse(
val title: String? = null,
val slug: String,
val type: String,
val preview: Images,
val tags: List<Tag>? = emptyList(),
val image_count: Int? = null,
) {
fun toSManga() = SManga.create().apply {
title = this@DetailsResponse.title ?: ""
url = "/${type.lowercase().trim()}/new/$slug"
thumbnail_url = preview.urls.thumb.url
genre = tags?.mapNotNull { it ->
it.name?.trim()?.split(" ")?.let { genre ->
genre.map {
it.replaceFirstChar { char ->
if (char.isLowerCase()) {
char.titlecase(
Locale.ROOT,
)
} else {
char.toString()
}
}
}
}?.joinToString(" ")
}?.joinToString()
description = buildString {
append("Type: $type\n")
image_count?.let { append("Images: $it\n") }
preview.publish_date?.let { append("Date: $it\n") }
}
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
status = SManga.COMPLETED
}
}
@Serializable
data class PageResponse(
val images: List<Images>? = null,
val preview: Images,
)
@Serializable
data class Images(
val publish_date: String? = null,
val urls: Urls,
)
@Serializable
data class Urls(
val url: String? = null,
val thumb: Url,
)
@Serializable
data class Url(
val url: String? = null,
)
@Serializable
data class Tag(
val name: String? = null,
)

View File

@ -0,0 +1,33 @@
package eu.kanade.tachiyomi.extension.all.simplycosplay
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 SimplyCosplayUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size >= 3) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${SimplyCosplay.SEARCH_PREFIX}/${pathSegments[0]}/new/${pathSegments[2]}")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("SimplyCosplayUrlActivit", e.toString())
}
} else {
Log.e("SimplyCosplayUrlActivit", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}