Update hikari with minor fixes. Buzzheavier extractor less prone to failure. #986

Merged
Sadwhy merged 4 commits from main into main 2025-05-06 09:58:30 -05:00
4 changed files with 71 additions and 22 deletions

View file

@ -1,22 +1,27 @@
package eu.kanade.tachiyomi.lib.buzzheavierextractor package eu.kanade.tachiyomi.lib.buzzheavierextractor
import eu.kanade.tachiyomi.animesource.model.Video import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.parseAs import eu.kanade.tachiyomi.util.parseAs
import java.io.IOException
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.internal.EMPTY_HEADERS import okhttp3.Request
import okhttp3.Response
class BuzzheavierExtractor( class BuzzheavierExtractor(
private val client: OkHttpClient, private val client: OkHttpClient,
private val headers: Headers, private val headers: Headers,
) { ) {
companion object {
private val SIZE_REGEX = Regex("""Size\s*-\s*([0-9.]+\s*[GMK]B)""")
}
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
fun videosFromUrl(url: String, prefix: String = "Buzzheavier - ", proxyUrl: String? = null): List<Video> { fun videosFromUrl(url: String, prefix: String = "Buzzheavier - ", proxyUrl: String? = null): List<Video> {
val httpUrl = url.toHttpUrl() val httpUrl = url.toHttpUrl()
@ -24,29 +29,52 @@ class BuzzheavierExtractor(
val dlHeaders = headers.newBuilder().apply { val dlHeaders = headers.newBuilder().apply {
add("Accept", "*/*") add("Accept", "*/*")
add("Host", httpUrl.host)
add("HX-Current-URL", url) add("HX-Current-URL", url)
add("HX-Request", "true") add("HX-Request", "true")
add("Priority", "u=1, i")
add("Referer", url) add("Referer", url)
}.build() }.build()
val videoHeaders = headers.newBuilder().apply { val videoHeaders = headers.newBuilder().apply {
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
add("Priority", "u=0, i")
add("Referer", url) add("Referer", url)
}.build() }.build()
val path = client.newCall( val siteRequest = client.newCall(GET(url)).execute()
GET("https://${httpUrl.host}/$id/download", dlHeaders) val parsedHtml = siteRequest.asJsoup()
).execute().headers["hx-redirect"].orEmpty() val detailsText = parsedHtml.selectFirst("li:contains(Details:)")?.text() ?: ""
val size = SIZE_REGEX.find(detailsText)?.groupValues?.getOrNull(1)?.trim() ?: "Unknown"
return if (path.isNotEmpty()) { val downloadRequest = GET("https://${httpUrl.host}/$id/download", dlHeaders)
val videoUrl = if (path.startsWith("http")) path else "https://${httpUrl.host}$path" val path = client.executeWithRetry(downloadRequest, 5, 204).use { response ->
listOf(Video(videoUrl, "${prefix}Video", videoUrl, videoHeaders)) response.header("hx-redirect").orEmpty()
} else if (proxyUrl?.isNotEmpty() == true) {
val videoUrl = client.newCall(GET(proxyUrl + id)).execute().parseAs<UrlDto>().url
listOf(Video(videoUrl, "${prefix}Video", videoUrl, videoHeaders))
} else {
emptyList()
} }
val videoUrl = if (path.isNotEmpty()) {
if (path.startsWith("http")) path else "https://${httpUrl.host}$path"
} else if (proxyUrl?.isNotEmpty() == true) {
client.executeWithRetry(GET(proxyUrl + id), 5, 200).parseAs<UrlDto>().url
} else {
return emptyList()
}
return listOf(Video(videoUrl, "${prefix}${size}", videoUrl, videoHeaders))
}
private fun OkHttpClient.executeWithRetry(request: Request, maxRetries: Int, validCode: Int): Response {
var response: Response? = null
for (attempt in 0 until maxRetries) {
response?.close()
response = this.newCall(request).execute()
if (response.code == validCode) {
return response
}
if (attempt < maxRetries - 1) {
Thread.sleep(1000)
}
}
return response ?: throw IOException("Failed to execute request after $maxRetries attempts")
} }
@Serializable @Serializable

View file

@ -1,7 +1,7 @@
ext { ext {
extName = 'Hikari' extName = 'Hikari'
extClass = '.Hikari' extClass = '.Hikari'
extVersionCode = 21 extVersionCode = 22
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View file

@ -29,24 +29,50 @@ data class AnimeDto(
val aniGenre: String? = null, val aniGenre: String? = null,
@SerialName("ani_studio") @SerialName("ani_studio")
val aniStudio: String? = null, val aniStudio: String? = null,
@SerialName("ani_producers")
val aniProducers: String? = null,
@SerialName("ani_stats") @SerialName("ani_stats")
val aniStats: Int? = null, val aniStats: Int? = null,
@SerialName("ani_time")
val aniTime: String? = null,
@SerialName("ani_ep")
val aniEp: String? = null,
@SerialName("ani_type")
val aniType: Int? = null,
@SerialName("ani_score")
val aniScore: Double? = null,
) { ) {
fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply { fun toSAnime(preferEnglish: Boolean): SAnime = SAnime.create().apply {
url = uid url = uid
title = if (preferEnglish) aniEName?.takeUnless(String::isBlank) ?: aniName else aniName title = if (preferEnglish) aniEName?.takeUnless(String::isBlank) ?: aniName else aniName
thumbnail_url = aniPoster thumbnail_url = aniPoster
genre = aniGenre?.split(",")?.joinToString(transform = String::trim) genre = aniGenre?.split(",")?.joinToString(transform = String::trim)
author = aniStudio artist = aniStudio
author = aniProducers?.split(",")?.joinToString(transform = String::trim)
description = buildString { description = buildString {
aniScore?.let { append("Score: %.2f/10\n\n".format(it)) }
aniSynopsis?.trim()?.let(::append) aniSynopsis?.trim()?.let(::append)
append("\n\n") append("\n\n")
aniType?.let {
val type = when (it) {
1 -> "TV"
2 -> "Movie"
3 -> "OVA"
4 -> "ONA"
5 -> "Special"
else -> "Unknown"
}
append("Type: $type\n")
}
aniEp?.let { append("Total Episode count: $it\n") }
aniTime?.let { append("Runtime: $it\n") }
aniSynonyms?.let { append("Synonyms: $it") } aniSynonyms?.let { append("Synonyms: $it") }
}.trim() }.trim()
status = when (aniStats) { status = when (aniStats) {
1 -> SAnime.ONGOING 1 -> SAnime.UNKNOWN
2 -> SAnime.COMPLETED 2 -> SAnime.COMPLETED
3 -> SAnime.ONGOING
else -> SAnime.UNKNOWN else -> SAnime.UNKNOWN
} }
} }

View file

@ -294,11 +294,6 @@ class Hikari : AnimeHttpSource(), ConfigurableAnimeSource {
entries = PREF_PROVIDERS entries = PREF_PROVIDERS
entryValues = PREF_PROVIDERS_VALUE entryValues = PREF_PROVIDERS_VALUE
setDefaultValue(PREF_PROVIDERS_DEFAULT) setDefaultValue(PREF_PROVIDERS_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
@Suppress("UNCHECKED_CAST")
preferences.edit().putStringSet(key, newValue as Set<String>).commit()
}
}.also(screen::addPreference) }.also(screen::addPreference)
ListPreference(screen.context).apply { ListPreference(screen.context).apply {