HuntersScans: migrate source (#1650)
* Migrate HuntersScans * Add filter to novels * Fix names * Add newline Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Remove unneeded map function Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Replace statusMap with a 'when' statement Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Add new line in build.gradle Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Move mangaSubString content to in line * Replace fetch(Manga|Update|Search) overrides to Parses * Remove unneeded try/catch * Cleanup * Rename some functions * Remove override id * Fix extra chapters and remove a possible infinite loop * Remove unneeded regex calls * Remove unneeded conditional * Cleanup * Fix: filter to remove novels --------- Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									33f95f820c
								
							
						
					
					
						commit
						2a0588e8d9
					
				
							
								
								
									
										22
									
								
								src/pt/huntersscans/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/pt/huntersscans/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  | 
 | ||||||
|  |     <application> | ||||||
|  |         <activity | ||||||
|  |             android:name=".pt.huntersscans.HuntersScansUrlActivity" | ||||||
|  |             android:excludeFromRecents="true" | ||||||
|  |             android:exported="true" | ||||||
|  |             android:theme="@android:style/Theme.NoDisplay"> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="android.intent.action.VIEW" /> | ||||||
|  | 
 | ||||||
|  |                 <category android:name="android.intent.category.DEFAULT" /> | ||||||
|  |                 <category android:name="android.intent.category.BROWSABLE" /> | ||||||
|  | 
 | ||||||
|  |                 <data android:host="huntersscan.xyz" /> | ||||||
|  |                 <data android:scheme="https" /> | ||||||
|  |                 <data android:pathPattern="/manga/..*" /> | ||||||
|  |             </intent-filter> | ||||||
|  |         </activity> | ||||||
|  |     </application> | ||||||
|  | </manifest> | ||||||
| @ -1,10 +1,11 @@ | |||||||
| ext { | ext { | ||||||
|     extName = 'Hunters Scans' |     extName = 'Hunters Scans' | ||||||
|     extClass = '.HuntersScans' |     extClass = '.HuntersScans' | ||||||
|     themePkg = 'madara' |     extVersionCode = 36 | ||||||
|     baseUrl = 'https://huntersscan.xyz' |  | ||||||
|     overrideVersionCode = 0 |  | ||||||
|     isNsfw = true |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| apply from: "$rootDir/common.gradle" | apply from: "$rootDir/common.gradle" | ||||||
|  | 
 | ||||||
|  | dependencies { | ||||||
|  |     implementation(project(':lib:randomua')) | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,23 +1,253 @@ | |||||||
| package eu.kanade.tachiyomi.extension.pt.huntersscans | package eu.kanade.tachiyomi.extension.pt.huntersscans | ||||||
| 
 | 
 | ||||||
| import eu.kanade.tachiyomi.multisrc.madara.Madara | import android.app.Application | ||||||
| import eu.kanade.tachiyomi.network.interceptor.rateLimit | import android.content.SharedPreferences | ||||||
|  | import android.util.Log | ||||||
|  | import androidx.preference.PreferenceScreen | ||||||
|  | import eu.kanade.tachiyomi.lib.randomua.addRandomUAPreferenceToScreen | ||||||
|  | import eu.kanade.tachiyomi.lib.randomua.getPrefCustomUA | ||||||
|  | import eu.kanade.tachiyomi.lib.randomua.getPrefUAType | ||||||
|  | import eu.kanade.tachiyomi.lib.randomua.setRandomUserAgent | ||||||
|  | import eu.kanade.tachiyomi.network.GET | ||||||
|  | import eu.kanade.tachiyomi.network.asObservableSuccess | ||||||
|  | import eu.kanade.tachiyomi.network.interceptor.rateLimitHost | ||||||
|  | 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 eu.kanade.tachiyomi.util.asJsoup | ||||||
|  | import okhttp3.HttpUrl | ||||||
|  | import okhttp3.HttpUrl.Companion.toHttpUrl | ||||||
| import okhttp3.OkHttpClient | import okhttp3.OkHttpClient | ||||||
| import java.text.SimpleDateFormat | import okhttp3.Request | ||||||
|  | import okhttp3.Response | ||||||
|  | import org.jsoup.nodes.Document | ||||||
|  | import org.jsoup.nodes.Element | ||||||
|  | import rx.Observable | ||||||
|  | import uy.kohesive.injekt.Injekt | ||||||
|  | import uy.kohesive.injekt.api.get | ||||||
| import java.util.Locale | import java.util.Locale | ||||||
| import java.util.concurrent.TimeUnit | import java.util.concurrent.TimeUnit | ||||||
| 
 | 
 | ||||||
| class HuntersScans : Madara( | class HuntersScans : ParsedHttpSource(), ConfigurableSource { | ||||||
|     "Hunters Scan", |     override val name = "Hunters Scans" | ||||||
|     "https://huntersscan.xyz/", |  | ||||||
|     "pt-BR", |  | ||||||
|     SimpleDateFormat("dd 'de' MMMMM 'de' yyyy", Locale("pt", "BR")), |  | ||||||
| ) { |  | ||||||
| 
 | 
 | ||||||
|     override val client: OkHttpClient = super.client.newBuilder() |     override val lang = "pt-BR" | ||||||
|         .rateLimit(1, 2, TimeUnit.SECONDS) | 
 | ||||||
|  |     override val supportsLatest = true | ||||||
|  | 
 | ||||||
|  |     override val baseUrl = "https://huntersscan.xyz" | ||||||
|  | 
 | ||||||
|  |     override val versionId = 2 | ||||||
|  | 
 | ||||||
|  |     private val preferences: SharedPreferences = | ||||||
|  |         Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) | ||||||
|  | 
 | ||||||
|  |     override val client: OkHttpClient = | ||||||
|  |         network.cloudflareClient.newBuilder() | ||||||
|  |             .setRandomUserAgent( | ||||||
|  |                 preferences.getPrefUAType(), | ||||||
|  |                 preferences.getPrefCustomUA(), | ||||||
|  |             ) | ||||||
|  |             .rateLimitHost(baseUrl.toHttpUrl(), 1, 2, TimeUnit.SECONDS) | ||||||
|             .build() |             .build() | ||||||
| 
 | 
 | ||||||
|     override val useNewChapterEndpoint = true |     override fun setupPreferenceScreen(screen: PreferenceScreen) { | ||||||
|     override val useLoadMoreRequest = LoadMoreStrategy.Always |         addRandomUAPreferenceToScreen(screen) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun chapterFromElement(element: Element) = throw UnsupportedOperationException() | ||||||
|  | 
 | ||||||
|  |     private fun fetchChapterList(url: HttpUrl, page: Int): List<SChapter> { | ||||||
|  |         return try { | ||||||
|  |             val mangaPaged = url.newBuilder() | ||||||
|  |                 .addQueryParameter("page", "$page") | ||||||
|  |                 .build() | ||||||
|  |             chapterListParseFromJS(client.newCall(GET(mangaPaged, headers)).execute()) | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             Log.e("HuntersScans", e.toString()) | ||||||
|  |             emptyList() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun chapterListParseFromJS(response: Response): List<SChapter> { | ||||||
|  |         val jScript = response.asJsoup().select(chapterListSelector()) | ||||||
|  |             .map { element -> element.html() } | ||||||
|  |             .filter { element -> element.isNotEmpty() } | ||||||
|  |             .first { chapterRegex.find(it) != null } | ||||||
|  | 
 | ||||||
|  |         val chaptersLinks = chapterRegex.findAll(jScript) | ||||||
|  |             .flatMap { result -> result.groups.mapNotNull { it?.value } } | ||||||
|  |             .toSet() | ||||||
|  | 
 | ||||||
|  |         return chaptersLinks.map { chapterLink -> | ||||||
|  |             SChapter.create().apply { | ||||||
|  |                 name = chapterLink.toChapterName() | ||||||
|  |                 setUrlWithoutDomain(chapterLink.toChapterAbsUrl()) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun containsDuplicate(chapters: List<SChapter>): Boolean { | ||||||
|  |         return chapters.size != chapters.distinctBy { it.name }.size | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun chapterListParse(response: Response): List<SChapter> { | ||||||
|  |         val chapters = mutableListOf<SChapter>() | ||||||
|  |         val alwaysVisibleChapters = mutableSetOf<SChapter>() | ||||||
|  | 
 | ||||||
|  |         val origin = response.request.url | ||||||
|  |         var currentPage = 1 | ||||||
|  | 
 | ||||||
|  |         do { | ||||||
|  |             val chapterList = fetchChapterList(origin, currentPage) | ||||||
|  |             if (chapterList.size <= 2) { | ||||||
|  |                 chapters += chapterList | ||||||
|  |                 break | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             chapters += chapterList.sortedBy { it.name.toFloat() } | ||||||
|  |             alwaysVisibleChapters += chapters.removeFirst() | ||||||
|  |             alwaysVisibleChapters += chapters.removeLast() | ||||||
|  | 
 | ||||||
|  |             currentPage++ | ||||||
|  |         } | ||||||
|  |         while (!containsDuplicate(chapters)) | ||||||
|  | 
 | ||||||
|  |         chapters += alwaysVisibleChapters | ||||||
|  | 
 | ||||||
|  |         return chapters | ||||||
|  |             .distinctBy { it.name } | ||||||
|  |             .sortedBy { it.name.toFloat() } | ||||||
|  |             .reversed() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun chapterListSelector() = "script" | ||||||
|  | 
 | ||||||
|  |     override fun imageUrlParse(document: Document) = "" | ||||||
|  | 
 | ||||||
|  |     override fun latestUpdatesFromElement(element: Element) = | ||||||
|  |         SManga.create().apply { | ||||||
|  |             val type = element.selectFirst("span")!!.ownText().toCapitalize() | ||||||
|  |             title = "${element.selectFirst("h3")!!.ownText()} - $type" | ||||||
|  |             thumbnail_url = element.selectFirst("img")?.absUrl("src") | ||||||
|  |             setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href")) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() | ||||||
|  | 
 | ||||||
|  |     override fun latestUpdatesParse(response: Response) = | ||||||
|  |         super.latestUpdatesParse(response).removeNovels() | ||||||
|  | 
 | ||||||
|  |     override fun latestUpdatesRequest(page: Int): Request { | ||||||
|  |         val url = "$baseUrl/ultimas-atualizacoes".toHttpUrl().newBuilder() | ||||||
|  |             .addQueryParameter("page", "$page") | ||||||
|  |             .build() | ||||||
|  |         return GET(url, headers) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun latestUpdatesSelector() = "main > div div:nth-child(2) > div.relative" | ||||||
|  | 
 | ||||||
|  |     override fun mangaDetailsParse(document: Document) = | ||||||
|  |         SManga.create().apply { | ||||||
|  |             val container = document.selectFirst("div.container")!! | ||||||
|  |             val type = container.selectFirst("ul > li:nth-child(1) p")!!.ownText().toCapitalize() | ||||||
|  |             title = "${container.selectFirst("h2")!!.ownText()} - $type" | ||||||
|  |             thumbnail_url = container.selectFirst("img")?.absUrl("src") | ||||||
|  |             genre = container.select("ul > li:nth-child(5) p").joinToString { it.ownText() } | ||||||
|  | 
 | ||||||
|  |             val statusLabel = container.selectFirst("ul > li:nth-child(3) p")?.ownText() | ||||||
|  |             status = when (statusLabel) { | ||||||
|  |                 "ongoing" -> SManga.ONGOING | ||||||
|  |                 else -> SManga.UNKNOWN | ||||||
|  |             } | ||||||
|  |             description = document.selectFirst("main > div > div")?.text() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     override fun pageListParse(document: Document) = | ||||||
|  |         document.select("main.container img") | ||||||
|  |             .mapIndexed { i, element -> Page(i, imageUrl = element.absUrl("src")) } | ||||||
|  | 
 | ||||||
|  |     override fun popularMangaFromElement(element: Element) = SManga.create().apply { | ||||||
|  |         val type = element.selectFirst("span")!!.ownText().toCapitalize() | ||||||
|  |         title = "${element.selectFirst("h2")!!.ownText()} - $type" | ||||||
|  |         thumbnail_url = element.selectFirst("img")?.absUrl("src") | ||||||
|  |         setUrlWithoutDomain(element.absUrl("href")) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun popularMangaNextPageSelector() = "li[aria-label='next page button']:not([aria-disabled])" | ||||||
|  | 
 | ||||||
|  |     override fun popularMangaParse(response: Response) = | ||||||
|  |         super.popularMangaParse(response).removeNovels() | ||||||
|  | 
 | ||||||
|  |     override fun popularMangaRequest(page: Int): Request { | ||||||
|  |         val url = "$baseUrl/manga".toHttpUrl().newBuilder() | ||||||
|  |             .addQueryParameter("page", "$page") | ||||||
|  |             .build() | ||||||
|  |         return GET(url, headers) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun popularMangaSelector() = "main > div a" | ||||||
|  | 
 | ||||||
|  |     override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) | ||||||
|  | 
 | ||||||
|  |     override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() | ||||||
|  | 
 | ||||||
|  |     override fun searchMangaParse(response: Response) = | ||||||
|  |         super.searchMangaParse(response).removeNovels() | ||||||
|  | 
 | ||||||
|  |     override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { | ||||||
|  |         val url = "$baseUrl/manga".toHttpUrl().newBuilder() | ||||||
|  |             .addQueryParameter("q", query) | ||||||
|  |             .addQueryParameter("page", "$page") | ||||||
|  |             .build() | ||||||
|  |         return GET(url, headers) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     override fun searchMangaSelector() = popularMangaSelector() | ||||||
|  | 
 | ||||||
|  |     override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { | ||||||
|  |         if (query.startsWith(slugPrefix)) { | ||||||
|  |             val mangaUrl = "/manga/${query.substringAfter(slugPrefix)}" | ||||||
|  |             return client.newCall(GET("$baseUrl$mangaUrl", headers)) | ||||||
|  |                 .asObservableSuccess().map { response -> | ||||||
|  |                     val manga = mangaDetailsParse(response).apply { | ||||||
|  |                         url = mangaUrl | ||||||
|  |                     } | ||||||
|  |                     MangasPage(listOf(manga), false) | ||||||
|  |                 } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return super.fetchSearchManga(page, query, filters) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun MangasPage.removeNovels(): MangasPage { | ||||||
|  |         return MangasPage( | ||||||
|  |             mangas = this.mangas.filter { !it.title.lowercase(Locale.ROOT).contains("novel") }, | ||||||
|  |             hasNextPage = this.hasNextPage, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun String.toCapitalize() = | ||||||
|  |         trim().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } | ||||||
|  | 
 | ||||||
|  |     private fun String.toChapterName(): String { | ||||||
|  |         return try { | ||||||
|  |             val matches = chapterNameRegex.find(trim())?.groupValues ?: emptyList() | ||||||
|  |             matches.last() | ||||||
|  |                 .replace(" ", "") | ||||||
|  |                 .replace("-", ".") | ||||||
|  |         } catch (e: Exception) { "0" } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun String.toChapterAbsUrl() = "$baseUrl${trim()}" | ||||||
|  | 
 | ||||||
|  |     companion object { | ||||||
|  |         val chapterRegex = """/ler/[\w+-]+-capitulo-[\d.-]+""".toRegex() | ||||||
|  |         val chapterNameRegex = """capitulo-([\d-.]+)""".toRegex() | ||||||
|  |         val slugPrefix = "slug:" | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,36 @@ | |||||||
|  | package eu.kanade.tachiyomi.extension.pt.huntersscans | ||||||
|  | 
 | ||||||
|  | import android.app.Activity | ||||||
|  | import android.content.ActivityNotFoundException | ||||||
|  | import android.content.Intent | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.util.Log | ||||||
|  | import kotlin.system.exitProcess | ||||||
|  | 
 | ||||||
|  | class HuntersScansUrlActivity : Activity() { | ||||||
|  |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|  |         super.onCreate(savedInstanceState) | ||||||
|  |         val pathSegments = intent?.data?.pathSegments | ||||||
|  | 
 | ||||||
|  |         if (pathSegments != null && pathSegments.size > 2) { | ||||||
|  |             val intent = Intent().apply { | ||||||
|  |                 action = "eu.kanade.tachiyomi.SEARCH" | ||||||
|  |                 putExtra("query", slug(pathSegments)) | ||||||
|  |                 putExtra("filter", packageName) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  |                 startActivity(intent) | ||||||
|  |             } catch (e: ActivityNotFoundException) { | ||||||
|  |                 Log.e("HuntersScansUrlActivity", e.toString()) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             Log.e("HuntersScansUrlActivity", "Could not parse URI from intent $intent") | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         finish() | ||||||
|  |         exitProcess(0) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private fun slug(pathSegments: List<String>) = "${HuntersScans.slugPrefix}${pathSegments.last()}" | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Chopper
						Chopper