Add NoyAcg (#14812)

* Add NoyAcg

* rename function
This commit is contained in:
stevenyomi 2023-01-06 21:29:26 +08:00 committed by GitHub
parent 79a5a0f948
commit cb1b9aa683
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 292 additions and 0 deletions

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.extension.zh.noyacg
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
const val LISTING_PAGE_SIZE = 20
@Serializable
class MangaDto(
@SerialName("Bid") private val id: Int,
@SerialName("Bookname") private val title: String,
@SerialName("Author") private val author: String,
@SerialName("Pname") private val character: String,
@SerialName("Ptag") private val genres: String,
@SerialName("Otag") private val parody: String,
@SerialName("Time") private val timestamp: Long,
@SerialName("Len") private val pageCount: Int,
) {
fun toSManga(imageCdn: String) = SManga.create().also {
it.url = id.toString()
it.title = title
it.author = author.formatNames()
it.description = "时间:${mangaDateFormat.format(timestamp * 1000)}\n" +
"页数:$pageCount\n" +
"原作:${parody.formatNames()}\n" +
"角色:${character.formatNames()}"
it.genre = genres.replace(" ", ", ")
it.status = SManga.COMPLETED
it.thumbnail_url = "$imageCdn/$id/m1.webp"
it.initialized = pageCount > 0
}
}
fun SManga.field(index: Int): String =
description!!.split("\n")[index].substringAfter('')
val SManga.timestamp: Long get() = dateFormat.parse(field(0))!!.time
val SManga.pageCount: Int get() = field(1).toInt()
val dateFormat get() = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
private val mangaDateFormat = dateFormat
fun String.formatNames() = split(" ").joinToString { name ->
name.split("-").joinToString(" ") { word -> word.replaceFirstChar { it.uppercaseChar() } }
}
@Serializable
class ListingPageDto(
private val info: List<MangaDto>? = null,
private val Info: List<MangaDto>? = null,
val len: Int,
) {
val entries get() = info ?: Info ?: emptyList()
}

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.extension.zh.noyacg
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.FormBody
fun getFilterListInternal() = FilterList(
Filter.Header("搜索选项"),
SearchTypeFilter(),
SortFilter(),
Filter.Separator(),
Filter.Header("排行榜(搜索文本时无效)"),
RankingFilter(),
RankingRangeFilter(),
)
interface ListingFilter {
fun addTo(builder: FormBody.Builder)
}
interface SearchFilter : ListingFilter
class SearchTypeFilter : SearchFilter, Filter.Select<String>("搜索范围", arrayOf("综合", "标签", "作者")) {
override fun addTo(builder: FormBody.Builder) {
builder.addEncoded("type", arrayOf("de", "tag", "author")[state])
}
}
class SortFilter : SearchFilter, Filter.Select<String>("排序", arrayOf("时间", "阅读量", "收藏")) {
override fun addTo(builder: FormBody.Builder) {
builder.addEncoded("sort", arrayOf("bid", "views", "favorites")[state])
}
}
class RankingFilter : Filter.Select<String>("排行榜", arrayOf("阅读榜", "收藏榜", "高质量榜")) {
val path get() = arrayOf("readLeaderboard", "favLeaderboard", "proportion")[state]
}
class RankingRangeFilter : ListingFilter, Filter.Select<String>("阅读/收藏榜范围", arrayOf("日榜", "周榜", "月榜")) {
override fun addTo(builder: FormBody.Builder) {
builder.addEncoded("type", arrayOf("day", "week", "moon")[state])
}
}

View File

@ -0,0 +1,146 @@
package eu.kanade.tachiyomi.extension.zh.noyacg
import android.app.Application
import androidx.preference.PreferenceScreen
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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.FormBody
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
class NoyAcg : HttpSource(), ConfigurableSource {
override val name get() = "NoyAcg"
override val lang get() = "zh"
override val supportsLatest get() = true
override val baseUrl get() = "https://app.noy.asia"
private val imageCdn by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000).imageCdn
}
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int): Request {
val body = FormBody.Builder()
.addEncoded("page", page.toString())
.addEncoded("type", "day")
.build()
return POST("$baseUrl/api/readLeaderboard", headers, body)
}
override fun popularMangaParse(response: Response): MangasPage {
val page = (response.request.body as FormBody).encodedValue(0).toInt()
val imageCdn = imageCdn
val listingPage: ListingPageDto = response.parseAs()
val entries = listingPage.entries.map { it.toSManga(imageCdn) }
val hasNextPage = page * LISTING_PAGE_SIZE < listingPage.len
return MangasPage(entries, hasNextPage)
}
override fun latestUpdatesRequest(page: Int): Request {
val body = FormBody.Builder()
.addEncoded("page", page.toString())
.build()
return POST("$baseUrl/api/booklist_v2", headers, body)
}
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun getFilterList() = getFilterListInternal()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filters = filters.ifEmpty { getFilterListInternal() }
val builder = FormBody.Builder()
.addEncoded("page", page.toString())
return if (query.isNotBlank()) {
builder.add("info", query)
for (filter in filters) if (filter is SearchFilter) filter.addTo(builder)
POST("$baseUrl/api/search_v2", headers, builder.build())
} else {
var path: String? = null
for (filter in filters) when (filter) {
is RankingFilter -> path = filter.path
is RankingRangeFilter -> filter.addTo(builder)
else -> {}
}
POST("$baseUrl/api/${path!!}", headers, builder.build())
}
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
// for WebView
override fun mangaDetailsRequest(manga: SManga) = GET("$baseUrl/#/book/${manga.url}")
override fun mangaDetailsParse(response: Response) = throw UnsupportedOperationException()
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
val body = FormBody.Builder()
.addEncoded("bid", manga.url)
.build()
val request = POST("$baseUrl/api/getbookinfo", headers, body)
return client.newCall(request).asObservableSuccess().map {
it.parseAs<MangaDto>().toSManga(imageCdn)
}
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val pageCount = manga.pageCount
if (pageCount <= 0) return Observable.just(emptyList())
val chapter = SChapter.create().apply {
url = "${manga.url}#$pageCount"
name = "单章节"
date_upload = manga.timestamp
chapter_number = -2f
}
return Observable.just(listOf(chapter))
}
// for WebView
override fun pageListRequest(chapter: SChapter) = GET("$baseUrl/#/read/" + chapter.url.substringBefore('#'))
override fun pageListParse(response: Response) = throw UnsupportedOperationException()
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
val mangaId = chapter.url.substringBefore('#')
val pageCount = chapter.url.substringAfter('#').toInt()
val imageCdn = imageCdn
val pageList = List(pageCount) {
Page(it, imageUrl = "$imageCdn/$mangaId/${it + 1}.webp")
}
return Observable.just(pageList)
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
private val json: Json by injectLazy()
private inline fun <reified T> Response.parseAs(): T = try {
json.decodeFromStream(body!!.byteStream())
} catch (e: Throwable) {
throw Exception("请在 WebView 中登录")
} finally {
close()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
getPreferencesInternal(screen.context).forEach(screen::addPreference)
}
}

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.extension.zh.noyacg
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.ListPreference
import kotlin.random.Random
fun getPreferencesInternal(context: Context) = arrayOf(
ListPreference(context).apply {
val count = IMAGE_CDN.size
key = IMAGE_CDN_PREF
title = "图片分流(重启生效)"
summary = "%s"
entries = Array(count) { "分流 ${it + 1}" }
entryValues = Array(count) { "$it" }
},
)
val SharedPreferences.imageCdn: String
get() {
val imageCdn = IMAGE_CDN
var index = getString(IMAGE_CDN_PREF, "-1")!!.toInt()
if (index !in imageCdn.indices) {
index = Random.nextInt(0, imageCdn.size)
edit().putString(IMAGE_CDN_PREF, index.toString()).apply()
}
return "https://" + imageCdn[index]
}
const val IMAGE_CDN_PREF = "IMAGE_CDN"
val IMAGE_CDN get() = arrayOf("img.noy.asia", "img.noyteam.online", "img.457475.xyz")