Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Aniplay #174

Merged
merged 1 commit into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/en/aniplay/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ ext {
extName = 'AniPlay'
extClass = '.AniPlay'
themePkg = 'anilist'
overrideVersionCode = 8
overrideVersionCode = 9
}

apply from: "$rootDir/common.gradle"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.lib.playlistutils.PlaylistUtils
import eu.kanade.tachiyomi.multisrc.anilist.AniListAnimeHttpSource
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.util.parallelFlatMap
import eu.kanade.tachiyomi.util.parallelMap
import eu.kanade.tachiyomi.util.parseAs
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
Expand Down Expand Up @@ -183,99 +182,152 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
}
?: emptyList()

val headersWithAction =
headers.newBuilder()
// next.js stuff I guess
.add("Next-Action", getHeaderValue(baseHost, NEXT_ACTION_SOURCES_LIST))
.build()

var timeouts = 0
var maxTimeout = 0
val episodeDataList = extras.parallelFlatMap { extra ->
val videos = extras.parallelFlatMap { extra ->
val languages = mutableListOf("sub").apply {
if (extra.hasDub) add("dub")
}
languages.parallelMap { language ->
maxTimeout += 1
languages.parallelFlatMap { language ->
val epNum = if (extra.episodeNum == extra.episodeNum.toInt().toFloat()) {
extra.episodeNum.toInt().toString() // If it has no fractional part, convert it to an integer
extra.episodeNum.toInt().toString()
} else {
extra.episodeNum.toString() // If it has a fractional part, leave it as a float
extra.episodeNum.toString()
}

val requestBody = "[\"$animeId\",\"${extra.source}\",\"${extra.episodeId}\",\"$epNum\",\"$language\"]"
.toRequestBody("application/json".toMediaType())

val params = mapOf(
"host" to extra.source,
"ep" to epNum,
"type" to language,
)

val builder = Uri.parse("$baseUrl/anime/watch/$animeId").buildUpon()
params.map { (k, v) -> builder.appendQueryParameter(k, v); }
val url = builder.build().toString()
try {
val request = POST(url, headersWithAction, requestBody)
val response = client.newCall(request).execute()
val url = builder.build()

val responseString = response.body.string()
val sourcesString = extractSourcesList(responseString) ?: return@parallelMap null
val data = sourcesString.parseAs<VideoSourceResponse>()
val headersWithAction =
headers.newBuilder()
.add("Next-Action", getHeaderValue(baseHost, NEXT_ACTION_SOURCES_LIST))
.build()

EpisodeData(
source = extra.source,
language = language,
response = data,
)
val requestBody = "[\"$animeId\",\"${extra.source}\",\"${extra.episodeId}\",\"$epNum\",\"$language\"]"
.toRequestBody("application/json".toMediaType())

val request = POST(url.toString(), headersWithAction, requestBody)

maxTimeout += 1
try {
getVideos(extra, language, request)
} catch (e: java.net.SocketTimeoutException) {
timeouts += 1
null
Log.e("AniPlay", "VideoList $url SocketTimeoutException", e)
timeouts++
emptyList()
} catch (e: IOException) {
Log.w("AniPlay", "VideoList $url IOException", e)
timeouts = -999
null // Return null to be filtered out
Log.e("AniPlay", "VideoList $url IOException", e)
emptyList()
} catch (e: Exception) {
Log.w("AniPlay", "VideoList $url Exception", e)
timeouts = -999
null // Return null to be filtered out
Log.e("AniPlay", "VideoList $url Exception", e)
emptyList()
}
}.filterNotNull() // Filter out null values due to errors
}
}

if (maxTimeout == timeouts && timeouts != 0) {
if (videos.isEmpty() && timeouts != 0 && maxTimeout == timeouts) {
throw Exception("Timed out")
}

val videos = episodeDataList.flatMap { episodeData ->
val defaultSource = episodeData.response.sources?.firstOrNull {
it.quality in listOf("default", "auto")
} ?: return@flatMap emptyList()

val subtitles = episodeData.response.subtitles
?.filter { it.lang != "Thumbnails" }
?.map { Track(it.url, it.lang) }
?: emptyList()

try {
playlistUtils.extractFromHls(
playlistUrl = defaultSource.url,
videoNameGen = { quality ->
val serverName = getServerName(episodeData.source)
val typeName = when {
subtitles.isNotEmpty() -> "SoftSub"
else -> getTypeName(episodeData.language)
}
"$serverName - $quality - $typeName"
},
subtitleList = subtitles,
return videos.sort()
}

private fun getVideos(extra: EpisodeExtra, language: String, request: Request): List<Video> {
val response = client.newCall(request).execute()

val responseString = response.body.string()
val sourcesString = extractSourcesList(responseString) ?: return emptyList()
Log.i("AniPlay", "${extra.source} $language -> $sourcesString")

when (extra.source.lowercase()) {
"yuki" -> {
val data = sourcesString.parseAs<VideoSourceResponseYuki>()
return processEpisodeDataYuki(
EpisodeDataYuki(
source = extra.source,
language = language,
response = data,
),
)
}
else -> {
val data = sourcesString.parseAs<VideoSourceResponse>()
return processEpisodeData(
EpisodeData(
source = extra.source,
language = language,
response = data,
),
)
} catch (e: Exception) {
Log.e("AniPlay", "extractFromHls Error: $e")
emptyList()
}
}
}

return videos.sort()
private fun processEpisodeDataYuki(episodeData: EpisodeDataYuki): List<Video> {
val defaultSource = episodeData.response.sources?.firstOrNull()

if (defaultSource == null) {
Log.e("AniPlay", "defaultSource is null (${episodeData.response})")
return emptyList()
}

val subtitles = episodeData.response.tracks
?.filter { it.kind?.lowercase() == "captions" }
?.map { Track(it.file, it.label ?: "Unknown") }
?: emptyList()

val serverName = getServerName(episodeData.source)
val typeName = getTypeName(episodeData.language).let {
if (it == "Sub" && subtitles.isNotEmpty()) "SoftSub" else it
}

try {
return playlistUtils.extractFromHls(
playlistUrl = defaultSource.url,
videoNameGen = { quality -> "$serverName - $quality - $typeName" },
subtitleList = subtitles,
)
} catch (e: Exception) {
Log.e("AniPlay", "processEpisodeDataYuki extractFromHls Error (\"$serverName - $typeName\"): $e")
}

return emptyList()
}

private fun processEpisodeData(episodeData: EpisodeData): List<Video> {
val defaultSource = episodeData.response.sources?.firstOrNull {
it.quality in listOf("default", "auto")
} ?: return emptyList()

val subtitles = episodeData.response.subtitles
?.filter { it.lang?.lowercase() != "thumbnails" }
?.map { Track(it.url, it.lang ?: "Unk") }
?: emptyList()

val serverName = getServerName(episodeData.source)
val typeName = when {
subtitles.isNotEmpty() -> "SoftSub"
else -> getTypeName(episodeData.language)
}

try {
return playlistUtils.extractFromHls(
playlistUrl = defaultSource.url,
videoNameGen = { quality -> "$serverName - $quality - $typeName" },
subtitleList = subtitles,
)
} catch (e: Exception) {
Log.e("AniPlay", "processEpisodeData extractFromHls Error (\"$serverName - $typeName\"): $e")
}

return emptyList()
}

override fun List<Video>.sort(): List<Video> {
Expand Down Expand Up @@ -431,7 +483,7 @@ class AniPlay : AniListAnimeHttpSource(), ConfigurableAnimeSource {
}

private fun getTypeName(value: String): String {
val index = PREF_TYPE_ENTRY_VALUES.indexOf(value)
val index = PREF_TYPE_ENTRY_VALUES.indexOf(value.lowercase())
if (index == -1) {
return "Other"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.animeextension.en.aniplay

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
Expand All @@ -22,20 +21,6 @@ data class EpisodeListResponse(
)
}

@Serializable
data class VideoSourceRequest(
val source: String,

@SerialName("episodeid")
val episodeId: String,

@SerialName("episodenum")
val episodeNum: String,

@SerialName("subtype")
val subType: String,
)

@Serializable
data class VideoSourceResponse(
val sources: List<Source>?,
Expand All @@ -44,13 +29,35 @@ data class VideoSourceResponse(
@Serializable
data class Source(
val url: String,
val quality: String,
val quality: String?,
)

@Serializable
data class Subtitle(
val url: String,
val lang: String,
val lang: String?,
)
}

@Serializable
data class VideoSourceResponseYuki(
val sources: List<Source>?,
val tracks: List<Subtitle>?,
val anilistID: Int?,
val malID: Int?,
) {
@Serializable
data class Source(
val url: String,
val type: String?,
)

@Serializable
data class Subtitle(
val file: String,
val label: String?,
val kind: String?,
val default: Boolean?,
)
}

Expand All @@ -68,3 +75,10 @@ data class EpisodeData(
val language: String,
val response: VideoSourceResponse,
)

@Serializable
data class EpisodeDataYuki(
val source: String,
val language: String,
val response: VideoSourceResponseYuki,
)
Loading