Skip to content

Commit

Permalink
chore: add episode url on calendar release dto and optimize sitemap.xml
Browse files Browse the repository at this point in the history
  • Loading branch information
Ziedelth committed Sep 17, 2024
1 parent 119ef92 commit 1d6d770
Show file tree
Hide file tree
Showing 19 changed files with 201 additions and 137 deletions.
63 changes: 41 additions & 22 deletions src/main/kotlin/fr/shikkanime/controllers/site/SEOController.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
package fr.shikkanime.controllers.site

import com.google.inject.Inject
import fr.shikkanime.dtos.URLDto
import fr.shikkanime.entities.SortParameter
import fr.shikkanime.entities.enums.CountryCode
import fr.shikkanime.entities.enums.EpisodeType
import fr.shikkanime.entities.enums.Link
import fr.shikkanime.services.caches.AnimeCacheService
import fr.shikkanime.services.caches.EpisodeMappingCacheService
import fr.shikkanime.services.caches.SimulcastCacheService
import fr.shikkanime.utils.Constant
import fr.shikkanime.utils.routes.Controller
import fr.shikkanime.utils.routes.Path
import fr.shikkanime.utils.routes.Response
import fr.shikkanime.utils.routes.method.Get
import fr.shikkanime.utils.withUTCString
import io.ktor.http.*
import java.time.ZonedDateTime

@Controller("/")
class SEOController {
@Inject
private lateinit var animeCacheService: AnimeCacheService
private fun ZonedDateTime.formatDateTime() = this.withUTCString().replace("Z", "+00:00")

@Inject
private lateinit var episodeMappingCacheService: EpisodeMappingCacheService
Expand All @@ -38,36 +40,53 @@ class SEOController {
@Path("sitemap.xml")
@Get
private fun sitemap(): Response {
val simulcasts = simulcastCacheService.findAll()!!.toMutableList()
val animes = animeCacheService.findAll()!!
.sortedBy { it.shortName.lowercase() }

simulcasts.forEach { simulcast ->
simulcast.lastReleaseDateTime =
animes.filter { anime -> anime.simulcasts!!.size == 1 && anime.simulcasts.any { it.uuid == simulcast.uuid } }
.maxByOrNull { d -> ZonedDateTime.parse(d.releaseDateTime) }?.releaseDateTime
}
val globalLastModification = "2024-03-20T17:00:00+00:00"

simulcasts.removeIf { simulcast -> simulcast.lastReleaseDateTime == null }

val episodeMapping = episodeMappingCacheService.findAllBy(
val lastReleaseDateTime = episodeMappingCacheService.findAllBy(
CountryCode.FR,
null,
null,
listOf(SortParameter("lastReleaseDateTime", SortParameter.Order.DESC)),
1,
1
)!!.data.firstOrNull()
)?.data?.firstOrNull()?.lastReleaseDateTime?.replace("Z", "+00:00") ?: globalLastModification

val urls = mutableSetOf(
URLDto(Constant.baseUrl, lastReleaseDateTime),
URLDto("${Constant.baseUrl}/calendar", lastReleaseDateTime),
URLDto("${Constant.baseUrl}/search", globalLastModification)
)

simulcastCacheService.findAllModified()?.mapTo(urls) {
URLDto("${Constant.baseUrl}/catalog/${it.slug}", it.lastReleaseDateTime!!)
}

episodeMappingCacheService.findAllSeo()?.groupBy { it[0] as String }?.forEach { (animeSlug, episodes) ->
val seasonMap = episodes.groupBy { it[1] as Int }
val firstSeasonDateTime = seasonMap.values.flatten().maxOf { it[4] as ZonedDateTime }

urls.add(URLDto("${Constant.baseUrl}/animes/$animeSlug", firstSeasonDateTime.formatDateTime()))

seasonMap.forEach { (season, seasonEpisodes) ->
val lastSeasonDateTime = seasonEpisodes.maxOf { it[4] as ZonedDateTime }
urls.add(URLDto("${Constant.baseUrl}/animes/$animeSlug/season-$season", lastSeasonDateTime.formatDateTime()))

seasonEpisodes.forEach {
val episodeType = it[2] as EpisodeType
val number = it[3] as Int
val episodeDateTime = it[4] as ZonedDateTime
urls.add(URLDto("${Constant.baseUrl}/animes/$animeSlug/season-$season/${episodeType.slug}-$number", episodeDateTime.formatDateTime()))
}
}
}

Link.entries.filter { !it.href.startsWith("/admin") && it.footer }
.mapTo(urls) { URLDto("${Constant.baseUrl}${it.href}", globalLastModification) }

return Response.template(
"/site/seo/sitemap.ftl",
null,
mutableMapOf(
"episodeMapping" to episodeMapping,
"simulcasts" to simulcasts,
"animes" to animes,
"seoLinks" to Link.entries.filter { !it.href.startsWith("/admin") && it.footer }.toList()
),
mutableMapOf("urls" to urls),
contentType = ContentType.Text.Xml
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package fr.shikkanime.converters.episode_mapping

import com.google.inject.Inject
import fr.shikkanime.converters.AbstractConverter
import fr.shikkanime.dtos.PlatformDto
import fr.shikkanime.dtos.mappings.EpisodeMappingWithoutAnimeDto
import fr.shikkanime.dtos.variants.EpisodeVariantWithoutMappingDto
import fr.shikkanime.entities.EpisodeMapping
import fr.shikkanime.entities.enums.LangType
import fr.shikkanime.services.EpisodeVariantService
import fr.shikkanime.utils.withUTCString

class EpisodeMappingToEpisodeMappingWithoutAnimeDtoConverter :
AbstractConverter<EpisodeMapping, EpisodeMappingWithoutAnimeDto>() {
@Inject
private lateinit var episodeVariantService: EpisodeVariantService

override fun convert(from: EpisodeMapping): EpisodeMappingWithoutAnimeDto {
val variants = episodeVariantService.findAllByMapping(from).sortedBy { it.releaseDateTime }

return EpisodeMappingWithoutAnimeDto(
uuid = from.uuid!!,
releaseDateTime = from.releaseDateTime.withUTCString(),
Expand All @@ -20,6 +30,14 @@ class EpisodeMappingToEpisodeMappingWithoutAnimeDtoConverter :
title = from.title,
description = from.description,
image = from.image!!,
variants = convert(variants, EpisodeVariantWithoutMappingDto::class.java),
platforms = convert(
variants.mapNotNull { it.platform }.sortedBy { it.name }.toSet(),
PlatformDto::class.java
)?.toList(),
langTypes = variants.map { LangType.fromAudioLocale(from.anime!!.countryCode!!, it.audioLocale!!) }
.distinct()
.sorted(),
status = from.status
)
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/kotlin/fr/shikkanime/dtos/URLDto.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package fr.shikkanime.dtos

data class URLDto(
val absoluteURL: String,
val lastModification: String,
)
7 changes: 3 additions & 4 deletions src/main/kotlin/fr/shikkanime/dtos/WeeklyAnimeDto.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package fr.shikkanime.dtos

import fr.shikkanime.dtos.mappings.EpisodeMappingWithoutAnimeDto
import fr.shikkanime.entities.enums.EpisodeType
import fr.shikkanime.entities.enums.LangType
import java.util.*

data class WeeklyAnimeDto(
val anime: AnimeDto,
val platforms: List<PlatformDto>,
val releaseDateTime: String,
val slug: String,
val langType: LangType,
val isReleased: Boolean,
val isMultipleReleased: Boolean,
val mappings: List<UUID>,

val episodeType: EpisodeType? = null,
val minNumber: Int? = null,
val maxNumber: Int? = null,
val number: Int? = null,
val mappings: List<EpisodeMappingWithoutAnimeDto>? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ data class EpisodeMappingDto(
val title: String?,
val description: String?,
val image: String,
val variants: List<EpisodeVariantWithoutMappingDto>?,
val variants: List<EpisodeVariantWithoutMappingDto>? = null,
val platforms: List<PlatformDto>? = null,
val langTypes: List<LangType>? = null,
val status: Status,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package fr.shikkanime.dtos.mappings

import fr.shikkanime.dtos.PlatformDto
import fr.shikkanime.dtos.enums.Status
import fr.shikkanime.dtos.variants.EpisodeVariantWithoutMappingDto
import fr.shikkanime.entities.enums.EpisodeType
import fr.shikkanime.entities.enums.LangType
import java.util.*

data class EpisodeMappingWithoutAnimeDto(
Expand All @@ -16,5 +19,8 @@ data class EpisodeMappingWithoutAnimeDto(
val title: String?,
val description: String?,
val image: String,
val variants: List<EpisodeVariantWithoutMappingDto>? = null,
val platforms: List<PlatformDto>? = null,
val langTypes: List<LangType>? = null,
val status: Status,
)
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,12 @@ class CrunchyrollPlatform :
alreadyFetched: List<Episode>
): List<Episode> {
val list = mutableListOf<Episode>()

val lastWeek = zonedDateTime.minusWeeks(1)
val lastWeekStartOfTheDay = lastWeek.withHour(0).withMinute(0).withSecond(0).withNano(0)
val lastWeek = zonedDateTime.minusWeeks(1).toLocalDate()

episodeVariantService.findAllIdentifierByDateRangeWithoutNextEpisode(
countryCode,
lastWeekStartOfTheDay,
lastWeek.plusSeconds(1),
lastWeek.atStartOfDay(Constant.utcZoneId),
lastWeek.atEndOfTheDay(Constant.utcZoneId),
getPlatform()
).forEach { identifier ->
val crunchyrollId = getCrunchyrollId(identifier) ?: run {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,27 @@ class EpisodeMappingRepository : AbstractRepository<EpisodeMapping>() {
}
}

fun findAllSeo(): List<Tuple> {
return database.entityManager.use {
val cb = it.criteriaBuilder
val query = cb.createTupleQuery()
val root = query.from(getEntityClass())

query.multiselect(
root[EpisodeMapping_.anime][Anime_.slug],
root[EpisodeMapping_.season],
root[EpisodeMapping_.episodeType],
root[EpisodeMapping_.number],
root[EpisodeMapping_.lastReleaseDateTime],
)

query.orderBy(cb.asc(root[EpisodeMapping_.releaseDateTime]))

createReadOnlyQuery(it, query)
.resultList
}
}

fun findByAnimeSeasonEpisodeTypeNumber(
animeUuid: UUID,
season: Int,
Expand Down
25 changes: 25 additions & 0 deletions src/main/kotlin/fr/shikkanime/repositories/SimulcastRepository.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package fr.shikkanime.repositories

import fr.shikkanime.entities.Anime
import fr.shikkanime.entities.Anime_
import fr.shikkanime.entities.Simulcast
import fr.shikkanime.entities.Simulcast_
import jakarta.persistence.Tuple

class SimulcastRepository : AbstractRepository<Simulcast>() {
override fun getEntityClass() = Simulcast::class.java
Expand All @@ -20,6 +23,28 @@ class SimulcastRepository : AbstractRepository<Simulcast>() {
}
}

fun findAllModified(): List<Tuple> {
return database.entityManager.use {
val cb = it.criteriaBuilder
val query = cb.createTupleQuery()

val root = query.from(Anime::class.java)
val simulcastJoin = root.join(Anime_.simulcasts)

query.multiselect(
simulcastJoin,
cb.greatest(root[Anime_.releaseDateTime])
)

query.groupBy(simulcastJoin)
query.orderBy(cb.desc(cb.greatest(root[Anime_.releaseDateTime])))

createReadOnlyQuery(it, query)
.resultList
}
}


fun findBySeasonAndYear(season: String, year: Int): Simulcast? {
return database.entityManager.use {
val cb = it.criteriaBuilder
Expand Down
61 changes: 33 additions & 28 deletions src/main/kotlin/fr/shikkanime/services/AnimeService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@ import fr.shikkanime.dtos.PlatformDto
import fr.shikkanime.dtos.WeeklyAnimeDto
import fr.shikkanime.dtos.WeeklyAnimesDto
import fr.shikkanime.dtos.enums.Status
import fr.shikkanime.dtos.mappings.EpisodeMappingWithoutAnimeDto
import fr.shikkanime.entities.*
import fr.shikkanime.entities.enums.CountryCode
import fr.shikkanime.entities.enums.LangType
import fr.shikkanime.entities.enums.Platform
import fr.shikkanime.repositories.AnimeRepository
import fr.shikkanime.utils.MapCache
import fr.shikkanime.utils.StringUtils
import fr.shikkanime.utils.*
import fr.shikkanime.utils.StringUtils.capitalizeWords
import fr.shikkanime.utils.withUTCString
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.ZoneId
import java.time.ZonedDateTime
Expand Down Expand Up @@ -88,48 +86,55 @@ class AnimeService : AbstractService<Anime, AnimeRepository>() {
fun getWeeklyAnimes(countryCode: CountryCode, member: Member?, startOfWeekDay: LocalDate): List<WeeklyAnimesDto> {
val zoneId = ZoneId.of(countryCode.timezone)
val startOfPreviousWeek = startOfWeekDay.minusWeeks(1).atStartOfDay(zoneId)
val endOfWeek = startOfWeekDay.with(DayOfWeek.SUNDAY).atTime(23, 59, 59).atZone(zoneId)
val endOfWeek = startOfWeekDay.atEndOfWeek().atEndOfTheDay(zoneId)

val tuples = episodeVariantService.findAllAnimeEpisodeMappingReleaseDateTimePlatformAudioLocaleByDateRange(
countryCode, member, startOfPreviousWeek, endOfWeek
)

return startOfWeekDay.datesUntil(startOfWeekDay.plusDays(7)).toList().map { date ->
val dateFormatter = DateTimeFormatter.ofPattern("EEEE", Locale.forLanguageTag(countryCode.locale))
val currentWeek = startOfWeekDay[ChronoField.ALIGNED_WEEK_OF_YEAR]

return (0..6).map { dayOffset ->
val date = startOfWeekDay.plusDays(dayOffset.toLong())
val zonedDate = date.atStartOfDay(zoneId)
val tuplesDay = tuples.filter { (it[2] as ZonedDateTime).withZoneSameInstant(zoneId).dayOfWeek == zonedDate.dayOfWeek }

WeeklyAnimesDto(
date.format(DateTimeFormatter.ofPattern("EEEE", Locale.forLanguageTag(countryCode.locale))).capitalizeWords(),
date.format(dateFormatter).capitalizeWords(),
tuplesDay.groupBy { triple ->
val anime = triple[0] as Anime
Triple(anime, (triple[1] as EpisodeMapping).episodeType!!, LangType.fromAudioLocale(anime.countryCode!!, triple[4] as String))
}.map { (triple, values) ->
val anime = triple.first
anime to LangType.fromAudioLocale(anime.countryCode!!, triple[4] as String)
}.flatMap { (pair, values) ->
val (anime, langType) = pair
val releaseDateTime = values.maxOf { it[2] as ZonedDateTime }

val mappings = values.filter {
(it[2] as ZonedDateTime).withZoneSameInstant(zoneId)[ChronoField.ALIGNED_WEEK_OF_YEAR] == startOfWeekDay[ChronoField.ALIGNED_WEEK_OF_YEAR]
(it[2] as ZonedDateTime).withZoneSameInstant(zoneId)[ChronoField.ALIGNED_WEEK_OF_YEAR] == currentWeek
}.map { it[1] as EpisodeMapping }
.distinctBy { it.uuid }
.sortedWith(compareBy({ it.releaseDateTime }, { it.season }, { it.episodeType }, { it.number }))

WeeklyAnimeDto(
AbstractConverter.convert(anime, AnimeDto::class.java),
AbstractConverter.convert(values.map { it[3] as Platform }.distinct(), PlatformDto::class.java)!!,
releaseDateTime.withUTCString(),
"/animes/${anime.slug}${
mappings.firstOrNull()
?.let { "/season-${it.season}" + ("/${it.episodeType!!.slug}-${it.number}".takeIf { mappings.size <= 1 } ?: "") } ?: ""
}",
triple.third,
mappings.isNotEmpty(),
mappings.size > 1,
mappings.map { it.uuid!! },
mappings.firstOrNull()?.episodeType,
mappings.minOfOrNull { it.number!! },
mappings.maxOfOrNull { it.number!! },
mappings.firstOrNull()?.number
)
mappings.groupBy { it.episodeType }.ifEmpty { mapOf(null to mappings) }.map { (episodeType, episodeMappings) ->
WeeklyAnimeDto(
AbstractConverter.convert(anime, AnimeDto::class.java),
AbstractConverter.convert(values.mapNotNull { it[3] as? Platform }.distinct(), PlatformDto::class.java)!!,
releaseDateTime.withUTCString(),
buildString {
append("/animes/${anime.slug}")
episodeMappings.firstOrNull()?.let {
append("/season-${it.season}")
if (mappings.size <= 1) append("/${it.episodeType!!.slug}-${it.number}")
}
},
langType,
episodeType,
episodeMappings.minOfOrNull { it.number!! },
episodeMappings.maxOfOrNull { it.number!! },
episodeMappings.firstOrNull()?.number,
AbstractConverter.convert(episodeMappings.takeIf { it.isNotEmpty() }, EpisodeMappingWithoutAnimeDto::class.java)
)
}
}.sortedWith(
compareBy(
{ ZonedDateTime.parse(it.releaseDateTime).withZoneSameInstant(zoneId).toLocalTime() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class EpisodeMappingService : AbstractService<EpisodeMapping, EpisodeMappingRepo

fun findAllSimulcastedByAnime(anime: Anime) = episodeMappingRepository.findAllSimulcastedByAnime(anime)

fun findAllSeo() = episodeMappingRepository.findAllSeo()

fun findLastNumber(anime: Anime, episodeType: EpisodeType, season: Int, platform: Platform, audioLocale: String) =
episodeMappingRepository.findLastNumber(anime, episodeType, season, platform, audioLocale)

Expand Down
Loading

0 comments on commit 1d6d770

Please sign in to comment.