Skip to content

Commit

Permalink
feat(simkl): add support for next episode airing
Browse files Browse the repository at this point in the history
  • Loading branch information
quickdesh committed Jul 26, 2023
1 parent 0c0bf51 commit ad0e308
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -920,8 +920,9 @@ class AnimeInfoScreenModel(
}

private suspend fun updateAiringTime(anime: Anime, trackItems: List<AnimeTrackItem>, manualFetch: Boolean) {
val airingEpisode = AniChartApi().loadAiringTime(anime, trackItems, manualFetch)
updateSuccessState { it.copy(nextAiringEpisode = airingEpisode) }
val airingEpisodeData = AniChartApi().loadAiringTime(anime, trackItems, manualFetch)
setAnimeViewerFlags.awaitSetNextEpisodeAiring(anime.id, airingEpisodeData)
updateSuccessState { it.copy(nextAiringEpisode = airingEpisodeData) }
}

// Track sheet - end
Expand Down
92 changes: 71 additions & 21 deletions app/src/main/java/eu/kanade/tachiyomi/util/AniChartApi.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package eu.kanade.tachiyomi.util
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.data.track.simkl.Simkl
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.ui.entries.anime.track.AnimeTrackItem
Expand All @@ -12,32 +13,34 @@ import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.entries.anime.model.Anime
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.time.OffsetDateTime
import java.util.Calendar

class AniChartApi {
private val client = OkHttpClient()
private val setAnimeViewerFlags: SetAnimeViewerFlags = Injekt.get()

internal suspend fun loadAiringTime(anime: Anime, trackItems: List<AnimeTrackItem>, manualFetch: Boolean): Pair<Int, Long> {
if (anime.status == SAnime.COMPLETED.toLong() && !manualFetch) return Pair(anime.nextEpisodeToAir, anime.nextEpisodeAiringAt)
var airingEpisodeData = Pair(anime.nextEpisodeToAir, anime.nextEpisodeAiringAt)
if (anime.status == SAnime.COMPLETED.toLong() && !manualFetch) return airingEpisodeData

return withIOContext {
var alId = 0L
var airingTime = Pair(0, 0L)
trackItems.forEach {
if (it.track != null) {
alId = when (it.service) {
is Anilist -> it.track.remoteId
is MyAnimeList -> getAlIdFromMal(it.track.remoteId)
else -> 0L
val matchingTrackItem = trackItems.firstOrNull {
(it.service is Anilist && it.track != null) ||
(it.service is MyAnimeList && it.track != null) ||
(it.service is Simkl && it.track != null)
} ?: return@withIOContext Pair(1, 0L)

matchingTrackItem.let { item ->
item.track!!.let {
airingEpisodeData = when (item.service) {
is Anilist -> getAnilistAiringEpisodeData(it.remoteId)
is MyAnimeList -> getAnilistAiringEpisodeData(getAlIdFromMal(it.remoteId))
is Simkl -> getSimklAiringEpisodeData(it.remoteId)
else -> Pair(1, 0L)
}
}
}
if (alId != 0L) {
airingTime = getAiringAt(alId)
setAnimeViewerFlags.awaitSetNextEpisodeAiring(anime.id, airingTime)
}
return@withIOContext airingTime
return@withIOContext airingEpisodeData
}
}

Expand Down Expand Up @@ -68,7 +71,7 @@ class AniChartApi {
}
}

private suspend fun getAiringAt(id: Long): Pair<Int, Long> {
private suspend fun getAnilistAiringEpisodeData(id: Long): Pair<Int, Long> {
return withIOContext {
val query = """
query {
Expand All @@ -89,13 +92,60 @@ class AniChartApi {
),
).execute()
} catch (e: Exception) {
return@withIOContext Pair(0, 0L)
return@withIOContext Pair(1, 0L)
}
val data = response.body.string()
val episodeNumber = data.substringAfter("episode\":").substringBefore(",").toIntOrNull() ?: 0
val episodeNumber = data.substringAfter("episode\":").substringBefore(",").toIntOrNull() ?: 1
val airingAt = data.substringAfter("airingAt\":").substringBefore("}").toLongOrNull() ?: 0L

return@withIOContext Pair(episodeNumber, airingAt)
}
}

private suspend fun getSimklAiringEpisodeData(id: Long): Pair<Int, Long> {
var episodeNumber = 1
var airingAt = 0L
return withIOContext {
val calendarTypes = listOf("anime", "tv", "movie_release")
calendarTypes.forEach {
val response = try {
client.newCall(GET("https://data.simkl.in/calendar/$it.json")).execute()
} catch (e: Exception) {
return@withIOContext Pair(1, 0L)
}

val body = response.body.string()

val data = removeAiredSimkl(body)

val malId = data.substringAfter("\"simkl_id\":$id,", "").substringAfter("\"mal\":\"").substringBefore("\"").toLongOrNull() ?: 0L
if (malId != 0L) return@withIOContext getAnilistAiringEpisodeData(getAlIdFromMal(malId))

val epNum = data.substringAfter("\"simkl_id\":$id,", "").substringBefore("\"}}").substringAfterLast("\"episode\":")
episodeNumber = epNum.substringBefore(",").toIntOrNull() ?: episodeNumber

val date = data.substringBefore("\"simkl_id\":$id,", "").substringAfterLast("\"date\":\"").substringBefore("\"")
airingAt = if (date.isNotBlank()) toUnixTimestamp(date) else airingAt

if (airingAt != 0L) return@withIOContext Pair(episodeNumber, airingAt)
}
return@withIOContext Pair(episodeNumber, airingAt)
}
}

private fun removeAiredSimkl(body: String): String {
val currentTimeInMillis = Calendar.getInstance().timeInMillis
val index = body.split("\"date\":\"").drop(1).indexOfFirst {
val date = it.substringBefore("\"")
val time = if (date.isNotBlank()) toUnixTimestamp(date) else 0L
time.times(1000) > currentTimeInMillis
}
return if (index >= 0) body.substring(index) else ""
}

private fun toUnixTimestamp(dateFormat: String): Long {
val offsetDateTime = OffsetDateTime.parse(dateFormat)
val instant = offsetDateTime.toInstant()
return instant.epochSecond
}
}

0 comments on commit ad0e308

Please sign in to comment.