add Wolfdotcom (#6534)
* wolfdotcom * fix selectors * domain preference and auto update * update domain number * auto update domain in ci * update regex * use * current domain number * don't set chapter number as it is more of an index than actual chapter num
This commit is contained in:
		
							parent
							
								
									bb2e8d2cde
								
							
						
					
					
						commit
						6b8b650004
					
				
							
								
								
									
										53
									
								
								src/ko/wolfdotcom/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/ko/wolfdotcom/build.gradle
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					ext {
 | 
				
			||||||
 | 
					    extName = 'Wolf.com'
 | 
				
			||||||
 | 
					    extClass = '.WolfFactory'
 | 
				
			||||||
 | 
					    extVersionCode = 1
 | 
				
			||||||
 | 
					    isNsfw = true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					apply from: "$rootDir/common.gradle"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def domainNumberFileName = "src/ko/wolfdotcom/src/eu/kanade/tachiyomi/extension/ko/wolfdotcom/DomainNumber.kt"
 | 
				
			||||||
 | 
					def domainNumberFile = new File(domainNumberFileName)
 | 
				
			||||||
 | 
					def backupFile = new File(domainNumberFileName + "_bak")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tasks.register('updateDomainNumber') {
 | 
				
			||||||
 | 
					    doLast {
 | 
				
			||||||
 | 
					        def domainNumber = -1
 | 
				
			||||||
 | 
					        def response = new URL("https://nicelink52.com/").text
 | 
				
			||||||
 | 
					        def matcher = response =~ ~/https?:\/\/wfwf(\d+)\.com/
 | 
				
			||||||
 | 
					        if (matcher) {
 | 
				
			||||||
 | 
					            domainNumber = matcher[0][1]
 | 
				
			||||||
 | 
					            println("[Wolf.com] new domain number: $domainNumber")
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            println("[Wolf.com] domain number not found")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (domainNumber != -1) {
 | 
				
			||||||
 | 
					            domainNumberFile.renameTo(backupFile)
 | 
				
			||||||
 | 
					            domainNumberFile.withPrintWriter {
 | 
				
			||||||
 | 
					                it.println("// THIS FILE IS AUTO-GENERATED, DO NOT COMMIT")
 | 
				
			||||||
 | 
					                it.println("package eu.kanade.tachiyomi.extension.ko.wolfdotcom")
 | 
				
			||||||
 | 
					                it.println("const val DEFAULT_DOMAIN_NUMBER = \"$domainNumber\"")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					preBuild.dependsOn updateDomainNumber
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tasks.register('restoreBackup') {
 | 
				
			||||||
 | 
					    doLast {
 | 
				
			||||||
 | 
					        if (backupFile.exists()) {
 | 
				
			||||||
 | 
					            println("[Wolf.com] Restoring placeholder file")
 | 
				
			||||||
 | 
					            domainNumberFile.delete()
 | 
				
			||||||
 | 
					            backupFile.renameTo(domainNumberFile)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tasks.configureEach { task ->
 | 
				
			||||||
 | 
					    if (task.name == "assembleDebug" || task.name == "assembleRelease") {
 | 
				
			||||||
 | 
					        task.finalizedBy(restoreBackup)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								src/ko/wolfdotcom/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ko/wolfdotcom/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ko/wolfdotcom/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ko/wolfdotcom/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ko/wolfdotcom/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ko/wolfdotcom/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ko/wolfdotcom/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ko/wolfdotcom/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 6.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ko/wolfdotcom/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ko/wolfdotcom/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 8.6 KiB  | 
@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ko.wolfdotcom
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const val DEFAULT_DOMAIN_NUMBER = "363"
 | 
				
			||||||
@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ko.wolfdotcom
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.Filter
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.FilterList
 | 
				
			||||||
 | 
					import okhttp3.HttpUrl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface UrlPartFilter {
 | 
				
			||||||
 | 
					    fun addToUrl(url: HttpUrl.Builder)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FilterData(
 | 
				
			||||||
 | 
					    val type: String,
 | 
				
			||||||
 | 
					    private val typeDisplayName: String? = null,
 | 
				
			||||||
 | 
					    val value: String?,
 | 
				
			||||||
 | 
					    private val valueDisplayName: String,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    override fun toString(): String {
 | 
				
			||||||
 | 
					        return "$typeDisplayName: $valueDisplayName"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SearchFilter(
 | 
				
			||||||
 | 
					    private val options: List<FilterData>,
 | 
				
			||||||
 | 
					) : Filter.Select<String>(
 | 
				
			||||||
 | 
					    "필터",
 | 
				
			||||||
 | 
					    options.map { it.toString() }.toTypedArray(),
 | 
				
			||||||
 | 
					),
 | 
				
			||||||
 | 
					    UrlPartFilter {
 | 
				
			||||||
 | 
					    override fun addToUrl(url: HttpUrl.Builder) {
 | 
				
			||||||
 | 
					        val selected = options[state]
 | 
				
			||||||
 | 
					        url.addQueryParameter("type1", selected.type)
 | 
				
			||||||
 | 
					        selected.value?.let {
 | 
				
			||||||
 | 
					            url.addQueryParameter("type2", it)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SortFilter(default: Int = 0) :
 | 
				
			||||||
 | 
					    Filter.Select<String>(
 | 
				
			||||||
 | 
					        "정렬 기준",
 | 
				
			||||||
 | 
					        options.map { it.first }.toTypedArray(),
 | 
				
			||||||
 | 
					        default,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    UrlPartFilter {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun addToUrl(url: HttpUrl.Builder) {
 | 
				
			||||||
 | 
					        url.addQueryParameter("o", options[state].second)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    companion object {
 | 
				
			||||||
 | 
					        private val options = listOf(
 | 
				
			||||||
 | 
					            "최신순" to "n",
 | 
				
			||||||
 | 
					            "인기순" to "f",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					val POPULAR = FilterList(SortFilter(1))
 | 
				
			||||||
 | 
					val LATEST = FilterList(SortFilter(0))
 | 
				
			||||||
@ -0,0 +1,417 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ko.wolfdotcom
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.app.Application
 | 
				
			||||||
 | 
					import android.content.SharedPreferences
 | 
				
			||||||
 | 
					import android.util.Log
 | 
				
			||||||
 | 
					import androidx.preference.EditTextPreference
 | 
				
			||||||
 | 
					import androidx.preference.PreferenceScreen
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.network.GET
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.network.asObservableSuccess
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.ConfigurableSource
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.Filter
 | 
				
			||||||
 | 
					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 eu.kanade.tachiyomi.util.asJsoup
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					import kotlinx.serialization.decodeFromString
 | 
				
			||||||
 | 
					import kotlinx.serialization.encodeToString
 | 
				
			||||||
 | 
					import kotlinx.serialization.json.Json
 | 
				
			||||||
 | 
					import okhttp3.HttpUrl.Companion.toHttpUrl
 | 
				
			||||||
 | 
					import okhttp3.Interceptor
 | 
				
			||||||
 | 
					import okhttp3.Request
 | 
				
			||||||
 | 
					import okhttp3.Response
 | 
				
			||||||
 | 
					import org.jsoup.Jsoup
 | 
				
			||||||
 | 
					import org.jsoup.nodes.Document
 | 
				
			||||||
 | 
					import rx.Observable
 | 
				
			||||||
 | 
					import uy.kohesive.injekt.Injekt
 | 
				
			||||||
 | 
					import uy.kohesive.injekt.api.get
 | 
				
			||||||
 | 
					import uy.kohesive.injekt.injectLazy
 | 
				
			||||||
 | 
					import java.io.IOException
 | 
				
			||||||
 | 
					import java.net.URLEncoder
 | 
				
			||||||
 | 
					import java.text.ParseException
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat
 | 
				
			||||||
 | 
					import java.util.Locale
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					open class Wolf(
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    private val browsePath: String,
 | 
				
			||||||
 | 
					    private val entryPath: String,
 | 
				
			||||||
 | 
					    private val readerPath: String,
 | 
				
			||||||
 | 
					) : HttpSource(), ConfigurableSource {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val name = "늑대닷컴 - $name"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val lang = "ko"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val baseUrl: String
 | 
				
			||||||
 | 
					        get() = "https://wfwf$domainNumber.com"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val supportsLatest = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val client = network.cloudflareClient.newBuilder()
 | 
				
			||||||
 | 
					        .addInterceptor(::domainNumberInterceptor)
 | 
				
			||||||
 | 
					        .build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val json: Json by injectLazy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val preference: SharedPreferences by lazy {
 | 
				
			||||||
 | 
					        Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun fetchPopularManga(page: Int): Observable<MangasPage> {
 | 
				
			||||||
 | 
					        return fetchSearchManga(page, "", POPULAR)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
 | 
				
			||||||
 | 
					        return fetchSearchManga(page, "", LATEST)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private var searchFilters: List<FilterData> = emptyList()
 | 
				
			||||||
 | 
					    private var filterParseError = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun getFilterList(): FilterList {
 | 
				
			||||||
 | 
					        val filters: MutableList<Filter<*>> = mutableListOf(
 | 
				
			||||||
 | 
					            SortFilter(),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (searchFilters.isNotEmpty()) {
 | 
				
			||||||
 | 
					            filters.add(
 | 
				
			||||||
 | 
					                SearchFilter(searchFilters),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        } else if (filterParseError) {
 | 
				
			||||||
 | 
					            filters.add(
 | 
				
			||||||
 | 
					                Filter.Header("unable to parse filters"),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            filters.add(
 | 
				
			||||||
 | 
					                Filter.Header("press 'reset' to attempt to load more filters"),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return FilterList(filters)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected open fun parseSearchFilters(document: Document) {
 | 
				
			||||||
 | 
					        if (searchFilters.isNotEmpty() || filterParseError) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            val displayName = document.select(".sub-tab > a").eachText()
 | 
				
			||||||
 | 
					            assert(displayName.size == 3)
 | 
				
			||||||
 | 
					            searchFilters =
 | 
				
			||||||
 | 
					                document.select(".tab-day > a, .tab-genre1 > a, .tab-genre2 > a, .tab-alphabet > a")
 | 
				
			||||||
 | 
					                    .map {
 | 
				
			||||||
 | 
					                        val url = it.absUrl("href").toHttpUrl()
 | 
				
			||||||
 | 
					                        val type = url.queryParameter("type1")!!
 | 
				
			||||||
 | 
					                        FilterData(
 | 
				
			||||||
 | 
					                            type = type,
 | 
				
			||||||
 | 
					                            typeDisplayName = when (type) {
 | 
				
			||||||
 | 
					                                "day", "complete" -> displayName[0]
 | 
				
			||||||
 | 
					                                "genre" -> displayName[1]
 | 
				
			||||||
 | 
					                                "alphabet" -> displayName[2]
 | 
				
			||||||
 | 
					                                else -> null
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                            value = url.queryParameter("type2"),
 | 
				
			||||||
 | 
					                            valueDisplayName = it.ownText(),
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					        } catch (e: Throwable) {
 | 
				
			||||||
 | 
					            Log.e(name, "error parsing filters", e)
 | 
				
			||||||
 | 
					            filterParseError = true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private lateinit var browseCache: List<List<BrowseItem>>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class BrowseItem(
 | 
				
			||||||
 | 
					        val id: Int,
 | 
				
			||||||
 | 
					        val title: String,
 | 
				
			||||||
 | 
					        val cover: String?,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
 | 
				
			||||||
 | 
					        if (query.isNotBlank()) {
 | 
				
			||||||
 | 
					            return querySearch(query)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return if (page == 1) {
 | 
				
			||||||
 | 
					            client.newCall(searchMangaRequest(page, query, filters))
 | 
				
			||||||
 | 
					                .asObservableSuccess()
 | 
				
			||||||
 | 
					                .map {
 | 
				
			||||||
 | 
					                    parseBrowsePage(it)
 | 
				
			||||||
 | 
					                    paginatedBrowsePage(0)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Observable.just(
 | 
				
			||||||
 | 
					                paginatedBrowsePage(page - 1),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
				
			||||||
 | 
					        val url = "$baseUrl/$browsePath".toHttpUrl().newBuilder().apply {
 | 
				
			||||||
 | 
					            filters.filterIsInstance<UrlPartFilter>().forEach { filter ->
 | 
				
			||||||
 | 
					                filter.addToUrl(this)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }.build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return GET(url, headers)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun parseBrowsePage(response: Response) {
 | 
				
			||||||
 | 
					        val document = response.asJsoup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        parseSearchFilters(document)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        browseCache = document.select(".webtoon-list > ul > li > a").map {
 | 
				
			||||||
 | 
					            val id = it.absUrl("href").toHttpUrl()
 | 
				
			||||||
 | 
					                .queryParameter("toon")!!.toInt()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            BrowseItem(
 | 
				
			||||||
 | 
					                id = id,
 | 
				
			||||||
 | 
					                title = it.selectFirst(".txt > .subject")!!.ownText(),
 | 
				
			||||||
 | 
					                cover = it.selectFirst(".img > img")?.attr("data-original"),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }.chunked(20)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun paginatedBrowsePage(index: Int): MangasPage {
 | 
				
			||||||
 | 
					        return MangasPage(
 | 
				
			||||||
 | 
					            browseCache[index].map {
 | 
				
			||||||
 | 
					                SManga.create().apply {
 | 
				
			||||||
 | 
					                    url = it.id.toString()
 | 
				
			||||||
 | 
					                    title = it.title
 | 
				
			||||||
 | 
					                    thumbnail_url = it.cover
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            browseCache.lastIndex > index,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val specialChars = Regex("""[^\p{InHangul_Syllables}0-9a-z ]""", RegexOption.IGNORE_CASE)
 | 
				
			||||||
 | 
					    private val styleImage = Regex("""background-image:url\(([^)]+)\)""")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun querySearch(query: String): Observable<MangasPage> {
 | 
				
			||||||
 | 
					        if (query.length < 2) {
 | 
				
			||||||
 | 
					            throw Exception("두 글자 이상 입력 해주세요.")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        val stdQuery = query.replace(specialChars, "")
 | 
				
			||||||
 | 
					        val searchUrl = "$baseUrl/search.html?q=${URLEncoder.encode(stdQuery, "EUC-KR")}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return client.newCall(GET(searchUrl, headers))
 | 
				
			||||||
 | 
					            .asObservableSuccess()
 | 
				
			||||||
 | 
					            .map { response ->
 | 
				
			||||||
 | 
					                val document = Jsoup.parseBodyFragment(response.body.string(), searchUrl)
 | 
				
			||||||
 | 
					                val entries = document.select("article.searchItem")
 | 
				
			||||||
 | 
					                    .filter { el ->
 | 
				
			||||||
 | 
					                        el.selectFirst("a.searchLink")!!.attr("href").contains(entryPath)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    .map { el ->
 | 
				
			||||||
 | 
					                        val mangaUrl = el.selectFirst("a.searchLink")!!.absUrl("href")
 | 
				
			||||||
 | 
					                            .toHttpUrl()
 | 
				
			||||||
 | 
					                        SManga.create().apply {
 | 
				
			||||||
 | 
					                            url = mangaUrl.queryParameter("toon")!!
 | 
				
			||||||
 | 
					                            title = el.selectFirst(".searchDetailTitle")!!.text()
 | 
				
			||||||
 | 
					                            thumbnail_url = el.selectFirst(".searchPng")
 | 
				
			||||||
 | 
					                                ?.attr("style")
 | 
				
			||||||
 | 
					                                ?.let {
 | 
				
			||||||
 | 
					                                    styleImage.find(it)?.groupValues?.get(1)
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                MangasPage(entries, false)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun getMangaUrl(manga: SManga): String {
 | 
				
			||||||
 | 
					        return baseUrl.toHttpUrl().newBuilder()
 | 
				
			||||||
 | 
					            .addPathSegment(entryPath)
 | 
				
			||||||
 | 
					            .addQueryParameter("toon", manga.url)
 | 
				
			||||||
 | 
					            .toString()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun mangaDetailsRequest(manga: SManga): Request {
 | 
				
			||||||
 | 
					        return GET(getMangaUrl(manga), headers)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun mangaDetailsParse(response: Response): SManga {
 | 
				
			||||||
 | 
					        val document = response.asJsoup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return SManga.create().apply {
 | 
				
			||||||
 | 
					            title = document.selectFirst(".text-box h1")!!.text()
 | 
				
			||||||
 | 
					            thumbnail_url = document.selectFirst(".img-box img")?.absUrl("src")
 | 
				
			||||||
 | 
					            description = document.selectFirst(".text-box .txt")?.text()
 | 
				
			||||||
 | 
					            genre = document.selectFirst(".text-box .sub:has(> strong:contains(장르))")?.ownText()?.replace("/", ", ")
 | 
				
			||||||
 | 
					            author = document.selectFirst(".text-box .sub:has(> strong:contains(작가))")?.ownText()?.replace("/", ", ")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun chapterListRequest(manga: SManga): Request {
 | 
				
			||||||
 | 
					        return mangaDetailsRequest(manga)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Serializable
 | 
				
			||||||
 | 
					    class ChapterUrl(
 | 
				
			||||||
 | 
					        val toon: String,
 | 
				
			||||||
 | 
					        val num: String,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun chapterListParse(response: Response): List<SChapter> {
 | 
				
			||||||
 | 
					        val document = response.asJsoup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return document.select(".webtoon-bbs-list a.view_open").map { el ->
 | 
				
			||||||
 | 
					            val chapUrl = el.absUrl("href").toHttpUrl()
 | 
				
			||||||
 | 
					            SChapter.create().apply {
 | 
				
			||||||
 | 
					                url = json.encodeToString(
 | 
				
			||||||
 | 
					                    ChapterUrl(
 | 
				
			||||||
 | 
					                        chapUrl.queryParameter("toon")!!,
 | 
				
			||||||
 | 
					                        chapUrl.queryParameter("num")!!,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                name = el.selectFirst(".subject")!!.ownText()
 | 
				
			||||||
 | 
					                date_upload = el.selectFirst(".date")?.text().parseDate()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun String?.parseDate(): Long {
 | 
				
			||||||
 | 
					        this ?: return 0L
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return try {
 | 
				
			||||||
 | 
					            dateFormat.parse(this)!!.time
 | 
				
			||||||
 | 
					        } catch (_: ParseException) {
 | 
				
			||||||
 | 
					            0L
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun getChapterUrl(chapter: SChapter): String {
 | 
				
			||||||
 | 
					        val chapUrl = json.decodeFromString<ChapterUrl>(chapter.url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return baseUrl.toHttpUrl().newBuilder()
 | 
				
			||||||
 | 
					            .addPathSegment(readerPath)
 | 
				
			||||||
 | 
					            .addQueryParameter("toon", chapUrl.toon)
 | 
				
			||||||
 | 
					            .addQueryParameter("num", chapUrl.num)
 | 
				
			||||||
 | 
					            .toString()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun pageListRequest(chapter: SChapter): Request {
 | 
				
			||||||
 | 
					        return GET(getChapterUrl(chapter), headers)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun pageListParse(response: Response): List<Page> {
 | 
				
			||||||
 | 
					        val document = response.asJsoup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return document.select(".image-view img").mapIndexed { idx, img ->
 | 
				
			||||||
 | 
					            Page(idx, imageUrl = img.absUrl("data-original"))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun setupPreferenceScreen(screen: PreferenceScreen) {
 | 
				
			||||||
 | 
					        EditTextPreference(screen.context).apply {
 | 
				
			||||||
 | 
					            key = PREF_DOMAIN_NUM
 | 
				
			||||||
 | 
					            title = "도메인 번호"
 | 
				
			||||||
 | 
					            setOnPreferenceChangeListener { _, newValue ->
 | 
				
			||||||
 | 
					                val value = newValue as String
 | 
				
			||||||
 | 
					                if (value.isEmpty() || value.toIntOrNull() == null) {
 | 
				
			||||||
 | 
					                    false
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    domainNumber = value.trim()
 | 
				
			||||||
 | 
					                    false
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }.also(screen::addPreference)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private var domainNumber = ""
 | 
				
			||||||
 | 
					        get() {
 | 
				
			||||||
 | 
					            val currentValue = field
 | 
				
			||||||
 | 
					            if (currentValue.isNotEmpty()) return currentValue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val prefValue = preference.getString(PREF_DOMAIN_NUM, "")!!
 | 
				
			||||||
 | 
					            val prefDefaultValue = preference.getString(PREF_DOMAIN_NUM_DEFAULT, "")!!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (prefDefaultValue != DEFAULT_DOMAIN_NUMBER) {
 | 
				
			||||||
 | 
					                preference.edit()
 | 
				
			||||||
 | 
					                    .putString(PREF_DOMAIN_NUM_DEFAULT, DEFAULT_DOMAIN_NUMBER)
 | 
				
			||||||
 | 
					                    .putString(PREF_DOMAIN_NUM, DEFAULT_DOMAIN_NUMBER)
 | 
				
			||||||
 | 
					                    .apply()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                field = DEFAULT_DOMAIN_NUMBER
 | 
				
			||||||
 | 
					                return DEFAULT_DOMAIN_NUMBER
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (prefValue.isNotEmpty()) {
 | 
				
			||||||
 | 
					                field = prefValue
 | 
				
			||||||
 | 
					                return prefValue
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return DEFAULT_DOMAIN_NUMBER
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        set(value) {
 | 
				
			||||||
 | 
					            preference.edit().putString(PREF_DOMAIN_NUM, value).apply()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            field = value
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun domainNumberInterceptor(chain: Interceptor.Chain): Response {
 | 
				
			||||||
 | 
					        val request = chain.request()
 | 
				
			||||||
 | 
					        val response = chain.proceed(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val url = request.url.toString()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (url.contains(domainRegex)) {
 | 
				
			||||||
 | 
					            val document = Jsoup.parse(response.peekBody(Long.MAX_VALUE).string())
 | 
				
			||||||
 | 
					            val newUrl = document.selectFirst("""#pop-content a[href~=^https?://wfwf\d+\.com]""")
 | 
				
			||||||
 | 
					                ?: return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            response.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val newDomainNum = domainRegex.find(newUrl.attr("href"))?.groupValues?.get(1)
 | 
				
			||||||
 | 
					                ?: throw IOException("Failed to update domain number")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            domainNumber = newDomainNum.trim()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return chain.proceed(
 | 
				
			||||||
 | 
					                request.newBuilder()
 | 
				
			||||||
 | 
					                    .url(
 | 
				
			||||||
 | 
					                        request.url.newBuilder()
 | 
				
			||||||
 | 
					                            .host(baseUrl.toHttpUrl().host)
 | 
				
			||||||
 | 
					                            .build(),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    .build(),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return response
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val domainRegex = Regex("""^https?://wfwf(\d+)\.com""")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun imageUrlParse(response: Response): String {
 | 
				
			||||||
 | 
					        throw UnsupportedOperationException()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    override fun popularMangaParse(response: Response): MangasPage {
 | 
				
			||||||
 | 
					        throw UnsupportedOperationException()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    override fun popularMangaRequest(page: Int): Request {
 | 
				
			||||||
 | 
					        throw UnsupportedOperationException()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    override fun latestUpdatesParse(response: Response): MangasPage {
 | 
				
			||||||
 | 
					        throw UnsupportedOperationException()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    override fun latestUpdatesRequest(page: Int): Request {
 | 
				
			||||||
 | 
					        throw UnsupportedOperationException()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    override fun searchMangaParse(response: Response): MangasPage {
 | 
				
			||||||
 | 
					        throw UnsupportedOperationException()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private const val PREF_DOMAIN_NUM = "domain_number"
 | 
				
			||||||
 | 
					private const val PREF_DOMAIN_NUM_DEFAULT = "domain_number_default"
 | 
				
			||||||
@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ko.wolfdotcom
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.SourceFactory
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.FilterList
 | 
				
			||||||
 | 
					import org.jsoup.nodes.Document
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WolfFactory : SourceFactory {
 | 
				
			||||||
 | 
					    override fun createSources() = listOf(
 | 
				
			||||||
 | 
					        Wolf("웹툰", "ing", "list", "view"), // webtoon
 | 
				
			||||||
 | 
					        Wolf("만화책", "cm", "cl", "cv"), // comic book
 | 
				
			||||||
 | 
					        object : Wolf("포토툰", "pt", "list", "view") { // phototoon
 | 
				
			||||||
 | 
					            override fun getFilterList(): FilterList {
 | 
				
			||||||
 | 
					                return FilterList()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            override fun parseSearchFilters(document: Document) {
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user