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