mirror of
https://github.com/yuzono/anime-extensions.git
synced 2026-06-13 13:39:44 +00:00
Animelib [RU]: Fix covers and video players (#375)
* New fix 403 error in Animelib extension
* Increment version code from 13 to 14
* Update src/ru/animelib/src/eu/kanade/tachiyomi/animeextension/ru/animelib/Animelib.kt
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
* Update src/ru/animelib/src/eu/kanade/tachiyomi/animeextension/ru/animelib/Animelib.kt
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
* Update src/ru/animelib/src/eu/kanade/tachiyomi/animeextension/ru/animelib/Animelib.kt
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
* Update build.gradle
* Make anime names nullable in AnimeData class
* Update Animelib.kt
* Update build.gradle
* Update Animelib.kt
Replaced manual URL construction (`"$serverUrl${it.href}"`) with `UrlUtils.fixUrl(href, serverUrl)` to correctly resolve relative and absolute URLs.
Simplified URL normalization logic by removing manual `startsWith("http")` checks in favor of `UrlUtils.fixUrl(trimmed, "https://$defaultCoverDomain")`.
Suggested by @cuong-tran
* Update Animelib.kt
I removed kodikHeaders and videoHeaders, and it works fine without them
* Update build.gradle
bump version
* Refactor
---------
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Animelib'
|
||||
extClass = '.Animelib'
|
||||
extVersionCode = 14
|
||||
extVersionCode = 15
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.animeextension.ru.animelib
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import android.widget.Toast
|
||||
import androidx.preference.EditTextPreference
|
||||
@@ -18,7 +19,6 @@ import eu.kanade.tachiyomi.animesource.model.Track
|
||||
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.POST
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import keiyoushi.utils.UrlUtils
|
||||
import keiyoushi.utils.bodyString
|
||||
@@ -27,12 +27,16 @@ import keiyoushi.utils.parallelCatchingFlatMap
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.tryParse
|
||||
import keiyoushi.utils.useAsJsoup
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.net.URLDecoder
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
@@ -54,7 +58,10 @@ class Animelib :
|
||||
|
||||
private val apiSite = "https://hapi.hentaicdn.org"
|
||||
private val apiUrl = "$apiSite/api"
|
||||
private val coverDomain = "cover.imglib.info"
|
||||
|
||||
// Some deployments use different cover hosts — include both common ones
|
||||
private val coverDomains = listOf("cover.hentaicdn.org", "cover.imglib.info")
|
||||
private val defaultCoverDomain = coverDomains[0]
|
||||
|
||||
private val playlistUtils by lazy { PlaylistUtils(client, headers) }
|
||||
private val dateFormatter by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
|
||||
@@ -87,15 +94,30 @@ class Animelib :
|
||||
override val client = network.client.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
val requestToProceed = if (request.url.host == coverDomain) {
|
||||
request.newBuilder()
|
||||
.header("Referer", "https://$domain/")
|
||||
.build()
|
||||
} else {
|
||||
request
|
||||
val host = request.url.host
|
||||
val apiHost = apiSite.toHttpUrl().host
|
||||
val builder = request.newBuilder()
|
||||
// Add Referer/Origin for cover and API hosts
|
||||
if (host in coverDomains || host == apiHost) {
|
||||
builder.header("Referer", "https://$domain/")
|
||||
builder.header("Origin", "https://$domain")
|
||||
}
|
||||
// Additional headers for API requests to bypass simple bot protections
|
||||
if (host == apiHost) {
|
||||
builder.header("Accept", "application/json, text/plain, */*")
|
||||
builder.header("X-Requested-With", "XMLHttpRequest")
|
||||
builder.header("User-Agent", "Mozilla/5.0 (Android)")
|
||||
}
|
||||
|
||||
chain.proceed(requestToProceed)
|
||||
val requestToProceed = builder.build()
|
||||
val response = chain.proceed(requestToProceed)
|
||||
|
||||
// Log 403 responses for easier debugging
|
||||
if (response.code == 403) {
|
||||
android.util.Log.w("Animelib", "HTTP 403 for ${request.url} (host=$host)")
|
||||
}
|
||||
|
||||
response
|
||||
}.build()
|
||||
|
||||
// =============================== Preference ===============================
|
||||
@@ -224,13 +246,7 @@ class Animelib :
|
||||
}
|
||||
|
||||
// =============================== Video List ===============================
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> = client.newCall(videoListRequest(episode))
|
||||
.awaitSuccess()
|
||||
.use { response ->
|
||||
videoListParseAsync(response)
|
||||
}
|
||||
|
||||
private suspend fun videoListParseAsync(response: Response): List<Video> {
|
||||
override fun videoListParse(response: Response): List<Video> = runBlocking {
|
||||
val episodeData = response.parseAs<EpisodeVideoData>()
|
||||
val videoServer = fetchPreferredVideoServer()
|
||||
val teams = preferences.getString(PREF_DUB_TEAM_KEY, "")?.split(',')
|
||||
@@ -253,7 +269,8 @@ class Animelib :
|
||||
} ?: preferredTeams
|
||||
|
||||
val ignoreSubs = preferences.getBoolean(PREF_IGNORE_SUBS_KEY, PREF_IGNORE_SUBS_DEFAULT)
|
||||
return videoInfoList?.parallelCatchingFlatMap { videoInfo ->
|
||||
|
||||
return@runBlocking videoInfoList?.parallelCatchingFlatMap { videoInfo ->
|
||||
if (ignoreSubs && videoInfo.translationInfo.id == 1) {
|
||||
return@parallelCatchingFlatMap emptyList()
|
||||
}
|
||||
@@ -352,17 +369,21 @@ class Animelib :
|
||||
.parseAs<VideoServerData>()
|
||||
|
||||
val serverPreference = preferences.getString(PREF_SERVER_KEY, PREF_SERVER_ENTRIES[0])
|
||||
if (serverPreference.isNullOrEmpty()) {
|
||||
return videoServers.data.videoServers[0].url
|
||||
}
|
||||
|
||||
for (videoServer in videoServers.data.videoServers) {
|
||||
if (videoServer.label == serverPreference) {
|
||||
return videoServer.url
|
||||
val chosen = if (serverPreference.isNullOrEmpty()) {
|
||||
videoServers.data.videoServers[0].url
|
||||
} else {
|
||||
var found: String? = null
|
||||
for (videoServer in videoServers.data.videoServers) {
|
||||
if (videoServer.label == serverPreference) {
|
||||
found = videoServer.url
|
||||
break
|
||||
}
|
||||
}
|
||||
found ?: videoServers.data.videoServers[0].url
|
||||
}
|
||||
|
||||
return videoServers.data.videoServers[0].url
|
||||
android.util.Log.d("Animelib", "fetchPreferredVideoServer: chosen=$chosen preference=$serverPreference servers=${videoServers.data.videoServers}")
|
||||
return chosen
|
||||
}
|
||||
|
||||
private suspend fun kodikVideoLinks(playerUrl: String?, teamName: String): List<Video> {
|
||||
@@ -391,18 +412,29 @@ class Animelib :
|
||||
val kodikDomain = formData.pd
|
||||
val formBody = FormBody.Builder()
|
||||
formBody.add("d", formData.d)
|
||||
formBody.add("d_sign", URLDecoder.decode(formData.dSign, "utf-8"))
|
||||
formBody.add("d_sign", Uri.decode(formData.dSign))
|
||||
formBody.add("pd", formData.pd)
|
||||
formBody.add("pd_sign", URLDecoder.decode(formData.pdSign, "utf-8"))
|
||||
formBody.add("ref", URLDecoder.decode(formData.ref, "utf-8"))
|
||||
formBody.add("ref_sign", URLDecoder.decode(formData.refSign, "utf-8"))
|
||||
formBody.add("pd_sign", Uri.decode(formData.pdSign))
|
||||
formBody.add("ref", Uri.decode(formData.ref))
|
||||
formBody.add("ref_sign", Uri.decode(formData.refSign))
|
||||
|
||||
val urlParts = playerUrl.split('/')
|
||||
formBody.add("type", urlParts[3])
|
||||
formBody.add("id", urlParts[4])
|
||||
formBody.add("hash", urlParts[5])
|
||||
|
||||
val videoInfoRequest = POST("https://$kodikDomain/ftor", body = formBody.build())
|
||||
val videoInfoRequest = Request.Builder()
|
||||
.url("https://$kodikDomain/ftor")
|
||||
.post(formBody.build())
|
||||
.headers(
|
||||
Headers.Builder().apply {
|
||||
set("Referer", "$baseUrl/")
|
||||
set("Origin", "https://$domain")
|
||||
set("User-Agent", "Mozilla/5.0 (Android)")
|
||||
}.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
val kodikData = client.newCall(videoInfoRequest).awaitSuccess().parseAs<KodikData>()
|
||||
|
||||
// Load js with encode algorithm and parse it
|
||||
@@ -459,8 +491,10 @@ class Animelib :
|
||||
|
||||
val hlsUrl = Base64.decode(base64Url, Base64.DEFAULT).toString(Charsets.UTF_8)
|
||||
val playlistUrl = UrlUtils.fixUrl(hlsUrl) ?: return@flatMap emptyList()
|
||||
|
||||
playlistUtils.extractFromHls(
|
||||
playlistUrl,
|
||||
|
||||
videoNameGen = { "$teamName (${quality}p Kodik)" },
|
||||
)
|
||||
}
|
||||
@@ -495,8 +529,12 @@ class Animelib :
|
||||
}
|
||||
}
|
||||
|
||||
val url = "$serverUrl${it.href}"
|
||||
// Build absolute URL for the video; `href` may be absolute or relative
|
||||
val href = it.href.trim()
|
||||
val url = UrlUtils.fixUrl(href, serverUrl) ?: return@mapNotNull null
|
||||
|
||||
val quality = "${videoInfo.team.name} (${it.quality}p Animelib)"
|
||||
|
||||
Video(url, quality, url, subtitleTracks = subtitles)
|
||||
}
|
||||
|
||||
@@ -524,20 +562,72 @@ class Animelib :
|
||||
}
|
||||
}
|
||||
|
||||
// Extract plain text from summary which can be either a string or a structured JSON doc
|
||||
private fun extractTextFromSummary(element: JsonElement?): String? {
|
||||
if (element == null) return null
|
||||
|
||||
val sb = StringBuilder()
|
||||
|
||||
fun recurse(el: JsonElement) {
|
||||
when (el) {
|
||||
is JsonPrimitive -> {
|
||||
// Append primitive content (string, number, etc.)
|
||||
sb.append(el.content)
|
||||
}
|
||||
is JsonObject -> {
|
||||
// If node has a 'text' field, append it
|
||||
el["text"]?.let { textEl ->
|
||||
if (textEl is JsonPrimitive) sb.append(textEl.content)
|
||||
}
|
||||
// Recurse into 'content' arrays or other fields that may contain text
|
||||
el["content"]?.let { recurse(it) }
|
||||
}
|
||||
is JsonArray -> {
|
||||
for (child in el) {
|
||||
recurse(child)
|
||||
// Separate block nodes with a space
|
||||
sb.append(" ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recurse(element)
|
||||
|
||||
val result = sb.toString().replace(spaceRegex, " ").trim()
|
||||
return result.ifEmpty { null }
|
||||
}
|
||||
|
||||
private val spaceRegex by lazy { Regex("\\s+") }
|
||||
|
||||
// Normalize cover URL which can be absolute, protocol-relative, or relative path
|
||||
private fun normalizeCoverUrl(url: String?): String? {
|
||||
if (url.isNullOrBlank()) return null
|
||||
val trimmed = url.trim()
|
||||
|
||||
return UrlUtils.fixUrl(trimmed, "https://$defaultCoverDomain")
|
||||
}
|
||||
|
||||
private fun AnimeData.toSAnime() = SAnime.create().apply {
|
||||
url = href
|
||||
title = rusName
|
||||
thumbnail_url = cover.default
|
||||
description = summary
|
||||
setUrlWithoutDomain(href)
|
||||
title = rusName ?: engName ?: run {
|
||||
when (otherNames) {
|
||||
is JsonArray -> otherNames.firstOrNull()?.let { if (it is JsonPrimitive) it.content else null }
|
||||
is JsonPrimitive -> otherNames.content
|
||||
else -> null
|
||||
}
|
||||
} ?: href
|
||||
thumbnail_url = normalizeCoverUrl(cover.default)
|
||||
description = extractTextFromSummary(summary)
|
||||
status = convertStatus(animeStatus.id)
|
||||
author = publisher?.joinToString { it.name }
|
||||
artist = authors?.joinToString { it.name }
|
||||
}
|
||||
|
||||
private fun EpisodeInfo.toSEpisode() = SEpisode.create().apply {
|
||||
url = "api/episodes/$id"
|
||||
setUrlWithoutDomain("api/episodes/$id")
|
||||
name = "Сезон $season Серия $number $episodeName"
|
||||
episode_number = number.toFloat()
|
||||
episode_number = number.toFloatOrNull() ?: 0f
|
||||
date_upload = dateFormatter.tryParse(date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.animeextension.ru.animelib
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
@Serializable
|
||||
data class AnimeStatus(
|
||||
@@ -34,17 +35,19 @@ data class AuthorInfo(
|
||||
@Serializable
|
||||
data class AnimeData(
|
||||
val id: Int,
|
||||
@SerialName("rus_name") val rusName: String,
|
||||
@SerialName("rus_name") val rusName: String? = null,
|
||||
@SerialName("eng_name") val engName: String? = null,
|
||||
@SerialName("slug_url") val href: String,
|
||||
@SerialName("status") val animeStatus: AnimeStatus,
|
||||
val cover: CoverInfo,
|
||||
|
||||
// Optional
|
||||
@SerialName("is_licensed") val licensed: Boolean? = null,
|
||||
val summary: String? = null,
|
||||
val summary: JsonElement? = null,
|
||||
val genres: List<GenreInfo>? = null,
|
||||
val publisher: List<PublisherInfo>? = null,
|
||||
val authors: List<AuthorInfo>? = null,
|
||||
@SerialName("otherNames") val otherNames: JsonElement? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
||||
Reference in New Issue
Block a user