Update hikari with minor fixes. Buzzheavier extractor less prone to failure. #986
4 changed files with 71 additions and 22 deletions
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue