diff --git a/src/main/kotlin/fr/shikkanime/caches/CountryCodeUUIDSeasonSortPaginationKeyCache.kt b/src/main/kotlin/fr/shikkanime/caches/CountryCodeUUIDSeasonSortPaginationKeyCache.kt new file mode 100644 index 00000000..9581c9e4 --- /dev/null +++ b/src/main/kotlin/fr/shikkanime/caches/CountryCodeUUIDSeasonSortPaginationKeyCache.kt @@ -0,0 +1,16 @@ +package fr.shikkanime.caches + +import fr.shikkanime.dtos.enums.Status +import fr.shikkanime.entities.SortParameter +import fr.shikkanime.entities.enums.CountryCode +import java.util.* + +data class CountryCodeUUIDSeasonSortPaginationKeyCache( + override val countryCode: CountryCode?, + val uuid: UUID?, + val season: Int?, + val sort: List, + override val page: Int, + override val limit: Int, + val status: Status? = null, +) : CountryCodePaginationKeyCache(countryCode, page, limit) diff --git a/src/main/kotlin/fr/shikkanime/controllers/api/EpisodeMappingController.kt b/src/main/kotlin/fr/shikkanime/controllers/api/EpisodeMappingController.kt index 6feb7d44..116f88e7 100644 --- a/src/main/kotlin/fr/shikkanime/controllers/api/EpisodeMappingController.kt +++ b/src/main/kotlin/fr/shikkanime/controllers/api/EpisodeMappingController.kt @@ -51,6 +51,7 @@ class EpisodeMappingController : HasPageableRoute() { private fun getAll( @QueryParam("country", description = "By default: FR", type = CountryCode::class) countryParam: String?, @QueryParam("anime") animeParam: UUID?, + @QueryParam("season") seasonParam: Int?, @QueryParam("page") pageParam: Int?, @QueryParam("limit") limitParam: Int?, @QueryParam("sort") sortParam: String?, @@ -63,6 +64,7 @@ class EpisodeMappingController : HasPageableRoute() { episodeMappingCacheService.findAllBy( CountryCode.fromNullable(countryParam) ?: CountryCode.FR, animeParam, + seasonParam, sortParameters, page, limit, diff --git a/src/main/kotlin/fr/shikkanime/controllers/site/SEOController.kt b/src/main/kotlin/fr/shikkanime/controllers/site/SEOController.kt index e3136b1c..12d32332 100644 --- a/src/main/kotlin/fr/shikkanime/controllers/site/SEOController.kt +++ b/src/main/kotlin/fr/shikkanime/controllers/site/SEOController.kt @@ -57,7 +57,8 @@ class SEOController { val episodeMapping = episodeMappingCacheService.findAllBy( CountryCode.FR, null, - listOf(SortParameter("releaseDateTime", SortParameter.Order.DESC)), + null, + listOf(SortParameter("lastReleaseDateTime", SortParameter.Order.DESC)), 1, 1 )!!.data.firstOrNull() diff --git a/src/main/kotlin/fr/shikkanime/controllers/site/SiteController.kt b/src/main/kotlin/fr/shikkanime/controllers/site/SiteController.kt index 5ff7351d..6a9f3379 100644 --- a/src/main/kotlin/fr/shikkanime/controllers/site/SiteController.kt +++ b/src/main/kotlin/fr/shikkanime/controllers/site/SiteController.kt @@ -1,7 +1,6 @@ package fr.shikkanime.controllers.site import com.google.inject.Inject -import fr.shikkanime.converters.AbstractConverter import fr.shikkanime.dtos.AnimeDto import fr.shikkanime.entities.SortParameter import fr.shikkanime.entities.enums.ConfigPropertyKey @@ -83,6 +82,7 @@ class SiteController { "episodeMappings" to episodeMappingCacheService.findAllBy( CountryCode.FR, null, + null, listOf( SortParameter("lastReleaseDateTime", SortParameter.Order.DESC), SortParameter("animeName", SortParameter.Order.DESC), @@ -123,8 +123,18 @@ class SiteController { @Path("animes/{slug}") @Get private fun animeDetail(@PathParam("slug") slug: String): Response { - val anime = animeCacheService.findBySlug(CountryCode.FR, slug) ?: return Response.redirect("/404") - val dto = AbstractConverter.convert(anime, AnimeDto::class.java) + val dto = animeCacheService.findBySlug(CountryCode.FR, slug) ?: return Response.redirect("/404") + return Response.redirect("/animes/${dto.slug}/season-${dto.seasons.first().number}") + } + + @Path("animes/{slug}/season-{season}") + @Get + private fun animeDetailBySeason( + @PathParam("slug") slug: String, + @PathParam("season") season: Int + ): Response { + val dto = animeCacheService.findBySlug(CountryCode.FR, slug) ?: return Response.redirect("/404") + val seasonDto = dto.seasons.firstOrNull { it.number == season } ?: return Response.redirect("/404") return Response.template( "/site/anime.ftl", @@ -132,9 +142,11 @@ class SiteController { mutableMapOf( "description" to dto.description?.let { StringUtils.sanitizeXSS(it) }, "anime" to dto, + "season" to seasonDto, "episodeMappings" to episodeMappingCacheService.findAllBy( CountryCode.FR, - anime.uuid, + dto.uuid, + season, listOf( SortParameter("releaseDateTime", SortParameter.Order.ASC), SortParameter("season", SortParameter.Order.ASC), diff --git a/src/main/kotlin/fr/shikkanime/converters/anime/AnimeToAnimeDtoConverter.kt b/src/main/kotlin/fr/shikkanime/converters/anime/AnimeToAnimeDtoConverter.kt index a4299ab0..bb7fa867 100644 --- a/src/main/kotlin/fr/shikkanime/converters/anime/AnimeToAnimeDtoConverter.kt +++ b/src/main/kotlin/fr/shikkanime/converters/anime/AnimeToAnimeDtoConverter.kt @@ -3,18 +3,24 @@ package fr.shikkanime.converters.anime import com.google.inject.Inject import fr.shikkanime.converters.AbstractConverter import fr.shikkanime.dtos.AnimeDto +import fr.shikkanime.dtos.SeasonDto import fr.shikkanime.dtos.SimulcastDto import fr.shikkanime.entities.Anime import fr.shikkanime.entities.enums.LangType +import fr.shikkanime.services.EpisodeMappingService import fr.shikkanime.services.SimulcastService.Companion.sortBySeasonAndYear import fr.shikkanime.services.caches.EpisodeVariantCacheService import fr.shikkanime.utils.StringUtils import fr.shikkanime.utils.withUTCString +import java.time.ZonedDateTime class AnimeToAnimeDtoConverter : AbstractConverter() { @Inject private lateinit var episodeVariantCacheService: EpisodeVariantCacheService + @Inject + private lateinit var episodeMappingService: EpisodeMappingService + override fun convert(from: Anime): AnimeDto { val audioLocales = episodeVariantCacheService.findAllAudioLocalesByAnime(from)!! @@ -35,6 +41,8 @@ class AnimeToAnimeDtoConverter : AbstractConverter() { )?.toList(), audioLocales = audioLocales, langTypes = audioLocales.map { LangType.fromAudioLocale(from.countryCode, it) }.distinct().sorted(), + seasons = episodeMappingService.findAllSeasonsByAnime(from) + .map { SeasonDto(it[0] as Int, (it[1] as ZonedDateTime).withUTCString()) }, status = from.status, ) } diff --git a/src/main/kotlin/fr/shikkanime/dtos/AnimeDto.kt b/src/main/kotlin/fr/shikkanime/dtos/AnimeDto.kt index 599cccdf..ef4e4a94 100644 --- a/src/main/kotlin/fr/shikkanime/dtos/AnimeDto.kt +++ b/src/main/kotlin/fr/shikkanime/dtos/AnimeDto.kt @@ -19,5 +19,6 @@ data class AnimeDto( val simulcasts: List?, val audioLocales: List? = null, val langTypes: List? = null, + val seasons: List = emptyList(), val status: Status? = null, ) diff --git a/src/main/kotlin/fr/shikkanime/dtos/SeasonDto.kt b/src/main/kotlin/fr/shikkanime/dtos/SeasonDto.kt new file mode 100644 index 00000000..30b0844b --- /dev/null +++ b/src/main/kotlin/fr/shikkanime/dtos/SeasonDto.kt @@ -0,0 +1,6 @@ +package fr.shikkanime.dtos + +data class SeasonDto( + val number: Int, + val lastReleaseDateTime: String, +) diff --git a/src/main/kotlin/fr/shikkanime/modules/CustomLuceneAnalysisDefinitionProvider.kt b/src/main/kotlin/fr/shikkanime/modules/CustomLuceneAnalysisDefinitionProvider.kt index 95204393..14683f28 100644 --- a/src/main/kotlin/fr/shikkanime/modules/CustomLuceneAnalysisDefinitionProvider.kt +++ b/src/main/kotlin/fr/shikkanime/modules/CustomLuceneAnalysisDefinitionProvider.kt @@ -10,7 +10,7 @@ class CustomLuceneAnalysisDefinitionProvider : LuceneAnalysisConfigurer { .tokenFilter("lowercase") .tokenFilter("asciifolding") .tokenFilter("ngram") - .param("minGramSize", "2") - .param("maxGramSize", "4") + .param("minGramSize", "3") + .param("maxGramSize", "6") } } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/modules/Routing.kt b/src/main/kotlin/fr/shikkanime/modules/Routing.kt index c2175aeb..7ff93086 100644 --- a/src/main/kotlin/fr/shikkanime/modules/Routing.kt +++ b/src/main/kotlin/fr/shikkanime/modules/Routing.kt @@ -279,6 +279,7 @@ private fun handlePathParam(kParameter: KParameter, parameters: Map pathParamValue?.let { UUID.fromString(it) } Platform::class.java -> pathParamValue?.let { Platform.valueOf(it) } String::class.java -> pathParamValue + Int::class.java -> pathParamValue?.toIntOrNull() else -> throw Exception("Unknown type ${kParameter.type}") } } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/repositories/EpisodeMappingRepository.kt b/src/main/kotlin/fr/shikkanime/repositories/EpisodeMappingRepository.kt index 4999ad22..6b89efbf 100644 --- a/src/main/kotlin/fr/shikkanime/repositories/EpisodeMappingRepository.kt +++ b/src/main/kotlin/fr/shikkanime/repositories/EpisodeMappingRepository.kt @@ -14,6 +14,7 @@ class EpisodeMappingRepository : AbstractRepository() { fun findAllBy( countryCode: CountryCode?, anime: Anime?, + season: Int?, sort: List, page: Int, limit: Int, @@ -26,6 +27,7 @@ class EpisodeMappingRepository : AbstractRepository() { val predicates = mutableListOf() anime?.let { predicates.add(cb.equal(root[EpisodeMapping_.anime], it)) } + season?.let { predicates.add(cb.equal(root[EpisodeMapping_.season], it)) } countryCode?.let { predicates.add(cb.equal(root[EpisodeMapping_.anime][Anime_.countryCode], it)) } status?.let { predicates.add(cb.equal(root[EpisodeMapping_.status], it)) } query.where(*predicates.toTypedArray()) @@ -52,12 +54,13 @@ class EpisodeMappingRepository : AbstractRepository() { } fun findAllUuidAndImage(): List { - return inTransaction { entityManager -> - val cb = entityManager.criteriaBuilder + return inTransaction { + val cb = it.criteriaBuilder val query = cb.createTupleQuery() val root = query.from(getEntityClass()) query.multiselect(root[EpisodeMapping_.uuid], root[EpisodeMapping_.image]) - entityManager.createQuery(query).resultList + createReadOnlyQuery(it, query) + .resultList } } @@ -69,7 +72,24 @@ class EpisodeMappingRepository : AbstractRepository() { query.where(cb.equal(root[EpisodeMapping_.anime], anime)) - it.createQuery(query) + createReadOnlyQuery(it, query) + .resultList + } + } + + fun findAllSeasonsByAnime(anime: Anime): List { + return inTransaction { + val cb = it.criteriaBuilder + val query = cb.createTupleQuery() + val root = query.from(getEntityClass()) + + query.multiselect(root[EpisodeMapping_.season], cb.greatest(root[EpisodeMapping_.lastReleaseDateTime])) + query.groupBy(root[EpisodeMapping_.season]) + query.where(cb.equal(root[EpisodeMapping_.anime], anime)) + query.orderBy(cb.asc(root[EpisodeMapping_.season])) + query.distinct(true) + + createReadOnlyQuery(it, query) .resultList } } @@ -94,7 +114,7 @@ class EpisodeMappingRepository : AbstractRepository() { ) ) - it.createQuery(query) + createReadOnlyQuery(it, query) .resultList .firstOrNull() } @@ -125,7 +145,7 @@ class EpisodeMappingRepository : AbstractRepository() { ) query.orderBy(cb.desc(root[EpisodeVariant_.mapping][EpisodeMapping_.number])) - it.createQuery(query) + createReadOnlyQuery(it, query) .resultList .firstOrNull() ?: 0 } diff --git a/src/main/kotlin/fr/shikkanime/repositories/EpisodeVariantRepository.kt b/src/main/kotlin/fr/shikkanime/repositories/EpisodeVariantRepository.kt index 95a9642c..6f2a2dbd 100644 --- a/src/main/kotlin/fr/shikkanime/repositories/EpisodeVariantRepository.kt +++ b/src/main/kotlin/fr/shikkanime/repositories/EpisodeVariantRepository.kt @@ -125,7 +125,7 @@ class EpisodeVariantRepository : AbstractRepository() { } } - fun findAllSimulcasted(countryCode: CountryCode): List { + fun findAllSimulcastedByAnime(anime: Anime): List { return inTransaction { entityManager -> val cb = entityManager.criteriaBuilder val query = cb.createQuery(EpisodeMapping::class.java) @@ -135,12 +135,18 @@ class EpisodeVariantRepository : AbstractRepository() { .select(root[EpisodeVariant_.mapping]) .where( cb.and( - cb.notEqual(root[EpisodeVariant_.audioLocale], countryCode.locale), + cb.notEqual(root[EpisodeVariant_.audioLocale], anime.countryCode!!.locale), cb.notEqual(root[EpisodeVariant_.mapping][EpisodeMapping_.episodeType], EpisodeType.FILM), cb.notEqual(root[EpisodeVariant_.mapping][EpisodeMapping_.episodeType], EpisodeType.SUMMARY), + cb.equal(root[EpisodeVariant_.mapping][EpisodeMapping_.anime], anime) ) ) - .orderBy(cb.asc(root[EpisodeVariant_.mapping][EpisodeMapping_.releaseDateTime])) + .orderBy( + cb.asc(root[EpisodeVariant_.mapping][EpisodeMapping_.releaseDateTime]), + cb.asc(root[EpisodeVariant_.mapping][EpisodeMapping_.season]), + cb.asc(root[EpisodeVariant_.mapping][EpisodeMapping_.episodeType]), + cb.asc(root[EpisodeVariant_.mapping][EpisodeMapping_.number]), + ) createReadOnlyQuery(entityManager, query) .resultList diff --git a/src/main/kotlin/fr/shikkanime/services/AnimeService.kt b/src/main/kotlin/fr/shikkanime/services/AnimeService.kt index 334397f8..adea4ad1 100644 --- a/src/main/kotlin/fr/shikkanime/services/AnimeService.kt +++ b/src/main/kotlin/fr/shikkanime/services/AnimeService.kt @@ -133,18 +133,13 @@ class AnimeService : AbstractService() { fun recalculateSimulcasts() { findAll().forEach { anime -> anime.simulcasts.clear() - update(anime) - } - episodeVariantService.findAllSimulcasted(CountryCode.FR) - .forEach { episodeMapping -> - val anime = find(episodeMapping.anime!!.uuid!!)!! + episodeVariantService.findAllSimulcastedByAnime(anime).forEach { episodeMapping -> addSimulcastToAnime(anime, episodeVariantService.getSimulcast(anime, episodeMapping)) - - if (episodeMapping.anime != anime) { - update(anime) - } } + + update(anime) + } } override fun save(entity: Anime): Anime { diff --git a/src/main/kotlin/fr/shikkanime/services/EpisodeMappingService.kt b/src/main/kotlin/fr/shikkanime/services/EpisodeMappingService.kt index 0c270ef4..4eb692f5 100644 --- a/src/main/kotlin/fr/shikkanime/services/EpisodeMappingService.kt +++ b/src/main/kotlin/fr/shikkanime/services/EpisodeMappingService.kt @@ -34,16 +34,19 @@ class EpisodeMappingService : AbstractService, page: Int, limit: Int, status: Status? = null - ) = episodeMappingRepository.findAllBy(countryCode, anime, sort, page, limit, status) + ) = episodeMappingRepository.findAllBy(countryCode, anime, season, sort, page, limit, status) fun findAllUuidAndImage() = episodeMappingRepository.findAllUuidAndImage() fun findAllByAnime(anime: Anime) = episodeMappingRepository.findAllByAnime(anime) + fun findAllSeasonsByAnime(anime: Anime) = episodeMappingRepository.findAllSeasonsByAnime(anime) + fun findLastNumber(anime: Anime, episodeType: EpisodeType, season: Int, platform: Platform, audioLocale: String) = episodeMappingRepository.findLastNumber(anime, episodeType, season, platform, audioLocale) diff --git a/src/main/kotlin/fr/shikkanime/services/EpisodeVariantService.kt b/src/main/kotlin/fr/shikkanime/services/EpisodeVariantService.kt index 978c4d04..3177dd5d 100644 --- a/src/main/kotlin/fr/shikkanime/services/EpisodeVariantService.kt +++ b/src/main/kotlin/fr/shikkanime/services/EpisodeVariantService.kt @@ -9,12 +9,15 @@ import fr.shikkanime.platforms.AbstractPlatform import fr.shikkanime.repositories.EpisodeVariantRepository 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.StringUtils import java.time.ZonedDateTime import java.time.temporal.ChronoUnit class EpisodeVariantService : AbstractService() { + private val logger = LoggerFactory.getLogger(javaClass) + @Inject private lateinit var episodeVariantRepository: EpisodeVariantRepository @@ -50,7 +53,7 @@ class EpisodeVariantService : AbstractService(classes = listOf(Anime::class.java)) { + private val findBySlugCache = MapCache(classes = listOf(Anime::class.java)) { animeService.findBySlug(it.countryCode, it.id) + .let { anime -> AbstractConverter.convert(anime, AnimeDto::class.java) } } private val weeklyMemberCache = diff --git a/src/main/kotlin/fr/shikkanime/services/caches/EpisodeMappingCacheService.kt b/src/main/kotlin/fr/shikkanime/services/caches/EpisodeMappingCacheService.kt index e7745d89..4f2fbf57 100644 --- a/src/main/kotlin/fr/shikkanime/services/caches/EpisodeMappingCacheService.kt +++ b/src/main/kotlin/fr/shikkanime/services/caches/EpisodeMappingCacheService.kt @@ -1,7 +1,7 @@ package fr.shikkanime.services.caches import com.google.inject.Inject -import fr.shikkanime.caches.CountryCodeUUIDSortPaginationKeyCache +import fr.shikkanime.caches.CountryCodeUUIDSeasonSortPaginationKeyCache import fr.shikkanime.dtos.EpisodeMappingDto import fr.shikkanime.dtos.PageableDto import fr.shikkanime.dtos.enums.Status @@ -22,7 +22,7 @@ class EpisodeMappingCacheService : AbstractCacheService { private lateinit var animeService: AnimeService private val findAllByCache = - MapCache>( + MapCache>( classes = listOf( EpisodeMapping::class.java, EpisodeVariant::class.java @@ -32,6 +32,7 @@ class EpisodeMappingCacheService : AbstractCacheService { episodeMappingService.findAllBy( it.countryCode, animeService.find(it.uuid), + it.season, it.sort, it.page, it.limit, @@ -44,9 +45,10 @@ class EpisodeMappingCacheService : AbstractCacheService { fun findAllBy( countryCode: CountryCode?, anime: UUID?, + season: Int?, sort: List, page: Int, limit: Int, status: Status? = null - ) = findAllByCache[CountryCodeUUIDSortPaginationKeyCache(countryCode, anime, sort, page, limit, status)] + ) = findAllByCache[CountryCodeUUIDSeasonSortPaginationKeyCache(countryCode, anime, season, sort, page, limit, status)] } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/socialnetworks/AbstractSocialNetwork.kt b/src/main/kotlin/fr/shikkanime/socialnetworks/AbstractSocialNetwork.kt index 87dede92..8c479f8e 100644 --- a/src/main/kotlin/fr/shikkanime/socialnetworks/AbstractSocialNetwork.kt +++ b/src/main/kotlin/fr/shikkanime/socialnetworks/AbstractSocialNetwork.kt @@ -53,7 +53,7 @@ abstract class AbstractSocialNetwork { } protected fun getShikkanimeUrl(episodeDto: EpisodeVariantDto) = - "${Constant.baseUrl}/animes/${episodeDto.mapping.anime.slug}?utm_campaign=episode_post&utm_medium=social&utm_source=${utmSource()}&utm_content=${episodeDto.uuid}" + "${Constant.baseUrl}/animes/${episodeDto.mapping.anime.slug}/season-${episodeDto.mapping.season}?utm_campaign=episode_post&utm_medium=social&utm_source=${utmSource()}&utm_content=${episodeDto.uuid}" abstract fun sendEpisodeRelease(episodeDto: EpisodeVariantDto, mediaImage: ByteArray) diff --git a/src/main/resources/templates/_freemarker_implicit.ftl b/src/main/resources/templates/_freemarker_implicit.ftl index 0a52803c..4574b55e 100644 --- a/src/main/resources/templates/_freemarker_implicit.ftl +++ b/src/main/resources/templates/_freemarker_implicit.ftl @@ -21,6 +21,7 @@ [#-- @ftlvariable name="su" type="fr.shikkanime.utils.StringUtils" --] [#-- @ftlvariable name="weeklyAnimes" type="kotlin.collections.AbstractList" --] [#-- @ftlvariable name="query" type="java.lang.String" --] +[#-- @ftlvariable name="season" type="fr.shikkanime.dtos.SeasonDto" --] [#-- @ftlvariable name="analyticsDomain" type="java.lang.String" --] [#-- @ftlvariable name="analyticsApi" type="java.lang.String" --] diff --git a/src/main/resources/templates/site/anime.ftl b/src/main/resources/templates/site/anime.ftl index a4d5a2d4..9ab34258 100644 --- a/src/main/resources/templates/site/anime.ftl +++ b/src/main/resources/templates/site/anime.ftl @@ -2,8 +2,8 @@ <#import "components/episode-mapping.ftl" as episodeMappingComponent /> <#import "components/langType.ftl" as langTypeComponent /> -<@navigation.display canonicalUrl="${baseUrl}/animes/${anime.slug}" openGraphImage="${apiUrl}/v1/attachments?uuid=${anime.uuid}&type=banner"> -
+<@navigation.display canonicalUrl="${baseUrl}/animes/${anime.slug}/season-${season.number}" openGraphImage="${apiUrl}/v1/attachments?uuid=${anime.uuid}&type=banner"> +
${anime.description} + + + +
-
+
<#list episodeMappings as episodeMapping> <@episodeMappingComponent.display episodeMapping=episodeMapping cover=false desktopColSize="col-md-2" mobileColSize="col-6" /> diff --git a/src/main/resources/templates/site/calendar.ftl b/src/main/resources/templates/site/calendar.ftl index 6ce33c03..3cf595e6 100644 --- a/src/main/resources/templates/site/calendar.ftl +++ b/src/main/resources/templates/site/calendar.ftl @@ -19,7 +19,8 @@ <#list dailyAnimes.releases as release>
-