Remove Pufei Manhua: site is down (#12826)
This commit is contained in:
		
							parent
							
								
									4a54a8c801
								
							
						
					
					
						commit
						5975d2528e
					
				| @ -1,2 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest package="eu.kanade.tachiyomi.extension" /> | ||||
| @ -1,15 +0,0 @@ | ||||
| apply plugin: 'com.android.application' | ||||
| apply plugin: 'kotlin-android' | ||||
| 
 | ||||
| ext { | ||||
|     extName = 'Pufei Manhua' | ||||
|     pkgNameSuffix = 'zh.pufei' | ||||
|     extClass = '.Pufei' | ||||
|     extVersionCode = 10 | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
| 
 | ||||
| dependencies { | ||||
|     implementation 'com.github.stevenyomi:unpacker:12a09e3c1a' // 1.1 | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 5.2 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 3.1 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 7.4 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 12 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 21 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 44 KiB | 
| @ -1,58 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.zh.pufei | ||||
| 
 | ||||
| import android.os.SystemClock | ||||
| import okhttp3.Interceptor | ||||
| import okhttp3.Response | ||||
| import java.io.IOException | ||||
| import java.util.concurrent.TimeUnit | ||||
| 
 | ||||
| // See https://github.com/tachiyomiorg/tachiyomi/pull/7389 | ||||
| internal class NonblockingRateLimiter( | ||||
|     private val permits: Int, | ||||
|     period: Long = 1, | ||||
|     unit: TimeUnit = TimeUnit.SECONDS, | ||||
| ) : Interceptor { | ||||
| 
 | ||||
|     private val requestQueue = ArrayList<Long>(permits) | ||||
|     private val rateLimitMillis = unit.toMillis(period) | ||||
| 
 | ||||
|     override fun intercept(chain: Interceptor.Chain): Response { | ||||
|         // Ignore canceled calls, otherwise they would jam the queue | ||||
|         if (chain.call().isCanceled()) { | ||||
|             throw IOException() | ||||
|         } | ||||
| 
 | ||||
|         synchronized(requestQueue) { | ||||
|             val now = SystemClock.elapsedRealtime() | ||||
|             val waitTime = if (requestQueue.size < permits) { | ||||
|                 0 | ||||
|             } else { | ||||
|                 val oldestReq = requestQueue[0] | ||||
|                 val newestReq = requestQueue[permits - 1] | ||||
| 
 | ||||
|                 if (newestReq - oldestReq > rateLimitMillis) { | ||||
|                     0 | ||||
|                 } else { | ||||
|                     oldestReq + rateLimitMillis - now // Remaining time | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Final check | ||||
|             if (chain.call().isCanceled()) { | ||||
|                 throw IOException() | ||||
|             } | ||||
| 
 | ||||
|             if (requestQueue.size == permits) { | ||||
|                 requestQueue.removeAt(0) | ||||
|             } | ||||
|             if (waitTime > 0) { | ||||
|                 requestQueue.add(now + waitTime) | ||||
|                 Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests | ||||
|             } else { | ||||
|                 requestQueue.add(now) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return chain.proceed(chain.request()) | ||||
|     } | ||||
| } | ||||
| @ -1,41 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.zh.pufei | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import okhttp3.Interceptor | ||||
| import okhttp3.MediaType.Companion.toMediaType | ||||
| import okhttp3.Response | ||||
| import okhttp3.ResponseBody.Companion.asResponseBody | ||||
| 
 | ||||
| object OctetStreamInterceptor : Interceptor { | ||||
| 
 | ||||
|     override fun intercept(chain: Interceptor.Chain): Response { | ||||
|         val request = chain.request() | ||||
|         val response = chain.proceed(request) | ||||
| 
 | ||||
|         if (response.header("Content-Type") != "application/octet-stream") { | ||||
|             return response | ||||
|         } | ||||
| 
 | ||||
|         if (response.header("Content-Length")!!.toInt() < 100) { // usually 96 | ||||
|             // The actual URL is '/.../xxx.jpg/0'. | ||||
|             val peek = response.peekBody(100).string() | ||||
|             if (peek.startsWith("The actual URL")) { | ||||
|                 response.body!!.close() | ||||
|                 val actualPath = peek.substringAfter('\'').substringBeforeLast('\'') | ||||
|                 return chain.proceed(GET("https://manhua.acimg.cn$actualPath")) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         val url = request.url.encodedPath | ||||
|         val mediaType = when { | ||||
|             url.endsWith(".h") -> webpMediaType | ||||
|             url.contains(".jpg") -> jpegMediaType | ||||
|             else -> return response | ||||
|         } | ||||
|         val body = response.body!!.source().asResponseBody(mediaType) | ||||
|         return response.newBuilder().body(body).build() | ||||
|     } | ||||
| 
 | ||||
|     private val jpegMediaType = "image/jpeg".toMediaType() | ||||
|     private val webpMediaType = "image/webp".toMediaType() | ||||
| } | ||||
| @ -1,223 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.zh.pufei | ||||
| 
 | ||||
| import android.app.Application | ||||
| import android.content.SharedPreferences | ||||
| import android.util.Base64 | ||||
| import androidx.preference.ListPreference | ||||
| import androidx.preference.PreferenceScreen | ||||
| import com.github.stevenyomi.unpacker.ProgressiveParser | ||||
| import com.github.stevenyomi.unpacker.Unpacker | ||||
| 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.ParsedHttpSource | ||||
| import okhttp3.FormBody | ||||
| import okhttp3.Request | ||||
| import okhttp3.Response | ||||
| import org.jsoup.nodes.Document | ||||
| import org.jsoup.nodes.Element | ||||
| import org.jsoup.select.Evaluator | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| 
 | ||||
| // Uses www733dm/IMH/dm456 theme | ||||
| class Pufei : ParsedHttpSource(), ConfigurableSource { | ||||
| 
 | ||||
|     override val name = "扑飞漫画" | ||||
|     override val lang = "zh" | ||||
|     override val supportsLatest = true | ||||
| 
 | ||||
|     private val preferences: SharedPreferences = | ||||
|         Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) | ||||
| 
 | ||||
|     private val domain = preferences.getString(MIRROR_PREF, "0")!! | ||||
|         .toInt().coerceAtMost(MIRRORS.size - 1).let { MIRRORS[it] } | ||||
| 
 | ||||
|     override val baseUrl = "http://m.$domain" | ||||
|     private val pcUrl = "http://www.$domain" | ||||
| 
 | ||||
|     override val client = network.client.newBuilder() | ||||
|         .addInterceptor(NonblockingRateLimiter(2)) | ||||
|         .addInterceptor(OctetStreamInterceptor) | ||||
|         .build() | ||||
| 
 | ||||
|     private val searchClient = network.client.newBuilder() | ||||
|         .followRedirects(false) | ||||
|         .build() | ||||
| 
 | ||||
|     override fun popularMangaRequest(page: Int) = GET("$baseUrl/manhua/paihang.html", headers) | ||||
|     override fun popularMangaNextPageSelector() = throw UnsupportedOperationException("Not used.") | ||||
|     override fun popularMangaSelector() = "ul#detail > li > a" | ||||
|     override fun popularMangaFromElement(element: Element) = SManga.create().apply { | ||||
|         url = element.attr("href").removeSuffix("/index.html") | ||||
|         title = element.selectFirst(Evaluator.Tag("h3")).text() | ||||
|         thumbnail_url = element.selectFirst(Evaluator.Tag("img")).attr("data-src") | ||||
|     } | ||||
| 
 | ||||
|     override fun popularMangaParse(response: Response): MangasPage { | ||||
|         val document = response.asPufeiJsoup() | ||||
|         val mangas = document.select(popularMangaSelector()).map { popularMangaFromElement(it) } | ||||
|         return MangasPage(mangas, false) | ||||
|     } | ||||
| 
 | ||||
|     override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/manhua/update.html", headers) | ||||
|     override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used.") | ||||
|     override fun latestUpdatesSelector() = popularMangaSelector() | ||||
|     override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) | ||||
| 
 | ||||
|     override fun latestUpdatesParse(response: Response): MangasPage { | ||||
|         val document = response.asPufeiJsoup() | ||||
|         val mangas = document.select(latestUpdatesSelector()).map { latestUpdatesFromElement(it) } | ||||
|         return MangasPage(mangas, false) | ||||
|     } | ||||
| 
 | ||||
|     private val searchCache = HashMap<String, String>() | ||||
| 
 | ||||
|     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { | ||||
|         return if (query.isNotBlank()) { | ||||
|             val path = searchCache.getOrPut(query) { | ||||
|                 val formBody = FormBody.Builder(GB2312) | ||||
|                     .addEncoded("tempid", "4") | ||||
|                     .addEncoded("show", "title,player,playadmin,bieming,pinyin") | ||||
|                     .add("keyboard", query) | ||||
|                     .build() | ||||
|                 val request = POST("$baseUrl/e/search/index.php", headers, formBody) | ||||
|                 searchClient.newCall(request).execute().header("location")!! | ||||
|             } | ||||
|             val sortQuery = parseSearchSort(filters) | ||||
|             GET("$baseUrl/e/search/$path$sortQuery&page=${page - 1}") | ||||
|         } else { | ||||
|             val path = parseFilters(page, filters) | ||||
|             if (path.isEmpty()) | ||||
|                 popularMangaRequest(page) | ||||
|             else | ||||
|                 GET("$baseUrl$path", headers) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Not used.") | ||||
|     override fun searchMangaSelector() = popularMangaSelector() | ||||
|     override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) | ||||
| 
 | ||||
|     override fun searchMangaParse(response: Response): MangasPage { | ||||
|         val document = response.asPufeiJsoup() | ||||
|         val mangas = document.select(searchMangaSelector()).map { searchMangaFromElement(it) } | ||||
|         val hasNextPage = run { | ||||
|             for (element in document.body().children().asReversed()) { | ||||
|                 if (element.tagName() == "a") return@run true | ||||
|                 else if (element.tagName() == "b") return@run false | ||||
|             } | ||||
|             false | ||||
|         } | ||||
|         return MangasPage(mangas, hasNextPage) | ||||
|     } | ||||
| 
 | ||||
|     override fun getFilterList() = getFilters() | ||||
| 
 | ||||
|     // 让 WebView 显示移动端页面 | ||||
|     override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, headers) | ||||
| 
 | ||||
|     override fun fetchMangaDetails(manga: SManga): Observable<SManga> = | ||||
|         client.newCall(GET(pcUrl + manga.urlWithCheck(), headers)).asObservableSuccess() | ||||
|             .map { mangaDetailsParse(it.asPufeiJsoup()) } | ||||
| 
 | ||||
|     override fun mangaDetailsParse(document: Document) = SManga.create().apply { | ||||
|         val details = document.selectFirst(Evaluator.Class("detailInfo")).children() | ||||
|         title = details[0].child(0).text() // div.titleInfo > h1 | ||||
|         val genreList = mutableListOf<String>() | ||||
|         for (item in details[1].children()) { // ul > li | ||||
|             when (item.child(0).text()) { // span | ||||
|                 "作者:" -> author = item.ownText() | ||||
|                 "类别:" -> item.ownText().let { if (it.isNotEmpty()) genreList.add(it) } | ||||
|                 "关键词:" -> item.ownText().let { if (it.isNotEmpty()) genreList.addAll(it.split(',')) } | ||||
|             } | ||||
|         } | ||||
|         author = author ?: details[0].ownText().removePrefix("作者:") | ||||
|         if (genreList.isEmpty()) { | ||||
|             genreList.add(document.selectFirst(Evaluator.Class("position")).child(1).text()) | ||||
|         } | ||||
|         genre = genreList.joinToString() | ||||
|         description = document.selectFirst("div.introduction")?.text() ?: details[2].ownText() | ||||
|         status = SManga.UNKNOWN // 所有漫画的标记都是连载,所以没有意义,参见 baseUrl/manhua/wanjie.html | ||||
|         thumbnail_url = document.selectFirst("img.pic").attr("src") | ||||
|     } | ||||
| 
 | ||||
|     override fun chapterListRequest(manga: SManga) = GET(pcUrl + manga.urlWithCheck(), headers) | ||||
| 
 | ||||
|     override fun chapterListSelector() = "div.plistBox ul > li > a" | ||||
| 
 | ||||
|     override fun chapterFromElement(element: Element) = SChapter.create().apply { | ||||
|         url = element.attr("href") | ||||
|         name = element.attr("title") | ||||
|     } | ||||
| 
 | ||||
|     override fun chapterListParse(response: Response): List<SChapter> { | ||||
|         val document = response.asPufeiJsoup() | ||||
|         val list = document.select(chapterListSelector()).map { chapterFromElement(it) } | ||||
|         if (isNewDateLogic && list.isNotEmpty()) { | ||||
|             val date = document.selectFirst("li.twoCol:contains(更新时间)").text().removePrefix("更新时间:").trim() | ||||
|             list[0].date_upload = dateFormat.parse(date)?.time ?: 0 | ||||
|         } | ||||
|         return list | ||||
|     } | ||||
| 
 | ||||
|     override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url, headers) | ||||
| 
 | ||||
|     override fun pageListParse(response: Response): List<Page> { | ||||
|         val html = String(response.body!!.bytes(), GB2312).let(::ProgressiveParser) | ||||
|         val base64 = html.substringBetween("cp=\"", "\"") | ||||
|         val script = String(Base64.decode(base64, Base64.DEFAULT)) | ||||
|         val result = Unpacker.unpack(script, "[", "]") | ||||
|             .ifEmpty { return emptyList() } | ||||
|             .replace("\\", "") | ||||
|             .removeSurrounding("\"").split("\",\"") | ||||
|         // baseUrl/skin/2014mh/view.js (imgserver), mobileUrl/skin/main.js (IMH.reader) | ||||
|         return result.mapIndexed { i, image -> | ||||
|             val imageUrl = if (image.startsWith("http")) image else IMAGE_SERVER + image | ||||
|             Page(i, imageUrl = imageUrl) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun pageListParse(document: Document) = throw UnsupportedOperationException("Not used.") | ||||
| 
 | ||||
|     override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used.") | ||||
| 
 | ||||
|     override fun setupPreferenceScreen(screen: PreferenceScreen) { | ||||
|         ListPreference(screen.context).apply { | ||||
|             key = MIRROR_PREF | ||||
|             title = "使用镜像网站" | ||||
|             summary = "选择要使用的镜像网站,重启生效\n已选择:%s" | ||||
|             entries = MIRRORS_DESCRIPTION | ||||
|             entryValues = MIRROR_VALUES | ||||
|             setDefaultValue("0") | ||||
|         }.let { screen.addPreference(it) } | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val MIRROR_PREF = "MIRROR" | ||||
|         private val MIRROR_VALUES = arrayOf("0", "1", "2", "3", "4") | ||||
|         private val MIRRORS = arrayOf( | ||||
|             "pufei.cc", | ||||
|             "pfmh.net", | ||||
|             "alimanhua.com", | ||||
|             "8nfw.com", | ||||
|             "pufei5.com", | ||||
|         ) | ||||
|         private val MIRRORS_DESCRIPTION = arrayOf( | ||||
|             "pufei.cc", | ||||
|             "pfmh.net", | ||||
|             "alimanhua.com (阿狸漫画)", | ||||
|             "8nfw.com (风之动漫)", | ||||
|             "pufei5.com (不推荐)", | ||||
|         ) | ||||
| 
 | ||||
|         private const val IMAGE_SERVER = "http://res.img.tueqi.com/" | ||||
|     } | ||||
| } | ||||
| @ -1,51 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.zh.pufei | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.source.model.Filter | ||||
| import eu.kanade.tachiyomi.source.model.FilterList | ||||
| 
 | ||||
| internal fun getFilters() = FilterList( | ||||
|     Filter.Header("排序只对文本搜索和分类筛选有效"), | ||||
|     SortFilter(), | ||||
|     Filter.Separator(), | ||||
|     Filter.Header("以下筛选最多使用一个,使用文本搜索时将会忽略"), | ||||
|     CategoryFilter(), | ||||
|     AlphabetFilter(), | ||||
| ) | ||||
| 
 | ||||
| internal fun parseSearchSort(filters: FilterList): String = | ||||
|     filters.filterIsInstance<SortFilter>().firstOrNull()?.let { SORT_QUERIES[it.state] } ?: "" | ||||
| 
 | ||||
| internal fun parseFilters(page: Int, filters: FilterList): String { | ||||
|     val pageStr = if (page == 1) "" else "_$page" | ||||
|     var category = 0 | ||||
|     var categorySort = 0 | ||||
|     var alphabet = 0 | ||||
|     for (filter in filters) when (filter) { | ||||
|         is SortFilter -> categorySort = filter.state | ||||
|         is CategoryFilter -> category = filter.state | ||||
|         is AlphabetFilter -> alphabet = filter.state | ||||
|         else -> {} | ||||
|     } | ||||
|     return if (category > 0) { | ||||
|         "/${CATEGORY_KEYS[category]}/${SORT_KEYS[categorySort]}$pageStr.html" | ||||
|     } else if (alphabet > 0) { | ||||
|         "/mh/${ALPHABET[alphabet].lowercase()}/index$pageStr.html" | ||||
|     } else { | ||||
|         "" | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| internal class SortFilter : Filter.Select<String>("排序", SORT_NAMES) | ||||
| 
 | ||||
| private val SORT_NAMES = arrayOf("添加时间", "更新时间", "点击次数") | ||||
| private val SORT_KEYS = arrayOf("index", "update", "view") | ||||
| private val SORT_QUERIES = arrayOf("&orderby=newstime", "&orderby=lastdotime", "&orderby=onclick") | ||||
| 
 | ||||
| internal class CategoryFilter : Filter.Select<String>("分类", CATEGORY_NAMES) | ||||
| 
 | ||||
| private val CATEGORY_NAMES = arrayOf("全部", "少年热血", "少女爱情", "武侠格斗", "科幻魔幻", "竞技体育", "搞笑喜剧", "耽美人生", "侦探推理", "恐怖灵异") | ||||
| private val CATEGORY_KEYS = arrayOf("", "shaonianrexue", "shaonvaiqing", "wuxiagedou", "kehuan", "jingjitiyu", "gaoxiaoxiju", "danmeirensheng", "zhentantuili", "kongbulingyi") | ||||
| 
 | ||||
| internal class AlphabetFilter : Filter.Select<String>("字母", ALPHABET) | ||||
| 
 | ||||
| private val ALPHABET = arrayOf("全部", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z") | ||||
| @ -1,28 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.zh.pufei | ||||
| 
 | ||||
| import eu.kanade.tachiyomi.AppInfo | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import okhttp3.Response | ||||
| import org.jsoup.Jsoup | ||||
| import org.jsoup.nodes.Document | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| 
 | ||||
| internal val GB2312 = charset("GB2312") | ||||
| 
 | ||||
| internal fun Response.asPufeiJsoup(): Document = | ||||
|     Jsoup.parse(String(body!!.bytes(), GB2312), request.url.toString()) | ||||
| 
 | ||||
| internal fun SManga.urlWithCheck(): String { | ||||
|     val result = url | ||||
|     if (result.endsWith("/index.html")) { | ||||
|         throw Exception("作品地址格式过期,请迁移更新") | ||||
|     } | ||||
|     return result | ||||
| } | ||||
| 
 | ||||
| internal val isNewDateLogic = AppInfo.getVersionCode() >= 81 | ||||
| 
 | ||||
| internal val dateFormat by lazy { | ||||
|     SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ENGLISH) | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 stevenyomi
						stevenyomi