HattoriManga: Theme change (#2960)

* Migrate HattoriManga

* Cleanup

* Sorted chapters

* Cleanup

* Add searchManga

* Cleanup

* Remove unneeded code

* Add search by intent

* Cleanup

* Add headers request

* Fix names

* Add nextPage

* Add limit in nextPage

* Fix AndroidManifest reference

* Move rateLimit

* Move fetchLatestUpdates parse to latestUpdatesParse

* Remove setUrlWithoutDomain

* Implements HttpSource

* Cleanup

* Fix author, artist and genres from mangaDetails

* Update src/tr/hattorimanga/src/eu/kanade/tachiyomi/extension/tr/hattorimanga/HattoriManga.kt

* Update src/tr/hattorimanga/src/eu/kanade/tachiyomi/extension/tr/hattorimanga/HattoriManga.kt

---------

Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
This commit is contained in:
Chopper 2024-05-13 12:34:28 -03:00 committed by Draff
parent 1ef8153eca
commit f3314852bd
5 changed files with 391 additions and 15 deletions

View 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=".tr.hattorimanga.HattoriMangaUrlActivity"
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="hattorimanga.com"
android:pathPattern="/manga/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,9 +1,7 @@
ext {
extName = 'Hattori Manga'
extClass = '.HattoriManga'
themePkg = 'madara'
baseUrl = 'https://hattorimanga.com'
overrideVersionCode = 0
extVersionCode = 37
isNsfw = true
}

View File

@ -1,23 +1,285 @@
package eu.kanade.tachiyomi.extension.tr.hattorimanga
import eu.kanade.tachiyomi.multisrc.madara.Madara
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class HattoriManga : Madara(
"Hattori Manga",
"https://hattorimanga.com",
"tr",
SimpleDateFormat("d MMM yyy", Locale("tr")),
) {
override fun pageListParse(document: Document): List<Page> {
val blocked = document.selectFirst(".content-blocked")
if (blocked != null) {
throw Exception(blocked.text()) // Bu bölümü okumak için Üye olmanız gerekiyor.
class HattoriManga : HttpSource() {
override val name: String = "Hattori Manga"
override val baseUrl: String = "https://hattorimanga.com"
override val lang: String = "tr"
override val supportsLatest: Boolean = true
override val versionId: Int = 2
private val json: Json by injectLazy()
private var csrfToken: String = ""
private var genresList: List<Genre> = emptyList()
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor { chain ->
val request = chain.request()
if (!request.url.toString().contains("manga/search")) {
return@addInterceptor chain.proceed(request)
}
val req = request.newBuilder()
.addHeader("X-Requested-With", "XMLHttpRequest")
.build()
if (csrfToken.isEmpty()) {
getCsrftoken()
}
val query = request.url.fragment!!
val response = chain.proceed(addFormBody(req, query))
with(response) {
return@addInterceptor when {
isPageExpired() -> {
close()
getCsrftoken()
chain.proceed(addFormBody(req, query))
}
else -> this
}
}
}
.rateLimit(4)
.build()
private fun addFormBody(request: Request, query: String): Request {
val body = FormBody.Builder()
.add("_token", csrfToken)
.add("query", query)
.build()
return request.newBuilder()
.url(request.url.toString().substringBefore("#"))
.post(body)
.build()
}
private fun getCsrftoken() {
val response = client.newCall(GET(baseUrl, headers)).execute()
val document = response.asJsoup()
csrfToken = document.selectFirst("meta[name=csrf-token]")!!.attr("content")
}
override fun chapterListParse(response: Response) =
throw UnsupportedOperationException()
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val slug = manga.url.substringAfterLast('/')
val chapters = mutableListOf<SChapter>()
var page = 1
do {
val dto = fetchChapterPageableList(slug, page, manga)
chapters += dto.chapters.map {
SChapter.create().apply {
name = it.title
date_upload = it.date.toDate()
url = "${manga.url}/${it.chapterSlug}"
}
}
page = dto.nextPage()
} while (dto.hasNextPage())
return Observable.just(chapters)
}
private fun fetchChapterPageableList(slug: String, page: Int, manga: SManga): HMChapterDto =
client.newCall(GET("$baseUrl/load-more-chapters/$slug?page=$page", headers))
.execute()
.parseAs<HMChapterDto>()
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-chapters")
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
return SManga.create().apply {
title = document.selectFirst("h3")!!.text()
thumbnail_url = document.selectFirst(".set-bg")?.absUrl("data-setbg")
description = document.selectFirst(".anime-details-text p")?.text()
author = document.selectFirst(".anime-details-widget li:has(span:contains(Yazar))")?.ownText()
artist = document.selectFirst(".anime-details-widget li:has(span:contains(Çizer))")?.ownText()
genre = document.selectFirst(".anime-details-widget li:has(span:contains(Etiketler))")
?.ownText()
?.split(",")
?.map { it.trim() }
?.joinToString()
setUrlWithoutDomain(document.location())
}
}
override fun pageListParse(response: Response): List<Page> {
return response.asJsoup().select(".image-wrapper img").mapIndexed { index, element ->
Page(index, imageUrl = "$baseUrl${element.attr("data-src")}")
}.takeIf { it.isNotEmpty() } ?: throw Exception("Oturum açmanız, WebView'ı açmanız ve oturum açmanız gerekir")
}
override fun latestUpdatesParse(response: Response): MangasPage {
return response.use {
val mangas = it.parseAs<HMLatestUpdateDto>().chapters.map {
SManga.create().apply {
val manga = it.manga
title = manga.title
thumbnail_url = "$baseUrl/storage/${manga.thumbnail}"
url = "/manga/${manga.slug}"
}
}.distinctBy { manga -> manga.title }
MangasPage(mangas, false)
}
}
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
if (genresList.isEmpty()) {
genresList = parseGenres(document)
}
return super.pageListParse(document)
val mangas = document
.select(".product-card.grow-box")
.map(::mangaFromElement)
return MangasPage(
mangas = mangas,
hasNextPage = document.selectFirst(".pagination .page-item:last-child:not(.disabled)") != null,
)
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga?page=$page", headers)
override fun searchMangaParse(response: Response) = popularMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val request = POST("$baseUrl/manga/search#$query", headers)
if (query.isNotBlank()) {
return request
}
val url = "$baseUrl/manga-index".toHttpUrl().newBuilder()
val selection = filters.filterIsInstance<GenreList>()
.flatMap { it.state }
.filter { it.state }
return when {
selection.isNotEmpty() -> {
selection.forEach { genre ->
url.addQueryParameter("genres[]", genre.id)
}
url.addQueryParameter("page", "$page")
GET(url.build(), headers)
}
else -> request
}
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(SEARCH_PREFIX)) {
val slug = query.removePrefix(SEARCH_PREFIX)
return client.newCall(GET("$baseUrl/$slug", headers))
.asObservableSuccess()
.map {
MangasPage(listOf(mangaDetailsParse(it)), false)
}
}
val request = searchMangaRequest(page, query, filters)
if (request.url.toString().contains("manga-index")) {
return super.fetchSearchManga(page, query, filters)
}
return client.newCall(request).asObservableSuccess().map { response ->
val mangas = response.parseAs<List<SearchManga>>().map {
SManga.create().apply {
title = it.title
description = it.description
author = it.author
artist = it.artist
thumbnail_url = "$baseUrl/storage/${it.thumbnail}"
url = "/manga/${it.slug}"
}
}
MangasPage(mangas, false)
}
}
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>()
filters += if (genresList.isNotEmpty()) {
GenreList("Türler", genresList)
} else {
Filter.Header("Türleri göstermeyi denemek için 'Sıfırla' düğmesine basın")
}
return FilterList(filters)
}
override fun imageUrlParse(response: Response) = ""
private fun mangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst("h5")!!.text()
thumbnail_url = element.selectFirst(".img-con")?.absUrl("data-setbg")
genre = element.select(".product-card-con ul li").joinToString { it.text() }
val script = element.attr("onclick")
setUrlWithoutDomain(REGEX_MANGA_URL.find(script)!!.groups["url"]!!.value)
}
private fun parseGenres(document: Document): List<Genre> {
return document.select(".tags-blog a")
.map { element -> Genre(element.text()) }
}
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
private fun Response.isPageExpired() = code == 419
private fun String.toDate(): Long =
try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
class GenreList(title: String, genres: List<Genre>) : Filter.Group<GenreCheckBox>(title, genres.map { GenreCheckBox(it.name, it.id) })
class GenreCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
class Genre(val name: String, val id: String = name)
companion object {
const val SEARCH_PREFIX = "slug:"
val REGEX_MANGA_URL = """='(?<url>[^']+)""".toRegex()
val dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.US)
}
}

View File

@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.extension.tr.hattorimanga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.math.min
@Serializable
class HMChapterDto(
val chapters: List<ChapterDto>,
val currentPage: Int,
val lastPage: Int,
) {
fun hasNextPage(): Boolean = currentPage < lastPage
fun nextPage(): Int = min(lastPage, currentPage + 1)
}
@Serializable
class ChapterDto(
@SerialName("title")
val title: String,
@SerialName("manga_slug")
val slug: String,
@SerialName("chapter_slug")
val chapterSlug: String,
@SerialName("formattedUploadTime")
val date: String,
)
@Serializable
class HMLatestUpdateDto(
val chapters: List<ChapterMangaDto>,
)
@Serializable
class ChapterMangaDto(
val manga: LatestUpdateDto,
)
@Serializable
class LatestUpdateDto(
val title: String,
val slug: String,
@SerialName("cover_image")
val thumbnail: String,
)
@Serializable
class SearchManga(
val slug: String,
val title: String,
val description: String,
@SerialName("cover_image")
val thumbnail: String,
val author: String,
val artist: String,
)

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.extension.tr.hattorimanga
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 HattoriMangaUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${HattoriManga.SEARCH_PREFIX}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(tag, e.toString())
}
} else {
Log.e(tag, "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}