Skip to content

Commit

Permalink
Add job to fetch old episodes
Browse files Browse the repository at this point in the history
  • Loading branch information
Ziedelth committed Apr 24, 2024
1 parent e9b652c commit 4f890cc
Show file tree
Hide file tree
Showing 19 changed files with 451 additions and 226 deletions.
1 change: 1 addition & 0 deletions src/main/kotlin/fr/shikkanime/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ fun initAll(adminPassword: AtomicReference<String>?, port: Int = 37100, wait: Bo
JobManager.scheduleJob("0 0 * * * ?", SavingImageCacheJob::class.java)
// Every day at midnight
JobManager.scheduleJob("0 0 0 * * ?", DeleteOldMetricsJob::class.java)
JobManager.scheduleJob("0 0 15 * * ?", FetchOldEpisodesJob::class.java)
// Every day at 9am
JobManager.scheduleJob("0 0 9 * * ?", FetchCalendarJob::class.java)
JobManager.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package fr.shikkanime.caches

import fr.shikkanime.entities.enums.CountryCode

data class CountryCodeAnimeIdKeyCache(
data class CountryCodeIdKeyCache(
val countryCode: CountryCode,
val animeId: String,
val id: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@ package fr.shikkanime.controllers.admin
import com.google.inject.Inject
import fr.shikkanime.converters.AbstractConverter
import fr.shikkanime.dtos.TokenDto
import fr.shikkanime.entities.Anime
import fr.shikkanime.entities.EpisodeMapping
import fr.shikkanime.entities.Simulcast
import fr.shikkanime.entities.enums.CountryCode
import fr.shikkanime.entities.enums.EpisodeType
import fr.shikkanime.entities.enums.Link
import fr.shikkanime.services.*
import fr.shikkanime.services.caches.SimulcastCacheService
import fr.shikkanime.utils.MapCache
import fr.shikkanime.utils.routes.AdminSessionAuthenticated
import fr.shikkanime.utils.routes.Controller
import fr.shikkanime.utils.routes.Path
Expand Down Expand Up @@ -114,24 +108,7 @@ class AdminController {
@Get
@AdminSessionAuthenticated
private fun invalidateSimulcasts(): Response {
animeService.findAll().forEach { anime ->
anime.simulcasts.clear()
animeService.update(anime)
}

episodeMappingService.findAll()
.filter { it.variants.any { variant -> variant.audioLocale != CountryCode.FR.locale } && it.episodeType != EpisodeType.FILM }
.sortedBy { it.releaseDateTime }
.forEach { episodeMapping ->
val anime = animeService.find(episodeMapping.anime!!.uuid!!)!!
animeService.addSimulcastToAnime(anime, episodeVariantService.getSimulcast(anime, episodeMapping))

if (episodeMapping.anime != anime) {
animeService.update(anime)
}
}

MapCache.invalidate(Anime::class.java, EpisodeMapping::class.java, Simulcast::class.java)
animeService.recalculateSimulcasts()
return Response.redirect(Link.DASHBOARD.href)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class SiteController {
@Path("animes/{slug}")
@Get
private fun animeDetail(@PathParam("slug") slug: String): Response {
val anime = animeCacheService.findBySlug(slug) ?: return Response.redirect("/404")
val anime = animeCacheService.findBySlug(CountryCode.FR, slug) ?: return Response.redirect("/404")
val dto = AbstractConverter.convert(anime, AnimeDto::class.java)

return Response.template(
Expand All @@ -136,6 +136,7 @@ class SiteController {
CountryCode.FR,
anime.uuid,
listOf(
SortParameter("releaseDateTime", SortParameter.Order.ASC),
SortParameter("season", SortParameter.Order.ASC),
SortParameter("episodeType", SortParameter.Order.ASC),
SortParameter("number", SortParameter.Order.ASC),
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/fr/shikkanime/entities/Anime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import java.util.*
name = "anime",
indexes = [
Index(name = "idx_anime_country_code", columnList = "country_code"),
Index(name = "idx_anime_slug", columnList = "slug"),
Index(name = "idx_anime_country_code_slug", columnList = "country_code, slug", unique = true)
]
)
@Indexed
Expand Down Expand Up @@ -45,7 +45,7 @@ class Anime(
inverseJoinColumns = [JoinColumn(name = "simulcast_uuid")]
)
var simulcasts: MutableSet<Simulcast> = mutableSetOf(),
@Column(nullable = false, unique = true)
@Column(nullable = false)
var slug: String? = null,
@Column(nullable = false, name = "last_release_date_time")
var lastReleaseDateTime: ZonedDateTime = releaseDateTime,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ enum class ConfigPropertyKey(val key: String) {
DISNEY_PLUS_AUTHORIZATION("disney_plus_authorization"),
DISNEY_PLUS_REFRESH_TOKEN("disney_plus_refresh_token"),
TRANSLATE_CALENDAR("translate_calendar"),
LAST_FETCH_OLD_EPISODES("last_fetch_old_episodes"),
FETCH_OLD_EPISODES_RANGE("fetch_old_episodes_range"),
}
238 changes: 238 additions & 0 deletions src/main/kotlin/fr/shikkanime/jobs/FetchOldEpisodesJob.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package fr.shikkanime.jobs

import com.google.inject.Inject
import fr.shikkanime.caches.CountryCodeIdKeyCache
import fr.shikkanime.entities.Anime
import fr.shikkanime.entities.EpisodeMapping
import fr.shikkanime.entities.EpisodeVariant
import fr.shikkanime.entities.Simulcast
import fr.shikkanime.entities.enums.ConfigPropertyKey
import fr.shikkanime.entities.enums.CountryCode
import fr.shikkanime.platforms.AbstractPlatform
import fr.shikkanime.platforms.AnimationDigitalNetworkPlatform
import fr.shikkanime.platforms.CrunchyrollPlatform
import fr.shikkanime.services.AnimeService
import fr.shikkanime.services.ConfigService
import fr.shikkanime.services.EpisodeMappingService
import fr.shikkanime.services.EpisodeVariantService
import fr.shikkanime.services.caches.ConfigCacheService
import fr.shikkanime.utils.Constant
import fr.shikkanime.utils.LoggerFactory
import fr.shikkanime.utils.MapCache
import fr.shikkanime.utils.ObjectParser.getAsInt
import fr.shikkanime.utils.ObjectParser.getAsString
import fr.shikkanime.utils.StringUtils
import fr.shikkanime.wrappers.CrunchyrollWrapper
import kotlinx.coroutines.runBlocking
import java.time.LocalDate
import java.time.Period
import java.util.logging.Level

class FetchOldEpisodesJob : AbstractJob {
private val logger = LoggerFactory.getLogger(javaClass)

@Inject
private lateinit var animeService: AnimeService

@Inject
private lateinit var episodeMappingService: EpisodeMappingService

@Inject
private lateinit var episodeVariantService: EpisodeVariantService

@Inject
private lateinit var animationDigitalNetworkPlatform: AnimationDigitalNetworkPlatform

@Inject
private lateinit var crunchyrollPlatform: CrunchyrollPlatform

@Inject
private lateinit var configService: ConfigService

@Inject
private lateinit var episodeCacheService: ConfigCacheService

override fun run() {
val config = configService.findByName(ConfigPropertyKey.LAST_FETCH_OLD_EPISODES.key) ?: run {
logger.warning("Config ${ConfigPropertyKey.LAST_FETCH_OLD_EPISODES.key} not found")
return
}

val range = episodeCacheService.getValueAsInt(ConfigPropertyKey.FETCH_OLD_EPISODES_RANGE)

if (range == -1) {
logger.warning("Config ${ConfigPropertyKey.FETCH_OLD_EPISODES_RANGE.key} not found")
return
}

val to = LocalDate.parse(config.propertyValue!!)
val from = to.minusDays(range.toLong())
val dates = from.datesUntil(to.plusDays(1), Period.ofDays(1)).toList().sorted()
val simulcasts = dates.map { "${Constant.seasons[(it.monthValue - 1) / 3]}-${it.year}".lowercase().replace("autumn", "fall") }.toSet()
val episodes = mutableListOf<AbstractPlatform.Episode>()
val start = System.currentTimeMillis()
logger.info("Fetching old episodes... (From ${dates.first()} to ${dates.last()})")

episodes.addAll(fetchAnimationDigitalNetwork(CountryCode.FR, dates))
episodes.addAll(fetchCrunchyroll(CountryCode.FR, simulcasts))

episodes.removeIf { it.releaseDateTime.toLocalDate() !in dates }

episodes.groupBy { it.anime + it.releaseDateTime.toLocalDate().toString() }.forEach { (_, animeDayEpisodes) ->
if (animeDayEpisodes.size > 5) {
logger.warning("More than 5 episodes for ${animeDayEpisodes.first().anime} on ${animeDayEpisodes.first().releaseDateTime.toLocalDate()}, removing...")
episodes.removeAll(animeDayEpisodes)
return@forEach
}
}

logger.info("Found ${episodes.size} episodes, saving...")
var realSaved = 0

val variants = episodes.sortedBy { it.releaseDateTime }.map { episode ->
episodeVariantService.findByIdentifier(episode.getIdentifier()) ?: run {
realSaved++
episodeVariantService.save(episode)
}
}

logger.info("Saved $realSaved episodes")
logger.info("Updating mappings...")

variants.groupBy { it.mapping!!.uuid }.forEach { (mappingUuid, variants) ->
val mapping = episodeMappingService.find(mappingUuid) ?: return@forEach
mapping.releaseDateTime = variants.minOf { it.releaseDateTime }
mapping.lastReleaseDateTime = variants.maxOf { it.releaseDateTime }
episodeMappingService.update(mapping)
}

logger.info("Updating animes...")

variants.groupBy { it.mapping!!.anime!!.uuid }.forEach { (animeUuid, variants) ->
val anime = animeService.find(animeUuid) ?: return@forEach
logger.info("Updating ${StringUtils.getShortName(anime.name!!)}...")
anime.releaseDateTime = variants.minOf { it.releaseDateTime }
anime.lastReleaseDateTime = variants.maxOf { it.releaseDateTime }
animeService.update(anime)
}

logger.info("Updating simulcasts...")
animeService.recalculateSimulcasts()
MapCache.invalidate(Anime::class.java, EpisodeMapping::class.java, EpisodeVariant::class.java, Simulcast::class.java)
logger.info("Updating config to the next fetch date...")
config.propertyValue = from.plusDays(1).toString()
configService.update(config)
logger.info("Take ${(System.currentTimeMillis() - start) / 1000}s to check ${dates.size} dates")
}

private fun fetchAnimationDigitalNetwork(countryCode: CountryCode, dates: List<LocalDate>): List<AbstractPlatform.Episode> {
val episodes = mutableListOf<AbstractPlatform.Episode>()

dates.forEachIndexed { _, date ->
val zonedDateTime = date.atStartOfDay(Constant.utcZoneId)

runBlocking {
animationDigitalNetworkPlatform.fetchApiContent(
countryCode,
zonedDateTime
)
}.forEach { episodeJson ->
try {
episodes.addAll(
animationDigitalNetworkPlatform.convertEpisode(
countryCode,
episodeJson.asJsonObject,
zonedDateTime,
false
)
)
} catch (e: Exception) {
logger.log(Level.SEVERE, "Error while converting episode (Episode ID: ${episodeJson.getAsString("id")})", e)
}
}
}

return episodes
}

private fun fetchCrunchyroll(countryCode: CountryCode, simulcasts: Set<String>): List<AbstractPlatform.Episode> {
val accessToken = runBlocking { CrunchyrollWrapper.getAnonymousAccessToken() }
val cms = runBlocking { CrunchyrollWrapper.getCMS(accessToken) }
val episodes = mutableListOf<AbstractPlatform.Episode>()

val series = simulcasts.flatMap { simulcastId ->
runBlocking {
CrunchyrollWrapper.getBrowse(
countryCode.locale,
accessToken,
sortBy = CrunchyrollWrapper.SortType.POPULARITY,
type = CrunchyrollWrapper.MediaType.SERIES,
100,
simulcast = simulcastId
)
}
}

val titles = series.map { jsonObject -> jsonObject.getAsString("title")!!.lowercase() }.toSet()
val ids = series.map { jsonObject -> jsonObject.getAsString("id")!! }.toSet()

crunchyrollPlatform.simulcasts[CountryCode.FR] = titles

series.forEach {
val postersTall = it.getAsJsonObject("images").getAsJsonArray("poster_tall")[0].asJsonArray
val postersWide = it.getAsJsonObject("images").getAsJsonArray("poster_wide")[0].asJsonArray
val image = postersTall?.maxByOrNull { poster -> poster.asJsonObject.getAsInt("width")!! }?.asJsonObject?.getAsString("source")!!
val banner = postersWide?.maxByOrNull { poster -> poster.asJsonObject.getAsInt("width")!! }?.asJsonObject?.getAsString("source")!!
val description = it.getAsString("description")

crunchyrollPlatform.animeInfoCache[CountryCodeIdKeyCache(countryCode, it.getAsString("id")!!)] =
CrunchyrollPlatform.CrunchyrollAnimeContent(image = image, banner = banner, description = description)
}

val episodeIds = ids.parallelStream().map { seriesId ->
runBlocking { CrunchyrollWrapper.getSeasons(countryCode.locale, accessToken, cms, seriesId) }
.filter { jsonObject ->
jsonObject.getAsJsonArray("subtitle_locales").map { it.asString }.contains(countryCode.locale)
}
.map { jsonObject -> jsonObject.getAsString("id")!! }
.flatMap { id ->
runBlocking {
CrunchyrollWrapper.getEpisodes(
countryCode.locale,
accessToken,
cms,
id
)
}
}
.map { jsonObject -> jsonObject.getAsString("id")!! }
}.toList().flatten().toSet()

episodeIds.chunked(25).parallelStream().forEach { episodeIdsChunked ->
val `object` = runBlocking {
CrunchyrollWrapper.getObject(
countryCode.locale,
accessToken,
cms,
*episodeIdsChunked.toTypedArray()
)
}

`object`.forEach { episodeJson ->
try {
episodes.add(
crunchyrollPlatform.convertEpisode(
countryCode,
episodeJson,
false,
)
)
} catch (e: Exception) {
logger.log(Level.SEVERE, "Error while converting episode (Episode ID: ${episodeJson.getAsString("id")})", e)
}
}
}

return episodes
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ class AnimationDigitalNetworkPlatform :
return list
}

private fun convertEpisode(
fun convertEpisode(
countryCode: CountryCode,
jsonObject: JsonObject,
zonedDateTime: ZonedDateTime
zonedDateTime: ZonedDateTime,
needSimulcast: Boolean = true
): List<Episode> {
val show = requireNotNull(jsonObject.getAsJsonObject("show")) { "Show is null" }
val season = jsonObject.getAsString("season")?.toIntOrNull() ?: 1
Expand All @@ -83,19 +84,21 @@ class AnimationDigitalNetworkPlatform :
)
}) && !contains) throw Exception("Anime is not an animation")

var isSimulcasted = show.getAsBoolean("simulcast") == true ||
show.getAsString("firstReleaseYear") in (0..1).map { (zonedDateTime.year - it).toString() } ||
contains
if (needSimulcast) {
var isSimulcasted = show.getAsBoolean("simulcast") == true ||
show.getAsString("firstReleaseYear") in (0..1).map { (zonedDateTime.year - it).toString() } ||
contains

val descriptionLowercase = animeDescription.lowercase()
val descriptionLowercase = animeDescription.lowercase()

isSimulcasted = isSimulcasted ||
configCacheService.getValueAsString(ConfigPropertyKey.ANIMATION_DITIGAL_NETWORK_SIMULCAST_DETECTION_REGEX)
?.let {
Regex(it).containsMatchIn(descriptionLowercase)
} == true
isSimulcasted = isSimulcasted ||
configCacheService.getValueAsString(ConfigPropertyKey.ANIMATION_DITIGAL_NETWORK_SIMULCAST_DETECTION_REGEX)
?.let {
Regex(it).containsMatchIn(descriptionLowercase)
} == true

if (!isSimulcasted) throw AnimeNotSimulcastedException("Anime is not simulcasted")
if (!isSimulcasted) throw AnimeNotSimulcastedException("Anime is not simulcasted")
}

val releaseDateString = requireNotNull(jsonObject.getAsString("releaseDate")) { "Release date is null" }
val releaseDate = ZonedDateTime.parse(releaseDateString)
Expand Down
Loading

0 comments on commit 4f890cc

Please sign in to comment.