Skip to content

Commit

Permalink
Feat(src/zh): Add new source Hanime1
Browse files Browse the repository at this point in the history
  • Loading branch information
Dark25 committed Oct 29, 2024
1 parent ade12c4 commit 5826738
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/zh/hanime1/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ext {
extName = 'Hanime1'
extClass = '.Hanime1'
extVersionCode = 1
isNsfw = true
}

apply from: "$rootDir/common.gradle"
Binary file added src/zh/hanime1/res/mipmap-hdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/zh/hanime1/res/mipmap-mdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/zh/hanime1/res/mipmap-xhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/zh/hanime1/res/mipmap-xxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/zh/hanime1/res/mipmap-xxxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.animeextension.zh.hanime1

import eu.kanade.tachiyomi.animesource.model.AnimeFilter

open class QueryFilter(name: String, val key: String, values: Array<String>) :
AnimeFilter.Select<String>(name, values) {
val selected: String
get() = if (state == 0) {
""
} else {
values[state]
}
}

open class TagFilter(val key: String, name: String, state: Boolean = false) :
AnimeFilter.CheckBox(name, state)

class GenreFilter(values: Array<String>) :
QueryFilter(
"影片類型",
"genre",
values.ifEmpty { arrayOf("全部", "裏番", "泡面番", "Motion Anime") },
)

class SortFilter(values: Array<String>) :
QueryFilter(
"排序方式",
"sort",
values.ifEmpty { arrayOf("最新上市", "最新上傳", "本日排行", "本週排行", "本月排行") },
)

object HotFilter : TagFilter("sort", "本周排行", true)

class YearFilter(values: Array<String>) :
QueryFilter("發佈年份", "year", values.ifEmpty { arrayOf("全部年份") })

class MonthFilter(values: Array<String>) :
QueryFilter("發佈月份", "month", values.ifEmpty { arrayOf("全部月份") })

class DateFilter(yearFilter: YearFilter, monthFilter: MonthFilter) :
AnimeFilter.Group<QueryFilter>("發佈日期", listOf(yearFilter, monthFilter))

class CategoryFilter(name: String, filters: List<TagFilter>) :
AnimeFilter.Group<TagFilter>(name, filters)

class BroadMatchFilter : TagFilter("broad", "廣泛配對")

class TagsFilter(filters: List<AnimeFilter<out Any>>) :
AnimeFilter.Group<AnimeFilter<out Any>>("標籤", filters)
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
package eu.kanade.tachiyomi.animeextension.zh.hanime1

import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale

enum class FilterUpdateState {
NONE,
UPDATING,
COMPLETED,
FAILED,
}

class Hanime1 : AnimeHttpSource(), ConfigurableAnimeSource {
override val baseUrl: String
get() = "https://hanime1.me"
override val lang: String
get() = "zh"
override val name: String
get() = "Hanime1.me"
override val supportsLatest: Boolean
get() = true

override val client =
network.client.newBuilder().addInterceptor(::checkFiltersInterceptor).build()

private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json by injectLazy<Json>()
private var filterUpdateState = FilterUpdateState.NONE
private val uploadDateFormat: SimpleDateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
}

override fun animeDetailsParse(response: Response): SAnime {
val jsoup = response.asJsoup()
return SAnime.create().apply {
genre = jsoup.select(".single-video-tag").not("[data-toggle]").eachText().joinToString()
author = jsoup.select("#video-artist-name").text()
jsoup.select("script[type=application/ld+json]").first()?.data()?.let {
val info = json.decodeFromString<JsonElement>(it).jsonObject
title = info["name"]!!.jsonPrimitive.content
description = info["description"]!!.jsonPrimitive.content
}
}
}

override fun episodeListParse(response: Response): List<SEpisode> {
val jsoup = response.asJsoup()
val nodes = jsoup.select("#playlist-scroll").first()!!.select(">div")
return nodes.mapIndexed { index, element ->
SEpisode.create().apply {
val href = element.select("a.overlay").attr("href")
setUrlWithoutDomain(href)
episode_number = (nodes.size - index).toFloat()
name = element.select("div.card-mobile-title").text()
if (href == response.request.url.toString()) {
// current video
jsoup.select("script[type=application/ld+json]").first()?.data()?.let {
val info = json.decodeFromString<JsonElement>(it).jsonObject
info["uploadDate"]?.jsonPrimitive?.content?.let { date ->
date_upload =
runCatching { uploadDateFormat.parse(date)?.time }.getOrNull() ?: 0L
}
}
}
}
}
}

override fun videoListParse(response: Response): List<Video> {
val sourceList = response.asJsoup().select("video source")
val preferQuality = preferences.getString(PREF_KEY_VIDEO_QUALITY, DEFAULT_QUALITY)
return sourceList.map {
val quality = it.attr("size")
val url = it.attr("src")
Video(url, "${quality}P", videoUrl = url)
}.sortedByDescending { preferQuality == it.quality }
}

override fun latestUpdatesParse(response: Response): AnimesPage = searchAnimeParse(response)

override fun latestUpdatesRequest(page: Int) = searchAnimeRequest(page, "", AnimeFilterList())

override fun popularAnimeParse(response: Response): AnimesPage = searchAnimeParse(response)

override fun popularAnimeRequest(page: Int) =
searchAnimeRequest(page, "", AnimeFilterList(HotFilter))

private fun String.appendInvisibleChar(): String {
// The search result title will be same as one episode name of anime.
// Adding extra char makes them has different title
return "${this}\u200B"
}

override fun searchAnimeParse(response: Response): AnimesPage {
val jsoup = response.asJsoup()
val nodes = jsoup.select("div.search-doujin-videos.hidden-xs")
val list = if (nodes.isNotEmpty()) {
nodes.map {
SAnime.create().apply {
setUrlWithoutDomain(it.select("a[class=overlay]").attr("href"))
thumbnail_url = it.select("img + img").attr("src")
title = it.select("div.card-mobile-title").text().appendInvisibleChar()
author = it.select(".card-mobile-user").text()
}
}
} else {
jsoup.select(".search-videos").map {
SAnime.create().apply {
setUrlWithoutDomain(it.parent()!!.attr("href"))
thumbnail_url = it.select("img").attr("src")
title = it.select(".home-rows-videos-title").text().appendInvisibleChar()
}
}
}
val nextPage = jsoup.select("li.page-item a.page-link[rel=next]")
return AnimesPage(list, nextPage.isNotEmpty())
}

override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request {
val searchUrl = baseUrl.toHttpUrl().newBuilder().addPathSegment("search")
if (query.isNotEmpty()) {
searchUrl.addQueryParameter("query", query)
}
filters.list.forEach {
when (it) {
is QueryFilter -> {
if (it.selected.isNotEmpty()) {
searchUrl.addQueryParameter(it.key, it.selected)
}
}

is TagFilter -> searchUrl.addQueryParameter(it.key, it.name)
else -> {}
}
}
if (page > 1) {
searchUrl.addQueryParameter("page", "$page")
}
return GET(searchUrl.build())
}

private fun checkFiltersInterceptor(chain: Interceptor.Chain): Response {
if (filterUpdateState == FilterUpdateState.NONE) {
updateFilters()
}
return chain.proceed(chain.request())
}

private fun updateFilters() {
filterUpdateState = FilterUpdateState.UPDATING
val exceptionHandler =
CoroutineExceptionHandler { _, _ -> filterUpdateState = FilterUpdateState.FAILED }
CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val jsoup = client.newCall(GET("$baseUrl/search")).awaitSuccess().asJsoup()
val genreList = jsoup.select("div.genre-option div.hentai-sort-options").eachText()
val sortList =
jsoup.select("div.hentai-sort-options-wrapper div.hentai-sort-options").eachText()
val yearList = jsoup.select("select#year option").eachAttr("value")
.map { it.ifEmpty { "全部年份" } }
val monthList = jsoup.select("select#month option").eachAttr("value")
.map { it.ifEmpty { "全部月份" } }
val categoryDict = mutableMapOf<String, MutableList<String>>()
var currentKey = ""
jsoup.select("div#tags div.modal-body").first()?.children()?.forEach {
if (it.tagName() == "h5") {
currentKey = it.text()
}
if (it.tagName() == "label") {
if (currentKey in categoryDict) {
categoryDict[currentKey]
} else {
categoryDict[currentKey] = mutableListOf()
categoryDict[currentKey]
}!!.add(it.select("input[name]").attr("value"))
}
}
preferences.edit().putString(PREF_KEY_GENRE_LIST, genreList.joinToString())
.putString(PREF_KEY_SORT_LIST, sortList.joinToString())
.putString(PREF_KEY_YEAR_LIST, yearList.joinToString())
.putString(PREF_KEY_MONTH_LIST, monthList.joinToString())
.putString(PREF_KEY_CATEGORY_LIST, json.encodeToString(categoryDict)).apply()
filterUpdateState = FilterUpdateState.COMPLETED
}
}

private fun <T : QueryFilter> createFilter(prefKey: String, block: (Array<String>) -> T): T {
val savedOptions = preferences.getString(prefKey, "")
if (savedOptions.isNullOrEmpty()) {
return block(emptyArray())
}
return block(savedOptions.split(", ").toTypedArray())
}

private fun createCategoryFilters(): List<AnimeFilter<out Any>> {
val result = mutableListOf<AnimeFilter<out Any>>(
BroadMatchFilter(),
)
val savedCategories = preferences.getString(PREF_KEY_CATEGORY_LIST, "")
if (savedCategories.isNullOrEmpty()) {
return result
}
json.decodeFromString<Map<String, List<String>>>(savedCategories).forEach {
result.add(CategoryFilter(it.key, it.value.map { value -> TagFilter("tags[]", value) }))
}
return result
}

override fun getFilterList(): AnimeFilterList {
return AnimeFilterList(
createFilter(PREF_KEY_GENRE_LIST) { GenreFilter(it) },
createFilter(PREF_KEY_SORT_LIST) { SortFilter(it) },
DateFilter(
createFilter(PREF_KEY_YEAR_LIST) { YearFilter(it) },
createFilter(PREF_KEY_MONTH_LIST) { MonthFilter(it) },
),
TagsFilter(createCategoryFilters()),
)
}

override fun setupPreferenceScreen(screen: PreferenceScreen) {
screen.addPreference(
ListPreference(screen.context).apply {
key = PREF_KEY_VIDEO_QUALITY
title = "設置首選畫質"
entries = arrayOf("1080P", "720P", "480P")
entryValues = entries
setDefaultValue(DEFAULT_QUALITY)
summary = "當前選擇:${preferences.getString(PREF_KEY_VIDEO_QUALITY, DEFAULT_QUALITY)}"
setOnPreferenceChangeListener { _, newValue ->
summary = "當前選擇:${newValue as String}"
true
}
},
)
}

companion object {
const val PREF_KEY_VIDEO_QUALITY = "PREF_KEY_VIDEO_QUALITY"

const val PREF_KEY_GENRE_LIST = "PREF_KEY_GENRE_LIST"
const val PREF_KEY_SORT_LIST = "PREF_KEY_SORT_LIST"
const val PREF_KEY_YEAR_LIST = "PREF_KEY_YEAR_LIST"
const val PREF_KEY_MONTH_LIST = "PREF_KEY_MONTH_LIST"
const val PREF_KEY_CATEGORY_LIST = "PREF_KEY_CATEGORY_LIST"

const val DEFAULT_QUALITY = "1080P"
}
}

0 comments on commit 5826738

Please sign in to comment.