parent
							
								
									79a5a0f948
								
							
						
					
					
						commit
						cb1b9aa683
					
				
							
								
								
									
										2
									
								
								src/zh/noyacg/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/zh/noyacg/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<manifest package="eu.kanade.tachiyomi.extension" />
 | 
			
		||||
							
								
								
									
										13
									
								
								src/zh/noyacg/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/zh/noyacg/build.gradle
									
									
									
									
									
										Normal 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"
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/zh/noyacg/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/zh/noyacg/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/zh/noyacg/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/zh/noyacg/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/zh/noyacg/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/zh/noyacg/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 7.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/zh/noyacg/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/zh/noyacg/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/zh/noyacg/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/zh/noyacg/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 24 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/zh/noyacg/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/zh/noyacg/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
										
											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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user