parent
79a5a0f948
commit
cb1b9aa683
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -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 |
|
@ -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()
|
||||
}
|
|
@ -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])
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
Loading…
Reference in New Issue