Add Hachiraw (#1096)

* Add Hachiraw

* Add URI intent handler

* Mark as NSFW
This commit is contained in:
beerpsi 2024-02-08 00:22:20 +07:00 committed by GitHub
parent 44e43bc26a
commit 393b282861
10 changed files with 339 additions and 0 deletions

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".ja.hachiraw.HachirawUrlActivity"
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="hachiraw.net"
android:pathPattern="/manga/..*"
android:scheme="https"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,8 @@
ext {
extName = "Hachiraw"
extClass = ".Hachiraw"
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@ -0,0 +1,186 @@
package eu.kanade.tachiyomi.extension.ja.hachiraw
import eu.kanade.tachiyomi.network.GET
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.ParsedHttpSource
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import java.text.SimpleDateFormat
import java.util.Locale
class Hachiraw : ParsedHttpSource() {
override val name = "Hachiraw"
override val baseUrl = "https://hachiraw.net"
override val lang = "ja"
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val dateFormat by lazy {
SimpleDateFormat("dd-MM-yyyy", Locale.ROOT)
}
override fun popularMangaRequest(page: Int) =
searchMangaRequest(
page,
"",
FilterList(SortFilter(2)),
)
override fun popularMangaSelector() = searchMangaSelector()
override fun popularMangaFromElement(element: Element) = searchMangaFromElement(element)
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
override fun latestUpdatesRequest(page: Int) =
searchMangaRequest(
page,
"",
FilterList(SortFilter(0)),
)
override fun latestUpdatesSelector() = searchMangaSelector()
override fun latestUpdatesFromElement(element: Element) = searchMangaFromElement(element)
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
return if (query.startsWith(PREFIX_SLUG_SEARCH)) {
val slug = query.removePrefix(PREFIX_SLUG_SEARCH)
val manga = SManga.create().apply { url = "/manga/$slug" }
fetchMangaDetails(manga)
.map {
it.url = "/manga/$slug"
MangasPage(listOf(it), false)
}
} else {
super.fetchSearchManga(page, query, filters)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val filterList = filters.ifEmpty { getFilterList() }
val sortFilter = filterList.filterIsInstance<SortFilter>().firstOrNull()
val genreFilter = filterList.filterIsInstance<GenreFilter>().firstOrNull()
val url = baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("list-manga")
if (query.isNotEmpty()) {
addQueryParameter("search", query)
} else if (genreFilter != null && genreFilter.state != 0) {
// Searching by genre is under /manga-list
setPathSegment(0, "manga-list")
addPathSegment(genreFilter.items[genreFilter.state].id)
}
if (page > 1) {
addPathSegment(page.toString())
}
if (sortFilter != null) {
addQueryParameter("order_by", sortFilter.items[sortFilter.state].id)
}
}.build()
return GET(url, headers)
}
override fun searchMangaSelector() = "div.ng-scope > div.top-15"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
element.selectFirst("a.ng-binding.SeriesName")!!.let {
setUrlWithoutDomain(it.attr("href"))
title = it.text()
}
thumbnail_url = element.selectFirst("img.img-fluid")?.absUrl("src")
}
override fun searchMangaNextPageSelector() = "ul.pagination li:contains(→)"
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val row = document.selectFirst("div.BoxBody > div.row")!!
title = row.selectFirst("h1")!!.text()
author = row.selectFirst("li.list-group-item:contains(著者)")?.ownText()
genre = row.select("li.list-group-item:contains(ジャンル) a").joinToString { it.text() }
thumbnail_url = row.selectFirst("img.img-fluid")?.absUrl("src")
description = buildString {
row.select("li.list-group-item:has(span.mlabel)").forEach {
val key = it.selectFirst("span")!!.text().removeSuffix(":")
val value = it.ownText()
if (key == "著者" || key == "ジャンル" || value.isEmpty() || value == "-") {
return@forEach
}
append(key)
append(": ")
appendLine(value)
}
val desc = row.select("div.Content").text()
if (desc.isNotEmpty()) {
appendLine()
append(desc)
}
}.trim()
}
override fun chapterListSelector() = "a.ChapterLink"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.selectFirst("span")!!.text()
date_upload = try {
val date = element.selectFirst("span.float-right")!!.text()
dateFormat.parse(date)!!.time
} catch (_: Exception) {
0L
}
}
override fun pageListParse(document: Document) =
document.select("#TopPage img").mapIndexed { i, it ->
Page(i, imageUrl = it.absUrl("src"))
}
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
override fun getFilterList() = FilterList(
// TODO: Not Google translate this
// "Genre filter is ignored when searching by title"
Filter.Header("タイトルで検索する場合、ジャンルフィルターは無視されます"),
Filter.Separator(),
SortFilter(),
GenreFilter(),
)
companion object {
internal const val PREFIX_SLUG_SEARCH = "slug:"
}
}

View File

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.extension.ja.hachiraw
import eu.kanade.tachiyomi.source.model.Filter
class SortFilter(state: Int = 2) : SelectFilter(
"Sort By",
listOf(
Selection("最新", "lastest"),
Selection("A-Z", "name"),
Selection("最も多くのビュー", "views"),
),
state,
)
// https://hachiraw.net/list-manga
// copy([...document.querySelectorAll("#TypeShow a")].map((e) => `Genre("${e.textContent.trim()}", "${e.href.split("/").slice(-1)}"),`).join("\n"))
class GenreFilter(state: Int = 0) : SelectFilter(
"Genres",
listOf(
// TODO: Is this the correct translation for "All"/"Everything"?
Selection("すべて", ""),
Selection("アクション", "action"),
Selection("メカ", "mecha"),
Selection("オルタナティブワールド", "alternative-world"),
Selection("神秘", "mystery"),
Selection("アダルト", "adult"),
Selection("ワンショット", "one-shot"),
Selection("アニメ", "anime"),
Selection("心理的", "psychological"),
Selection("コメディ", "comedy"),
Selection("ロマンス", "romance"),
Selection("漫画", "comic"),
Selection("学校生活", "school-life"),
Selection("同人誌", "doujinshi"),
Selection("SF", "sci-fi"),
Selection("ドラマ", "drama"),
Selection("青年", "seinen"),
Selection("エッチ", "Ecchi"),
Selection("少女", "Shoujo"),
Selection("ファンタジー", "Fantasy"),
Selection("少女愛", "shojou-ai"),
Selection("ジェンダーベンダー", "Gender-Bender"),
Selection("少年", "Shounen"),
Selection("ハーレム", "Harem"),
Selection("少年愛", "shounen-ai"),
Selection("歴史的", "historical"),
Selection("人生のひとこま", "slice-of-life"),
Selection("ホラー", "Horror"),
Selection("汚い", "Smut"),
Selection("じょうせい", "Josei"),
Selection("スポーツ", "Sports"),
Selection("ライブアクション", "live-action"),
Selection("超自然的な", "supernatural"),
Selection("マンファ", "Manhua"),
Selection("悲劇", "Tragedy"),
Selection("医学", "Medical"),
Selection("冒険", "Adventure"),
Selection("武道", "Martial-art"),
Selection("やおい", "Yaoi"),
Selection("成熟した", "Mature"),
Selection("異世界", "Isekai"),
Selection("魔法", "Magic"),
Selection("ロリコン", "Lolicon"),
Selection("ワンショット", "Oneshot"),
Selection("該当なし", "N-A"),
Selection("食べ物", "Food"),
Selection("ゲーム", "Game"),
Selection("戦争", "War"),
Selection("エルフ", "Elves"),
Selection("武道", "martial-arts"),
Selection("少女愛", "shoujo-ai"),
Selection("更新中", "updating"),
Selection("百合", "yuri"),
Selection("ショタコン", "shotacon"),
Selection("SMARTOON", "SMARTOON"),
),
state,
)
open class SelectFilter(name: String, val items: List<Selection>, state: Int = 0) : Filter.Select<String>(
name,
items.map { it.name }.toTypedArray(),
state,
)
data class Selection(
val name: String,
val id: String,
)

View File

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.extension.ja.hachiraw
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 HachirawUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val intent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Hachiraw.PREFIX_SLUG_SEARCH}${pathSegments[1]}")
putExtra("filter", packageName)
}
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
Log.e("HachirawUrlActivity", "Could not start activity", e)
}
} else {
Log.e("HachirawUrlActivity", "Could not parse URI from intent $intent")
}
finish()
exitProcess(0)
}
}