diff --git a/build.gradle.kts b/build.gradle.kts index 462195b5..41945abf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,10 +2,29 @@ import java.net.URI val ktorVersion = "2.3.8" val kotlinVersion = "1.9.22" +val ktorSwaggerUiVersion = "2.7.4" +val hibernateCoreVersion = "6.4.4.Final" val hibernateSearchVersion = "7.1.0.CR1" -val junitVersion = "5.10.2" val tikaVersion = "3.0.0-BETA" -val ktorSwaggerUiVersion = "2.7.4" +val postgresqlVersion = "42.7.0" +val reflectionsVersion = "0.10.2" +val guiceVersion = "7.0.0" +val liquibaseCoreVersion = "4.26.0" +val quartzVersion = "2.5.0-rc1" +val guavaVersion = "33.0.0-jre" +val jacksonVersion = "2.16.1" +val playwrightVersion = "1.41.2" +val jsoupVersion = "1.17.2" +val gsonVersion = "2.10.1" +val openCvVersion = "4.9.0-0" +val bcprovVersion = "1.77" +val javaImageScalingVersion = "0.8.6" +val jdaVersion = "5.0.0-beta.20" +val twitter4jVersion = "4.0.7" +val twitter4jV2Version = "1.4.3" + +val junitVersion = "5.10.2" +val h2Version = "2.2.224" plugins { kotlin("jvm") version "1.9.22" @@ -48,35 +67,35 @@ dependencies { implementation("io.ktor:ktor-client-okhttp:$ktorVersion") implementation("io.ktor:ktor-client-okhttp-jvm:$ktorVersion") implementation("io.github.smiley4:ktor-swagger-ui:$ktorSwaggerUiVersion") - implementation("org.hibernate.orm:hibernate-core:6.4.4.Final") + implementation("org.hibernate.orm:hibernate-core:$hibernateCoreVersion") implementation("org.hibernate.search:hibernate-search-mapper-orm:$hibernateSearchVersion") implementation("org.hibernate.search:hibernate-search-backend-lucene:$hibernateSearchVersion") - implementation("org.postgresql:postgresql:42.7.0") - implementation("org.reflections:reflections:0.10.2") - implementation("com.google.inject:guice:7.0.0") - implementation("org.liquibase:liquibase-core:4.26.0") - implementation("org.quartz-scheduler:quartz:2.5.0-rc1") - implementation("com.google.guava:guava:33.0.0-jre") - implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.16.1") - implementation("com.microsoft.playwright:playwright:1.41.2") - implementation("org.jsoup:jsoup:1.17.2") - implementation("com.google.code.gson:gson:2.10.1") - implementation("org.openpnp:opencv:4.9.0-0") - implementation("org.bouncycastle:bcprov-jdk18on:1.77") - implementation("com.mortennobel:java-image-scaling:0.8.6") + implementation("org.postgresql:postgresql:$postgresqlVersion") + implementation("org.reflections:reflections:$reflectionsVersion") + implementation("com.google.inject:guice:$guiceVersion") + implementation("org.liquibase:liquibase-core:$liquibaseCoreVersion") + implementation("org.quartz-scheduler:quartz:$quartzVersion") + implementation("com.google.guava:guava:$guavaVersion") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:$jacksonVersion") + implementation("com.microsoft.playwright:playwright:$playwrightVersion") + implementation("org.jsoup:jsoup:$jsoupVersion") + implementation("com.google.code.gson:gson:$gsonVersion") + implementation("org.openpnp:opencv:$openCvVersion") + implementation("org.bouncycastle:bcprov-jdk18on:$bcprovVersion") + implementation("com.mortennobel:java-image-scaling:$javaImageScalingVersion") implementation("org.apache.tika:tika-core:$tikaVersion") implementation("org.apache.tika:tika-langdetect-optimaize:$tikaVersion") // Social networks - implementation("com.github.discord-jda:JDA:v5.0.0-beta.20") - implementation("org.twitter4j:twitter4j-core:4.0.7") - implementation("io.github.takke:jp.takke.twitter4j-v2:1.4.3") + implementation("com.github.discord-jda:JDA:$jdaVersion") + implementation("org.twitter4j:twitter4j-core:$twitter4jVersion") + implementation("io.github.takke:jp.takke.twitter4j-v2:$twitter4jV2Version") testImplementation("io.ktor:ktor-server-tests-jvm:$ktorVersion") testImplementation("io.ktor:ktor-client-mock:$ktorVersion") testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion") - testImplementation("com.h2database:h2:2.2.224") + testImplementation("com.h2database:h2:$h2Version") } kotlin { diff --git a/src/main/kotlin/fr/shikkanime/Application.kt b/src/main/kotlin/fr/shikkanime/Application.kt index bf129908..9e6e81c4 100644 --- a/src/main/kotlin/fr/shikkanime/Application.kt +++ b/src/main/kotlin/fr/shikkanime/Application.kt @@ -49,7 +49,7 @@ fun main() { JobManager.scheduleJob("0 0 * * * ?", SavingImageCacheJob::class.java) JobManager.scheduleJob("0 */10 * * * ?", GarbageCollectorJob::class.java) JobManager.scheduleJob("0 0 0 * * ?", DeleteOldMetricsJob::class.java) - JobManager.scheduleJob("0 0 * * * ?", FetchOldEpisodeDescriptionJob::class.java) + JobManager.scheduleJob("0 0 * * * ?", FetchDeprecatedEpisodeJob::class.java) JobManager.start() logger.info("Starting server...") diff --git a/src/main/kotlin/fr/shikkanime/controllers/admin/AdminPlatformController.kt b/src/main/kotlin/fr/shikkanime/controllers/admin/AdminPlatformController.kt index 20e33fee..a50f25b6 100644 --- a/src/main/kotlin/fr/shikkanime/controllers/admin/AdminPlatformController.kt +++ b/src/main/kotlin/fr/shikkanime/controllers/admin/AdminPlatformController.kt @@ -92,7 +92,7 @@ class AdminPlatformController { ?: abstractPlatform.configuration!!.newPlatformSimulcast() simulcast.of(parameters) - if (uuid == null) { + if (simulcast.uuid == null) { simulcast.uuid = UUID.randomUUID() abstractPlatform.configuration?.addPlatformSimulcast(simulcast) } diff --git a/src/main/kotlin/fr/shikkanime/controllers/site/SiteController.kt b/src/main/kotlin/fr/shikkanime/controllers/site/SiteController.kt index 46081572..02bc4d3b 100644 --- a/src/main/kotlin/fr/shikkanime/controllers/site/SiteController.kt +++ b/src/main/kotlin/fr/shikkanime/controllers/site/SiteController.kt @@ -115,7 +115,7 @@ class SiteController { @Get private fun catalogSimulcast(@PathParam("slug") slug: String): Response { val findAll = simulcastCacheService.findAll()!! - val currentSimulcast = findAll.first { it.slug == slug } + val currentSimulcast = findAll.firstOrNull { it.slug == slug } ?: return Response.redirect("/404") return Response.template( Link.CATALOG.template, @@ -138,6 +138,7 @@ class SiteController { @Get private fun error404(): Response { return Response.template( + HttpStatusCode.NotFound, "/site/404.ftl", "Page introuvable" ) diff --git a/src/main/kotlin/fr/shikkanime/converters/episode/EpisodeDtoToEpisodeConverter.kt b/src/main/kotlin/fr/shikkanime/converters/episode/EpisodeDtoToEpisodeConverter.kt deleted file mode 100644 index 16df2173..00000000 --- a/src/main/kotlin/fr/shikkanime/converters/episode/EpisodeDtoToEpisodeConverter.kt +++ /dev/null @@ -1,38 +0,0 @@ -package fr.shikkanime.converters.episode - -import com.google.inject.Inject -import fr.shikkanime.converters.AbstractConverter -import fr.shikkanime.dtos.EpisodeDto -import fr.shikkanime.entities.Anime -import fr.shikkanime.entities.Episode -import fr.shikkanime.services.EpisodeService -import java.time.ZonedDateTime - -class EpisodeDtoToEpisodeConverter : AbstractConverter() { - @Inject - private lateinit var episodeService: EpisodeService - - override fun convert(from: EpisodeDto): Episode { - val foundEpisode = episodeService.findByHash(from.hash) - - if (foundEpisode != null) { - return foundEpisode - } - - return Episode( - platform = from.platform, - anime = convert(from.anime, Anime::class.java), - episodeType = from.episodeType, - langType = from.langType, - hash = from.hash, - releaseDateTime = ZonedDateTime.parse(from.releaseDateTime), - season = from.season, - number = from.number, - title = from.title, - url = from.url, - image = from.image, - duration = from.duration, - description = from.description, - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/converters/episode/EpisodeToEpisodeDtoConverter.kt b/src/main/kotlin/fr/shikkanime/converters/episode/EpisodeToEpisodeDtoConverter.kt index 60637bc7..f6147f22 100644 --- a/src/main/kotlin/fr/shikkanime/converters/episode/EpisodeToEpisodeDtoConverter.kt +++ b/src/main/kotlin/fr/shikkanime/converters/episode/EpisodeToEpisodeDtoConverter.kt @@ -25,6 +25,7 @@ class EpisodeToEpisodeDtoConverter : AbstractConverter() { duration = from.duration, description = from.description, uncensored = from.image!!.contains("nc/", true), + lastUpdateDateTime = from.lastUpdateDateTime?.withUTC()?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) ) } } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/dtos/EpisodeDto.kt b/src/main/kotlin/fr/shikkanime/dtos/EpisodeDto.kt index a3301f28..43392129 100644 --- a/src/main/kotlin/fr/shikkanime/dtos/EpisodeDto.kt +++ b/src/main/kotlin/fr/shikkanime/dtos/EpisodeDto.kt @@ -23,4 +23,5 @@ data class EpisodeDto( val duration: Long, val description: String?, val uncensored: Boolean, + val lastUpdateDateTime: String?, ) : Serializable diff --git a/src/main/kotlin/fr/shikkanime/entities/Episode.kt b/src/main/kotlin/fr/shikkanime/entities/Episode.kt index d934da09..058614be 100644 --- a/src/main/kotlin/fr/shikkanime/entities/Episode.kt +++ b/src/main/kotlin/fr/shikkanime/entities/Episode.kt @@ -40,4 +40,6 @@ data class Episode( var duration: Long = -1, @Column(nullable = true, columnDefinition = "VARCHAR(1000)") var description: String? = null, + @Column(nullable = true, name = "last_update_date_time") + var lastUpdateDateTime: ZonedDateTime? = releaseDateTime, ) : ShikkEntity(uuid) diff --git a/src/main/kotlin/fr/shikkanime/entities/enums/ConfigPropertyKey.kt b/src/main/kotlin/fr/shikkanime/entities/enums/ConfigPropertyKey.kt index 1a690c6e..a3e79b84 100644 --- a/src/main/kotlin/fr/shikkanime/entities/enums/ConfigPropertyKey.kt +++ b/src/main/kotlin/fr/shikkanime/entities/enums/ConfigPropertyKey.kt @@ -12,4 +12,5 @@ enum class ConfigPropertyKey(val key: String) { SOCIAL_NETWORK_EPISODES_SIZE_LIMIT("social_network_episodes_size_limit"), FETCH_OLD_EPISODE_DESCRIPTION_SIZE("fetch_old_episode_description_size"), GOOGLE_SITE_VERIFICATION_ID("google_site_verification_id"), + FETCH_DEPRECATED_EPISODE_DATE("fetch_deprecated_episode_date"), } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/jobs/AbstractJob.kt b/src/main/kotlin/fr/shikkanime/jobs/AbstractJob.kt index d12fd588..8064938b 100644 --- a/src/main/kotlin/fr/shikkanime/jobs/AbstractJob.kt +++ b/src/main/kotlin/fr/shikkanime/jobs/AbstractJob.kt @@ -1,5 +1,5 @@ package fr.shikkanime.jobs -abstract class AbstractJob { - abstract fun run() +fun interface AbstractJob { + fun run() } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/jobs/DeleteOldMetricsJob.kt b/src/main/kotlin/fr/shikkanime/jobs/DeleteOldMetricsJob.kt index 0bd8feb7..a8d43282 100644 --- a/src/main/kotlin/fr/shikkanime/jobs/DeleteOldMetricsJob.kt +++ b/src/main/kotlin/fr/shikkanime/jobs/DeleteOldMetricsJob.kt @@ -4,7 +4,7 @@ import com.google.inject.Inject import fr.shikkanime.services.MetricService import java.time.ZonedDateTime -class DeleteOldMetricsJob : AbstractJob() { +class DeleteOldMetricsJob : AbstractJob { @Inject private lateinit var metricService: MetricService diff --git a/src/main/kotlin/fr/shikkanime/jobs/FetchOldEpisodeDescriptionJob.kt b/src/main/kotlin/fr/shikkanime/jobs/FetchDeprecatedEpisodeJob.kt similarity index 67% rename from src/main/kotlin/fr/shikkanime/jobs/FetchOldEpisodeDescriptionJob.kt rename to src/main/kotlin/fr/shikkanime/jobs/FetchDeprecatedEpisodeJob.kt index 12ea0d86..c5d81385 100644 --- a/src/main/kotlin/fr/shikkanime/jobs/FetchOldEpisodeDescriptionJob.kt +++ b/src/main/kotlin/fr/shikkanime/jobs/FetchDeprecatedEpisodeJob.kt @@ -5,20 +5,22 @@ import com.google.inject.Inject import fr.shikkanime.entities.Episode import fr.shikkanime.entities.enums.ConfigPropertyKey import fr.shikkanime.entities.enums.CountryCode +import fr.shikkanime.entities.enums.EpisodeType import fr.shikkanime.entities.enums.Platform import fr.shikkanime.services.EpisodeService import fr.shikkanime.services.caches.ConfigCacheService import fr.shikkanime.utils.HttpRequest 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.withUTC import fr.shikkanime.wrappers.AnimationDigitalNetworkWrapper import fr.shikkanime.wrappers.CrunchyrollWrapper import kotlinx.coroutines.runBlocking +import java.time.ZonedDateTime import java.util.logging.Level -class FetchOldEpisodeDescriptionJob : AbstractJob() { +class FetchDeprecatedEpisodeJob : AbstractJob { private val logger = LoggerFactory.getLogger(javaClass) @Inject @@ -31,13 +33,18 @@ class FetchOldEpisodeDescriptionJob : AbstractJob() { val httpRequest = HttpRequest() val anonymousAccessToken = runBlocking { CrunchyrollWrapper.getAnonymousAccessToken() } val cms = runBlocking { CrunchyrollWrapper.getCMS(anonymousAccessToken) } + val takeSize = configCacheService.getValueAsInt(ConfigPropertyKey.FETCH_OLD_EPISODE_DESCRIPTION_SIZE, 0) - val crunchyrollEpisodes = episodeService.findAllByPlatform(Platform.CRUN) - val adnEpisodes = episodeService.findAllByPlatform(Platform.ANIM) + val now = ZonedDateTime.now().withSecond(0).withNano(0).withUTC() + val deprecatedDateTime = now.minusDays( + configCacheService.getValueAsInt(ConfigPropertyKey.FETCH_DEPRECATED_EPISODE_DATE, 30).toLong() + ) + + val crunchyrollEpisodes = episodeService.findAllByPlatformDeprecatedEpisodes(Platform.CRUN, deprecatedDateTime) + val adnEpisodes = episodeService.findAllByPlatformDeprecatedEpisodes(Platform.ANIM, deprecatedDateTime) val episodes = (crunchyrollEpisodes + adnEpisodes) - .filter { it.description.isNullOrBlank() } .shuffled() .take(takeSize) @@ -45,13 +52,23 @@ class FetchOldEpisodeDescriptionJob : AbstractJob() { episodes.forEachIndexed { index, episode -> logger.info("Fetching episode description ${index + 1}/${episodes.size}") - val s = "${episode.anime?.name} - S${episode.season} EP${episode.number}" + + val s = "${episode.anime?.name} - S${episode.season} ${ + when (episode.episodeType!!) { + EpisodeType.EPISODE -> "EP" + EpisodeType.SPECIAL -> "SP" + EpisodeType.FILM -> "MOV" + } + }${episode.number}" try { val content = runBlocking { normalizeContent(episode, httpRequest, anonymousAccessToken, cms) } ?: return@forEachIndexed - val description = normalizeDescription(episode, content) - logger.config("$s : $description") + val title = normalizeTitle(episode.platform!!, content) + val description = normalizeDescription(episode.platform!!, content) + logger.config("$s : $title - $description") + episode.title = title episode.description = description + episode.lastUpdateDateTime = now episodeService.update(episode) } catch (e: Exception) { logger.log(Level.SEVERE, "Error while fetching episode description for $s", e) @@ -65,7 +82,7 @@ class FetchOldEpisodeDescriptionJob : AbstractJob() { httpRequest.close() } - fun normalizeUrl(platform: Platform, countryCode: CountryCode, url: String): String { + private fun normalizeUrl(platform: Platform, countryCode: CountryCode, url: String): String { return when (platform) { Platform.CRUN -> { val other = "https://www.crunchyroll.com/${countryCode.name.lowercase()}/" @@ -91,18 +108,28 @@ class FetchOldEpisodeDescriptionJob : AbstractJob() { else -> { val split = episode.url!!.split("/") - val animeNameEncoded = split[split.size - 2] - val animeId = AnimationDigitalNetworkWrapper.getShow(animeNameEncoded).getAsInt("id")!! val videoId = split[split.size - 1].split("-")[0].toInt() - AnimationDigitalNetworkWrapper.getShowVideos(animeId, videoId)[0] + AnimationDigitalNetworkWrapper.getShowVideo(videoId) } } } fun normalizeUrl(url: String) = "/watch/([A-Z0-9]+)".toRegex().find(url)!!.groupValues[1] - private fun normalizeDescription(episode: Episode, content: JsonObject): String? { - var description = when (episode.platform) { + private fun normalizeTitle(platform: Platform, content: JsonObject): String? { + var title = when (platform) { + Platform.CRUN -> content.getAsString("title") + else -> content.getAsString("name") + } + + title = title?.replace("\n", "") + title = title?.replace("\r", "") + title = title?.trim() + return title + } + + fun normalizeDescription(platform: Platform, content: JsonObject): String? { + var description = when (platform) { Platform.CRUN -> content.getAsString("description") else -> content.getAsString("summary") } diff --git a/src/main/kotlin/fr/shikkanime/jobs/FetchEpisodesJob.kt b/src/main/kotlin/fr/shikkanime/jobs/FetchEpisodesJob.kt index 9cdd4640..f39e6492 100644 --- a/src/main/kotlin/fr/shikkanime/jobs/FetchEpisodesJob.kt +++ b/src/main/kotlin/fr/shikkanime/jobs/FetchEpisodesJob.kt @@ -15,7 +15,7 @@ import java.time.ZonedDateTime import java.util.logging.Level -class FetchEpisodesJob : AbstractJob() { +class FetchEpisodesJob : AbstractJob { private val logger = LoggerFactory.getLogger(javaClass) private var isInitialized = false private var isRunning = false diff --git a/src/main/kotlin/fr/shikkanime/jobs/GarbageCollectorJob.kt b/src/main/kotlin/fr/shikkanime/jobs/GarbageCollectorJob.kt index 85d8c0ae..b232ab66 100644 --- a/src/main/kotlin/fr/shikkanime/jobs/GarbageCollectorJob.kt +++ b/src/main/kotlin/fr/shikkanime/jobs/GarbageCollectorJob.kt @@ -1,6 +1,6 @@ package fr.shikkanime.jobs -class GarbageCollectorJob : AbstractJob() { +class GarbageCollectorJob : AbstractJob { override fun run() { System.gc() } diff --git a/src/main/kotlin/fr/shikkanime/jobs/MetricJob.kt b/src/main/kotlin/fr/shikkanime/jobs/MetricJob.kt index e09aa913..4390aad9 100644 --- a/src/main/kotlin/fr/shikkanime/jobs/MetricJob.kt +++ b/src/main/kotlin/fr/shikkanime/jobs/MetricJob.kt @@ -7,7 +7,7 @@ import java.lang.management.ManagementFactory import javax.management.Attribute import javax.management.ObjectName -class MetricJob : AbstractJob() { +class MetricJob : AbstractJob { @Inject private lateinit var metricService: MetricService diff --git a/src/main/kotlin/fr/shikkanime/jobs/SavingImageCacheJob.kt b/src/main/kotlin/fr/shikkanime/jobs/SavingImageCacheJob.kt index 829e7cab..649b14f1 100644 --- a/src/main/kotlin/fr/shikkanime/jobs/SavingImageCacheJob.kt +++ b/src/main/kotlin/fr/shikkanime/jobs/SavingImageCacheJob.kt @@ -2,7 +2,7 @@ package fr.shikkanime.jobs import fr.shikkanime.services.ImageService -class SavingImageCacheJob : AbstractJob() { +class SavingImageCacheJob : AbstractJob { override fun run() { ImageService.saveCache() } diff --git a/src/main/kotlin/fr/shikkanime/platforms/AbstractPlatform.kt b/src/main/kotlin/fr/shikkanime/platforms/AbstractPlatform.kt index 41c13f5c..85365965 100644 --- a/src/main/kotlin/fr/shikkanime/platforms/AbstractPlatform.kt +++ b/src/main/kotlin/fr/shikkanime/platforms/AbstractPlatform.kt @@ -9,7 +9,6 @@ import fr.shikkanime.utils.ObjectParser import fr.shikkanime.utils.isEqualOrAfter import kotlinx.coroutines.runBlocking import java.io.File -import java.lang.reflect.ParameterizedType import java.time.ZonedDateTime import java.util.logging.Level @@ -20,6 +19,7 @@ abstract class AbstractPlatform, K : Any, V> { private var apiCache = mutableMapOf, V>() abstract fun getPlatform(): Platform + abstract fun getConfigurationClass(): Class abstract suspend fun fetchApiContent(key: K, zonedDateTime: ZonedDateTime): V abstract fun fetchEpisodes(zonedDateTime: ZonedDateTime, bypassFileContent: File? = null): List @@ -68,11 +68,4 @@ abstract class AbstractPlatform, K : Any, V> { if (!folder.exists()) folder.mkdirs() return File(folder, "${getPlatform().platformName.lowercase().replace(" ", "-")}.json") } - - private fun getConfigurationClass(): Class { - val type = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] - require(type is Class<*>) { "Configuration class must be a class" } - @Suppress("UNCHECKED_CAST") - return type as Class - } } diff --git a/src/main/kotlin/fr/shikkanime/platforms/AnimationDigitalNetworkPlatform.kt b/src/main/kotlin/fr/shikkanime/platforms/AnimationDigitalNetworkPlatform.kt index a63bb18a..034628ee 100644 --- a/src/main/kotlin/fr/shikkanime/platforms/AnimationDigitalNetworkPlatform.kt +++ b/src/main/kotlin/fr/shikkanime/platforms/AnimationDigitalNetworkPlatform.kt @@ -25,6 +25,8 @@ class AnimationDigitalNetworkPlatform : AbstractPlatform>() { override fun getPlatform(): Platform = Platform.ANIM + override fun getConfigurationClass() = AnimationDigitalNetworkConfiguration::class.java + override suspend fun fetchApiContent(key: CountryCode, zonedDateTime: ZonedDateTime): List { return AnimationDigitalNetworkWrapper.getLatestVideos(zonedDateTime.toLocalDate()) } diff --git a/src/main/kotlin/fr/shikkanime/platforms/CrunchyrollPlatform.kt b/src/main/kotlin/fr/shikkanime/platforms/CrunchyrollPlatform.kt index 6338444b..3545d497 100644 --- a/src/main/kotlin/fr/shikkanime/platforms/CrunchyrollPlatform.kt +++ b/src/main/kotlin/fr/shikkanime/platforms/CrunchyrollPlatform.kt @@ -12,6 +12,7 @@ import fr.shikkanime.services.caches.ConfigCacheService import fr.shikkanime.utils.HttpRequest import fr.shikkanime.utils.MapCache import fr.shikkanime.utils.ObjectParser +import fr.shikkanime.utils.ObjectParser.getAsBoolean import fr.shikkanime.utils.ObjectParser.getAsInt import fr.shikkanime.utils.ObjectParser.getAsLong import fr.shikkanime.utils.ObjectParser.getAsString @@ -33,6 +34,7 @@ class CrunchyrollPlatform : AbstractPlatform try { @@ -168,7 +172,7 @@ class CrunchyrollPlatform : AbstractPlatform { var bodyAsText = content bodyAsText = bodyAsText.replace(System.lineSeparator(), "").replace("\n", "") @@ -362,7 +368,6 @@ class CrunchyrollPlatform : AbstractPlatform() { override fun getPlatform(): Platform = Platform.DISN + override fun getConfigurationClass() = DisneyPlusConfiguration::class.java + override suspend fun fetchApiContent( key: CountryCodeDisneyPlusSimulcastKeyCache, zonedDateTime: ZonedDateTime diff --git a/src/main/kotlin/fr/shikkanime/platforms/NetflixPlatform.kt b/src/main/kotlin/fr/shikkanime/platforms/NetflixPlatform.kt index 4a6aea0f..a658809c 100644 --- a/src/main/kotlin/fr/shikkanime/platforms/NetflixPlatform.kt +++ b/src/main/kotlin/fr/shikkanime/platforms/NetflixPlatform.kt @@ -16,6 +16,8 @@ import java.time.format.DateTimeFormatter class NetflixPlatform : AbstractPlatform>() { override fun getPlatform(): Platform = Platform.NETF + override fun getConfigurationClass() = NetflixConfiguration::class.java + override suspend fun fetchApiContent( key: CountryCodeNetflixSimulcastKeyCache, zonedDateTime: ZonedDateTime diff --git a/src/main/kotlin/fr/shikkanime/platforms/configuration/AnimationDigitalNetworkConfiguration.kt b/src/main/kotlin/fr/shikkanime/platforms/configuration/AnimationDigitalNetworkConfiguration.kt index 3e32b51b..c334b535 100644 --- a/src/main/kotlin/fr/shikkanime/platforms/configuration/AnimationDigitalNetworkConfiguration.kt +++ b/src/main/kotlin/fr/shikkanime/platforms/configuration/AnimationDigitalNetworkConfiguration.kt @@ -1,3 +1,5 @@ package fr.shikkanime.platforms.configuration -class AnimationDigitalNetworkConfiguration : PlatformConfiguration() \ No newline at end of file +class AnimationDigitalNetworkConfiguration : PlatformConfiguration() { + override fun newPlatformSimulcast() = PlatformSimulcast() +} \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/platforms/configuration/CrunchyrollConfiguration.kt b/src/main/kotlin/fr/shikkanime/platforms/configuration/CrunchyrollConfiguration.kt index e05a1e8f..a434442c 100644 --- a/src/main/kotlin/fr/shikkanime/platforms/configuration/CrunchyrollConfiguration.kt +++ b/src/main/kotlin/fr/shikkanime/platforms/configuration/CrunchyrollConfiguration.kt @@ -5,6 +5,8 @@ import io.ktor.http.* data class CrunchyrollConfiguration( var simulcastCheckDelayInMinutes: Long = 60, ) : PlatformConfiguration() { + override fun newPlatformSimulcast() = PlatformSimulcast() + override fun of(parameters: Parameters) { super.of(parameters) parameters["simulcastCheckDelayInMinutes"]?.let { simulcastCheckDelayInMinutes = it.toLong() } diff --git a/src/main/kotlin/fr/shikkanime/platforms/configuration/DisneyPlusConfiguration.kt b/src/main/kotlin/fr/shikkanime/platforms/configuration/DisneyPlusConfiguration.kt index c0c591f6..3cf4d7b7 100644 --- a/src/main/kotlin/fr/shikkanime/platforms/configuration/DisneyPlusConfiguration.kt +++ b/src/main/kotlin/fr/shikkanime/platforms/configuration/DisneyPlusConfiguration.kt @@ -38,6 +38,8 @@ data class DisneyPlusConfiguration( } } + override fun newPlatformSimulcast() = DisneyPlusSimulcast() + override fun of(parameters: Parameters) { super.of(parameters) parameters["authorization"]?.let { authorization = it } diff --git a/src/main/kotlin/fr/shikkanime/platforms/configuration/NetflixConfiguration.kt b/src/main/kotlin/fr/shikkanime/platforms/configuration/NetflixConfiguration.kt index 033c2d96..dcd5fb9c 100644 --- a/src/main/kotlin/fr/shikkanime/platforms/configuration/NetflixConfiguration.kt +++ b/src/main/kotlin/fr/shikkanime/platforms/configuration/NetflixConfiguration.kt @@ -54,4 +54,6 @@ class NetflixConfiguration : PlatformConfiguration( +abstract class PlatformConfiguration( var availableCountries: MutableSet = mutableSetOf(), var apiCheckDelayInMinutes: Long = 0, val simulcasts: MutableSet = mutableSetOf(), val blacklistedSimulcasts: MutableSet = mutableSetOf(), ) { - fun newPlatformSimulcast(): S { - @Suppress("UNCHECKED_CAST") - return ((javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class).getConstructor() - .newInstance() - } + abstract fun newPlatformSimulcast(): S - fun addPlatformSimulcast(simulcast: PlatformSimulcast) { - @Suppress("UNCHECKED_CAST") - simulcasts.add(simulcast as S) - } + @Suppress("UNCHECKED_CAST") + fun addPlatformSimulcast(simulcast: PlatformSimulcast) = simulcasts.add(simulcast as S) open fun of(parameters: Parameters) { parameters["availableCountries"]?.let { - availableCountries = if (it.isNotBlank()) - CountryCode.from(it.split(",")) as MutableSet - else - mutableSetOf() + availableCountries = if (it.isNotBlank()) CountryCode.from(it.split(",")).toMutableSet() else mutableSetOf() } - parameters["apiCheckDelayInMinutes"]?.let { apiCheckDelayInMinutes = it.toLong() } - parameters["blacklistedSimulcasts"]?.let { blacklistedSimulcasts.clear() blacklistedSimulcasts.addAll(it.split("||")) @@ -68,24 +56,24 @@ open class PlatformConfiguration( open fun toConfigurationFields() = mutableSetOf( ConfigurationField( - label = "Available countries", + "Available countries", name = "availableCountries", type = "text", value = availableCountries.joinToString(",") ), ConfigurationField( - label = "API check delay", - caption = "In minutes", - name = "apiCheckDelayInMinutes", - type = "number", - value = apiCheckDelayInMinutes.toString() + "API check delay", + "In minutes", + "apiCheckDelayInMinutes", + "number", + apiCheckDelayInMinutes.toString() ), ConfigurationField( - label = "Blacklisted simulcasts", - caption = "Separate with ||", - name = "blacklistedSimulcasts", - type = "textarea", - value = blacklistedSimulcasts.joinToString("||") - ), + "Blacklisted simulcasts", + "Separate with ||", + "blacklistedSimulcasts", + "textarea", + blacklistedSimulcasts.joinToString("||") + ) ) } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/plugins/HTTP.kt b/src/main/kotlin/fr/shikkanime/plugins/HTTP.kt index ced84551..f6885fb0 100644 --- a/src/main/kotlin/fr/shikkanime/plugins/HTTP.kt +++ b/src/main/kotlin/fr/shikkanime/plugins/HTTP.kt @@ -42,6 +42,10 @@ fun Application.configureHTTP() { return@status } + if (path.startsWith("/404")) { + return@status + } + if (!path.startsWith("/api") && !path.startsWith("/admin")) { call.respondRedirect("/404") } diff --git a/src/main/kotlin/fr/shikkanime/plugins/Routing.kt b/src/main/kotlin/fr/shikkanime/plugins/Routing.kt index 315bda9c..79d5d453 100644 --- a/src/main/kotlin/fr/shikkanime/plugins/Routing.kt +++ b/src/main/kotlin/fr/shikkanime/plugins/Routing.kt @@ -263,7 +263,7 @@ private suspend fun handleTemplateResponse(call: ApplicationCall, controller: An modelMap["description"] = configCacheService.getValueAsString(ConfigPropertyKey.SEO_DESCRIPTION) ?: "" configCacheService.getValueAsString(ConfigPropertyKey.GOOGLE_SITE_VERIFICATION_ID)?.let { modelMap["googleSiteVerification"] = it } - call.respond(FreeMarkerContent(map["template"] as String, modelMap, "", response.contentType)) + call.respond(response.status, FreeMarkerContent(map["template"] as String, modelMap, "", response.contentType)) } private fun replacePathWithParameters(path: String, parameters: Map>): String = diff --git a/src/main/kotlin/fr/shikkanime/repositories/AbstractRepository.kt b/src/main/kotlin/fr/shikkanime/repositories/AbstractRepository.kt index 611de1c5..d2a330f5 100644 --- a/src/main/kotlin/fr/shikkanime/repositories/AbstractRepository.kt +++ b/src/main/kotlin/fr/shikkanime/repositories/AbstractRepository.kt @@ -17,7 +17,6 @@ abstract class AbstractRepository { protected lateinit var database: Database protected fun getEntityClass(): Class { - @Suppress("UNCHECKED_CAST") return (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class } @@ -45,11 +44,7 @@ abstract class AbstractRepository { .setHint(AvailableHints.HINT_READ_ONLY, true) } - fun buildPageableQuery( - query: TypedQuery, - page: Int, - limit: Int - ): Pageable { + fun buildPageableQuery(query: TypedQuery, page: Int, limit: Int): Pageable { val scrollableResults = query.unwrap(Query::class.java) .setReadOnly(true) .setFetchSize(limit) @@ -58,24 +53,12 @@ abstract class AbstractRepository { val list = mutableListOf() var total = 0L - scrollableResults.use { - if (!it.first()) - return@use - - val result = it.scroll((limit * page) - limit) - - if (!result) - return@use - + if (scrollableResults.first() && scrollableResults.scroll((limit * page) - limit)) { for (i in 0 until limit) { - @Suppress("UNCHECKED_CAST") - list.add(it.get() as E) - - if (!it.next()) - break + list.add(scrollableResults.get() as E) + if (!scrollableResults.next()) break } - - total = if (it.last()) it.rowNumber + 1L else 0 + total = if (scrollableResults.last()) scrollableResults.rowNumber + 1L else 0 } return Pageable(list, page, limit, total) diff --git a/src/main/kotlin/fr/shikkanime/repositories/AnimeRepository.kt b/src/main/kotlin/fr/shikkanime/repositories/AnimeRepository.kt index 9cd0fda4..81ca23c2 100644 --- a/src/main/kotlin/fr/shikkanime/repositories/AnimeRepository.kt +++ b/src/main/kotlin/fr/shikkanime/repositories/AnimeRepository.kt @@ -107,7 +107,6 @@ class AnimeRepository : AbstractRepository() { fun findAllByName(name: String, countryCode: CountryCode?, page: Int, limit: Int): Pageable { val searchSession = Search.session(database.entityManager) - @Suppress("UNCHECKED_CAST") val searchResult = searchSession.search(Anime::class.java) .where { w -> findWhere(w, name, countryCode) } .fetch((limit * page) - limit, limit) as SearchResult diff --git a/src/main/kotlin/fr/shikkanime/repositories/EpisodeRepository.kt b/src/main/kotlin/fr/shikkanime/repositories/EpisodeRepository.kt index a65656ea..72e4d214 100644 --- a/src/main/kotlin/fr/shikkanime/repositories/EpisodeRepository.kt +++ b/src/main/kotlin/fr/shikkanime/repositories/EpisodeRepository.kt @@ -9,6 +9,7 @@ import fr.shikkanime.entities.enums.LangType import fr.shikkanime.entities.enums.Platform import jakarta.persistence.Tuple import org.hibernate.Hibernate +import java.time.ZonedDateTime import java.util.* class EpisodeRepository : AbstractRepository() { @@ -131,10 +132,15 @@ class EpisodeRepository : AbstractRepository() { } } - fun findAllByPlatform(platform: Platform): List { + fun findAllByPlatformDeprecatedEpisodes(platform: Platform, lastUpdateDateTime: ZonedDateTime): List { return inTransaction { - createReadOnlyQuery(it, "FROM Episode WHERE platform = :platform AND description IS NULL", getEntityClass()) + createReadOnlyQuery( + it, + "FROM Episode WHERE platform = :platform AND (lastUpdateDateTime < :lastUpdateDateTime OR lastUpdateDateTime IS NULL)", + getEntityClass() + ) .setParameter("platform", platform) + .setParameter("lastUpdateDateTime", lastUpdateDateTime) .resultList .initialize() } diff --git a/src/main/kotlin/fr/shikkanime/services/EpisodeService.kt b/src/main/kotlin/fr/shikkanime/services/EpisodeService.kt index f2ab165c..e160a8d2 100644 --- a/src/main/kotlin/fr/shikkanime/services/EpisodeService.kt +++ b/src/main/kotlin/fr/shikkanime/services/EpisodeService.kt @@ -45,7 +45,8 @@ class EpisodeService : AbstractService() { fun findAllUUIDAndImage() = episodeRepository.findAllUUIDAndImage() - fun findAllByPlatform(platform: Platform) = episodeRepository.findAllByPlatform(platform) + fun findAllByPlatformDeprecatedEpisodes(platform: Platform, lastUpdateDateTime: ZonedDateTime) = + episodeRepository.findAllByPlatformDeprecatedEpisodes(platform, lastUpdateDateTime) fun addImage(uuid: UUID, image: String) { ImageService.add(uuid, ImageService.Type.IMAGE, image, 640, 360) diff --git a/src/main/kotlin/fr/shikkanime/services/caches/AbstractCacheService.kt b/src/main/kotlin/fr/shikkanime/services/caches/AbstractCacheService.kt index 0e2fd168..d2b96400 100644 --- a/src/main/kotlin/fr/shikkanime/services/caches/AbstractCacheService.kt +++ b/src/main/kotlin/fr/shikkanime/services/caches/AbstractCacheService.kt @@ -1,3 +1,3 @@ package fr.shikkanime.services.caches -abstract class AbstractCacheService \ No newline at end of file +interface AbstractCacheService \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/services/caches/AnimeCacheService.kt b/src/main/kotlin/fr/shikkanime/services/caches/AnimeCacheService.kt index 51617387..aea01c58 100644 --- a/src/main/kotlin/fr/shikkanime/services/caches/AnimeCacheService.kt +++ b/src/main/kotlin/fr/shikkanime/services/caches/AnimeCacheService.kt @@ -14,7 +14,7 @@ import fr.shikkanime.services.AnimeService import fr.shikkanime.utils.MapCache import java.util.* -class AnimeCacheService : AbstractCacheService() { +class AnimeCacheService : AbstractCacheService { @Inject private lateinit var animeService: AnimeService diff --git a/src/main/kotlin/fr/shikkanime/services/caches/ConfigCacheService.kt b/src/main/kotlin/fr/shikkanime/services/caches/ConfigCacheService.kt index 40effe4e..14024375 100644 --- a/src/main/kotlin/fr/shikkanime/services/caches/ConfigCacheService.kt +++ b/src/main/kotlin/fr/shikkanime/services/caches/ConfigCacheService.kt @@ -6,7 +6,7 @@ import fr.shikkanime.entities.enums.ConfigPropertyKey import fr.shikkanime.services.ConfigService import fr.shikkanime.utils.MapCache -class ConfigCacheService : AbstractCacheService() { +class ConfigCacheService : AbstractCacheService { @Inject private lateinit var configService: ConfigService diff --git a/src/main/kotlin/fr/shikkanime/services/caches/EpisodeCacheService.kt b/src/main/kotlin/fr/shikkanime/services/caches/EpisodeCacheService.kt index 29dc4979..04a68267 100644 --- a/src/main/kotlin/fr/shikkanime/services/caches/EpisodeCacheService.kt +++ b/src/main/kotlin/fr/shikkanime/services/caches/EpisodeCacheService.kt @@ -11,7 +11,7 @@ import fr.shikkanime.services.EpisodeService import fr.shikkanime.utils.MapCache import java.util.* -class EpisodeCacheService : AbstractCacheService() { +class EpisodeCacheService : AbstractCacheService { @Inject private lateinit var episodeService: EpisodeService diff --git a/src/main/kotlin/fr/shikkanime/services/caches/MemberCacheService.kt b/src/main/kotlin/fr/shikkanime/services/caches/MemberCacheService.kt index aa8be806..e29ce4eb 100644 --- a/src/main/kotlin/fr/shikkanime/services/caches/MemberCacheService.kt +++ b/src/main/kotlin/fr/shikkanime/services/caches/MemberCacheService.kt @@ -6,7 +6,7 @@ import fr.shikkanime.services.MemberService import fr.shikkanime.utils.MapCache import java.util.* -class MemberCacheService : AbstractCacheService() { +class MemberCacheService : AbstractCacheService { @Inject private lateinit var memberService: MemberService diff --git a/src/main/kotlin/fr/shikkanime/services/caches/SimulcastCacheService.kt b/src/main/kotlin/fr/shikkanime/services/caches/SimulcastCacheService.kt index 868f39c6..283ddb27 100644 --- a/src/main/kotlin/fr/shikkanime/services/caches/SimulcastCacheService.kt +++ b/src/main/kotlin/fr/shikkanime/services/caches/SimulcastCacheService.kt @@ -8,7 +8,7 @@ import fr.shikkanime.entities.Simulcast import fr.shikkanime.services.SimulcastService import fr.shikkanime.utils.MapCache -class SimulcastCacheService : AbstractCacheService() { +class SimulcastCacheService : AbstractCacheService { @Inject private lateinit var simulcastService: SimulcastService diff --git a/src/main/kotlin/fr/shikkanime/utils/MapCache.kt b/src/main/kotlin/fr/shikkanime/utils/MapCache.kt index b6aa392d..b426dce7 100644 --- a/src/main/kotlin/fr/shikkanime/utils/MapCache.kt +++ b/src/main/kotlin/fr/shikkanime/utils/MapCache.kt @@ -5,7 +5,7 @@ import com.google.common.cache.CacheLoader import com.google.common.cache.LoadingCache import java.time.Duration -class MapCache( +class MapCache( private var duration: Duration? = null, private val classes: List> = listOf(), private val block: (K) -> V, @@ -25,10 +25,10 @@ class MapCache( } cache = builder - .build(object : CacheLoader() { - override fun load(key: K): V { + .build(object : CacheLoader() { + override fun load(key: K): V & Any { logger.info("Loading $key") - return block(key) + return block(key)!! } }) } diff --git a/src/main/kotlin/fr/shikkanime/utils/routes/Response.kt b/src/main/kotlin/fr/shikkanime/utils/routes/Response.kt index 04a2f213..fc1554e8 100644 --- a/src/main/kotlin/fr/shikkanime/utils/routes/Response.kt +++ b/src/main/kotlin/fr/shikkanime/utils/routes/Response.kt @@ -61,19 +61,28 @@ open class Response( ) fun template( + code: HttpStatusCode, template: String, title: String? = null, model: Map = mapOf(), session: TokenDto? = null, contentType: ContentType = ContentType.Text.Html.withCharset(Charsets.UTF_8), ): Response = Response( - HttpStatusCode.OK, + code, type = ResponseType.TEMPLATE, data = mapOf("template" to template, "title" to title, "model" to model), session = session, contentType = contentType ) + fun template( + template: String, + title: String? = null, + model: Map = mapOf(), + session: TokenDto? = null, + contentType: ContentType = ContentType.Text.Html.withCharset(Charsets.UTF_8), + ) = template(HttpStatusCode.OK, template, title, model, session, contentType) + fun template( link: Link, model: Map = mapOf(), diff --git a/src/main/kotlin/fr/shikkanime/wrappers/AnimationDigitalNetworkWrapper.kt b/src/main/kotlin/fr/shikkanime/wrappers/AnimationDigitalNetworkWrapper.kt index c2aca575..56cf1454 100644 --- a/src/main/kotlin/fr/shikkanime/wrappers/AnimationDigitalNetworkWrapper.kt +++ b/src/main/kotlin/fr/shikkanime/wrappers/AnimationDigitalNetworkWrapper.kt @@ -31,14 +31,14 @@ object AnimationDigitalNetworkWrapper { ?: throw Exception("Failed to get show id") } - suspend fun getShowVideos(animeId: Int, videoId: Int, limit: Int = 1): List { - val response = HttpRequest().get("${BASE_URL}video/show/$animeId?videoId=$videoId&limit=$limit") + suspend fun getShowVideo(videoId: Int): JsonObject { + val response = HttpRequest().get("${BASE_URL}video/$videoId/public?withMicrodata=true&withSeo=true") if (response.status.value != 200) { - throw Exception("Failed to get videos") + throw Exception("Failed to get video") } - return ObjectParser.fromJson(response.bodyAsText()).getAsJsonArray("videos")?.map { it.asJsonObject } - ?: throw Exception("Failed to get videos") + return ObjectParser.fromJson(response.bodyAsText()).getAsJsonObject("video") + ?: throw Exception("Failed to get video") } } \ No newline at end of file diff --git a/src/main/resources/db/changelog/2024/02/07-changelog.xml b/src/main/resources/db/changelog/2024/02/07-changelog.xml new file mode 100644 index 00000000..35c94c72 --- /dev/null +++ b/src/main/resources/db/changelog/2024/02/07-changelog.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + SELECT COUNT(*) + FROM config + WHERE property_key = 'fetch_deprecated_episode_date' + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.xml b/src/main/resources/db/changelog/db.changelog-master.xml index 0a0f8f26..daab7708 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -24,4 +24,5 @@ + \ No newline at end of file diff --git a/src/test/kotlin/fr/shikkanime/OldEpisodeScraper.kt b/src/test/kotlin/fr/shikkanime/OldEpisodeScraper.kt index f5cd4856..cdee0eb3 100644 --- a/src/test/kotlin/fr/shikkanime/OldEpisodeScraper.kt +++ b/src/test/kotlin/fr/shikkanime/OldEpisodeScraper.kt @@ -107,7 +107,7 @@ fun main() { val ids = series.map { jsonObject -> jsonObject.getAsString("id")!! }.toSet() println("Simulcasts: $titles") - crunchyrollPlatform.simulcasts[CountryCode.FR] = titles + crunchyrollPlatform.simulcasts.set(CountryCode.FR, titles) series.forEach { val postersTall = it.getAsJsonObject("images").getAsJsonArray("poster_tall")[0].asJsonArray @@ -116,7 +116,10 @@ fun main() { val banner = postersWide?.maxByOrNull { poster -> poster.asJsonObject.getAsInt("width")!! }?.asJsonObject?.getAsString("source")!! val description = it.getAsString("description") - crunchyrollPlatform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, it.getAsString("id")!!)] = CrunchyrollPlatform.CrunchyrollAnimeContent(image = image, banner = banner, description = description,) + crunchyrollPlatform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, it.getAsString("id")!!), + CrunchyrollPlatform.CrunchyrollAnimeContent(image = image, banner = banner, description = description) + ) } val episodeIds = ids.parallelStream().map { seriesId -> diff --git a/src/test/kotlin/fr/shikkanime/jobs/FetchDeprecatedEpisodeJobTest.kt b/src/test/kotlin/fr/shikkanime/jobs/FetchDeprecatedEpisodeJobTest.kt new file mode 100644 index 00000000..8c7a4825 --- /dev/null +++ b/src/test/kotlin/fr/shikkanime/jobs/FetchDeprecatedEpisodeJobTest.kt @@ -0,0 +1,62 @@ +package fr.shikkanime.jobs + +import fr.shikkanime.entities.enums.Platform +import fr.shikkanime.utils.HttpRequest +import fr.shikkanime.wrappers.AnimationDigitalNetworkWrapper +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class FetchDeprecatedEpisodeJobTest { + private val fetchDeprecatedEpisodeJob = FetchDeprecatedEpisodeJob() + + @Test + fun normalizeUrl() { + assertEquals( + "GMKUXPD53", + fetchDeprecatedEpisodeJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/GMKUXPD53/") + ) + assertEquals( + "G14U415N4", + fetchDeprecatedEpisodeJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/G14U415N4/the-panicked-foolish-angel-and-demon") + ) + assertEquals( + "G14U415D2", + fetchDeprecatedEpisodeJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/G14U415D2/natsukawa-senpai-is-super-good-looking") + ) + assertEquals( + "G8WUN158J", + fetchDeprecatedEpisodeJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/G8WUN158J/") + ) + assertEquals( + "GEVUZD021", + fetchDeprecatedEpisodeJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/GEVUZD021/becoming-a-three-star-chef") + ) + assertEquals( + "GK9U3KWN4", + fetchDeprecatedEpisodeJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/GK9U3KWN4/yukis-world") + ) + } + + @Test + fun bug() { + val normalizeUrl = "https://www.crunchyroll.com/fr/media-918855" + + val lastPage = HttpRequest().use { + it.getBrowser(normalizeUrl) + it.lastPageUrl!! + } + + val id = fetchDeprecatedEpisodeJob.normalizeUrl(lastPage) + assertEquals("GVWU07GP0", id) + } + + @Test + fun normalizeDescription() { + val content = runBlocking { AnimationDigitalNetworkWrapper.getShowVideo(24108) } + assertEquals( + "Alors qu'ils sont en route pour leur voyage scolaire, une classe de lycéens est invoquée dans un autre monde afin de participer à une lutte sanguinaire pour devenir des « sages », la classe dirigeante de ce nouveau monde. Chaque élève se voit attribuer un pouvoir, sauf Yogiri et Tomochika, qui ont été laissés pour mort par leurs camarades…", + fetchDeprecatedEpisodeJob.normalizeDescription(Platform.ANIM, content) + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/fr/shikkanime/jobs/FetchOldEpisodeDescriptionJobTest.kt b/src/test/kotlin/fr/shikkanime/jobs/FetchOldEpisodeDescriptionJobTest.kt deleted file mode 100644 index 732c9142..00000000 --- a/src/test/kotlin/fr/shikkanime/jobs/FetchOldEpisodeDescriptionJobTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -package fr.shikkanime.jobs - -import fr.shikkanime.utils.HttpRequest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class FetchOldEpisodeDescriptionJobTest { - private val fetchOldEpisodeDescriptionJob = FetchOldEpisodeDescriptionJob() - - @Test - fun normalizeUrl() { - assertEquals("GMKUXPD53", fetchOldEpisodeDescriptionJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/GMKUXPD53/")) - assertEquals("G14U415N4", fetchOldEpisodeDescriptionJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/G14U415N4/the-panicked-foolish-angel-and-demon")) - assertEquals("G14U415D2", fetchOldEpisodeDescriptionJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/G14U415D2/natsukawa-senpai-is-super-good-looking")) - assertEquals("G8WUN158J", fetchOldEpisodeDescriptionJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/G8WUN158J/")) - assertEquals("GEVUZD021", fetchOldEpisodeDescriptionJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/GEVUZD021/becoming-a-three-star-chef")) - assertEquals("GK9U3KWN4", fetchOldEpisodeDescriptionJob.normalizeUrl("https://www.crunchyroll.com/fr/watch/GK9U3KWN4/yukis-world")) - } - - @Test - fun bug() { - val normalizeUrl = "https://www.crunchyroll.com/fr/media-918855" - - val lastPage = HttpRequest().use { - it.getBrowser(normalizeUrl) - it.lastPageUrl!! - } - - val id = fetchOldEpisodeDescriptionJob.normalizeUrl(lastPage) - assertEquals("GVWU07GP0", id) - } -} \ No newline at end of file diff --git a/src/test/kotlin/fr/shikkanime/platforms/CrunchyrollPlatformTest.kt b/src/test/kotlin/fr/shikkanime/platforms/CrunchyrollPlatformTest.kt index 02e41331..6b69658c 100644 --- a/src/test/kotlin/fr/shikkanime/platforms/CrunchyrollPlatformTest.kt +++ b/src/test/kotlin/fr/shikkanime/platforms/CrunchyrollPlatformTest.kt @@ -43,144 +43,178 @@ class CrunchyrollPlatformTest { val s = "2023-12-11T18:00:00Z" val zonedDateTime = ZonedDateTime.parse(s) - platform.simulcasts[CountryCode.FR] = setOf( - "16bit sensation: another layer", - "a girl & her guard dog", - "a playthrough of a certain dude's vrmmo life", - "a returner's magic should be special", - "after-school hanako-kun", - "all saints street", - "arknights", - "berserk of gluttony", - "bullbuster", - "butareba -the story of a man turned into a pig-", - "captain tsubasa saison 2, junior youth arc", - "dead mount death play", - "dr. stone", - "firefighter daigo: rescuer in orange", - "frieren", - "girlfriend, girlfriend", - "heaven official's blessing", - "hypnosismic -division rap battle- rhyme anima", - "i'm giving the disgraced noble lady i rescued a crash course in naughtiness", - "i'm in love with the villainess", - "idolish7", - "je survivrai grâce aux potions !", - "jujutsu kaisen", - "kamierabi god.app", - "kawagoe boys sing -now or never-", - "kenshin le vagabond (2023)", - "kizuna no allele", - "l'attaque des titans", - "la valkyrie aux cheveux de jais", - "les 100 petites amies qui t'aiiiment à en mourir", - "les carnets de l'apothicaire", - "les quatre frères yuzuki", - "let me check the walkthrough first", - "mf ghost", - "migi&dali", - "moi, quand je me réincarne en slime", - "my new boss is goofy", - "one piece", - "our dating story: the experienced you and the inexperienced me", - "overtake!", - "paradox live the animation", - "power of hope ~precure full bloom~", - "reign of the seven spellblades", - "ron kamonohashi: deranged detective", - "sasaki and miyano", - "shadowverse", - "shangri-la frontier", - "shy", - "spy x family", - "stardust telepath", - "tearmoon empire", - "tenchi muyo! gxp paradise starting", - "the ancient magus bride", - "the faraway paladin", - "the idolm@ster million live!", - "the kingdoms of ruin", - "the rising of the shield hero", - "the saint's magic power is omnipotent", - "umamusume: pretty derby", - "under ninja", - "witch family!", - "am i actually the strongest?", - "atelier ryza: ever darkness & the secret hideout the animation", - "ayaka", - "ayakashi triangle", - "bang dream! it's mygo!!!!!", - "bungo stray dogs", - "cardfight!! vanguard overdress", - "classroom for heroes", - "fate/strange fake -whispers of dawn-", - "horimiya", - "la princesse & la bête", - "liar, liar", - "link click", - "malevolent spirits: mononogatari", - "masamune-kun's revenge", - "mon chat à tout faire est encore tout déprimé", - "mushoku tensei: jobless reincarnation", - "my tiny senpai", - "my unique skill makes me op even at level 1", - "nier:automata ver1.1a", - "reborn as a vending machine, i now wander the dungeon", - "rent-a-girlfriend", - "sainte cecilia et le pasteur lawrence", - "sugar apple fairy tale", - "sweet reincarnation", - "sword art online -fulldive-", - "tenpuru : on ne vit pas de solitude et d'eau fraîche", - "the devil is a part-timer!", - "the duke of death and his maid", - "the gene of ai", - "the girl i like forgot her glasses", - "the great cleric", - "the misfit of demon king academy", - "theatre of darkness: yamishibai", - "undead murder farce", - "zom 100: bucket list of the dead" - ) - - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "im-in-love-with-the-villainess")] = + platform.simulcasts.set( + CountryCode.FR, setOf( + "16bit sensation: another layer", + "a girl & her guard dog", + "a playthrough of a certain dude's vrmmo life", + "a returner's magic should be special", + "after-school hanako-kun", + "all saints street", + "arknights", + "berserk of gluttony", + "bullbuster", + "butareba -the story of a man turned into a pig-", + "captain tsubasa saison 2, junior youth arc", + "dead mount death play", + "dr. stone", + "firefighter daigo: rescuer in orange", + "frieren", + "girlfriend, girlfriend", + "heaven official's blessing", + "hypnosismic -division rap battle- rhyme anima", + "i'm giving the disgraced noble lady i rescued a crash course in naughtiness", + "i'm in love with the villainess", + "idolish7", + "je survivrai grâce aux potions !", + "jujutsu kaisen", + "kamierabi god.app", + "kawagoe boys sing -now or never-", + "kenshin le vagabond (2023)", + "kizuna no allele", + "l'attaque des titans", + "la valkyrie aux cheveux de jais", + "les 100 petites amies qui t'aiiiment à en mourir", + "les carnets de l'apothicaire", + "les quatre frères yuzuki", + "let me check the walkthrough first", + "mf ghost", + "migi&dali", + "moi, quand je me réincarne en slime", + "my new boss is goofy", + "one piece", + "our dating story: the experienced you and the inexperienced me", + "overtake!", + "paradox live the animation", + "power of hope ~precure full bloom~", + "reign of the seven spellblades", + "ron kamonohashi: deranged detective", + "sasaki and miyano", + "shadowverse", + "shangri-la frontier", + "shy", + "spy x family", + "stardust telepath", + "tearmoon empire", + "tenchi muyo! gxp paradise starting", + "the ancient magus bride", + "the faraway paladin", + "the idolm@ster million live!", + "the kingdoms of ruin", + "the rising of the shield hero", + "the saint's magic power is omnipotent", + "umamusume: pretty derby", + "under ninja", + "witch family!", + "am i actually the strongest?", + "atelier ryza: ever darkness & the secret hideout the animation", + "ayaka", + "ayakashi triangle", + "bang dream! it's mygo!!!!!", + "bungo stray dogs", + "cardfight!! vanguard overdress", + "classroom for heroes", + "fate/strange fake -whispers of dawn-", + "horimiya", + "la princesse & la bête", + "liar, liar", + "link click", + "malevolent spirits: mononogatari", + "masamune-kun's revenge", + "mon chat à tout faire est encore tout déprimé", + "mushoku tensei: jobless reincarnation", + "my tiny senpai", + "my unique skill makes me op even at level 1", + "nier:automata ver1.1a", + "reborn as a vending machine, i now wander the dungeon", + "rent-a-girlfriend", + "sainte cecilia et le pasteur lawrence", + "sugar apple fairy tale", + "sweet reincarnation", + "sword art online -fulldive-", + "tenpuru : on ne vit pas de solitude et d'eau fraîche", + "the devil is a part-timer!", + "the duke of death and his maid", + "the gene of ai", + "the girl i like forgot her glasses", + "the great cleric", + "the misfit of demon king academy", + "theatre of darkness: yamishibai", + "undead murder farce", + "zom 100: bucket list of the dead" + ) + ) + + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "im-in-love-with-the-villainess"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/3f84cdad5e39d6d35ffc2b127c0509c7.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "shy")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "shy"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/d117d6da529adb8ee4d972af79024365.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "dead-mount-death-play")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "dead-mount-death-play"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/3c9bebb79d5abf889928349df37a42b0.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "kawagoe-boys-sing-now-or-never-")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "kawagoe-boys-sing-now-or-never-"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/cfdf23b73e919c35c4c6168d0939907a.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "ron-kamonohashis-forbidden-deductions")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "ron-kamonohashis-forbidden-deductions"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/a755fe3f774aedeb7115a6d0419c5fe7.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "stardust-telepath")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "stardust-telepath"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/9b1a36e0ebf66dc87f4fa8f623bf1ec5.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "shangri-la-frontier")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "shangri-la-frontier"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/a2f948157077e3d65471329d9dd43be1.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "mf-ghost")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "mf-ghost"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/00c8ed6414e0ef37dbacf36bec86565a.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "berserk-of-gluttony")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "berserk-of-gluttony"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/a5ebfaaf02d69d1c0b254f4a49c43082.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "overtake")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "overtake"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/150047844fbb4c07a3c57b5def7b42b0.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache( - CountryCode.FR, - "the-family-circumstances-of-the-irregular-witch" - )] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache( + CountryCode.FR, + "the-family-circumstances-of-the-irregular-witch" + ), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/43fce24dc917ee4e415aeae5231be54e.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache( - CountryCode.FR, - "the-100-girlfriends-who-really-really-really-really-really-love-you" - )] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache( + CountryCode.FR, + "the-100-girlfriends-who-really-really-really-really-really-love-you" + ), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/0ab1a205c9c541ad87cefdbe7fd23390.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "captain-tsubasa-junior-youth-arc")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "captain-tsubasa-junior-youth-arc"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/f0dcc54fec6d201f5a2c7abd01d56e09.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "the-idolmster-million-live")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "the-idolmster-million-live"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/550acd8c749fa3820d4f75868d769e71.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "one-piece")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "one-piece"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/f154230aab3191aba977f337d392f812.jpe") - platform.animeInfoCache[CountryCodeAnimeIdKeyCache(CountryCode.FR, "the-apothecary-diaries")] = + ) + platform.animeInfoCache.set( + CountryCodeAnimeIdKeyCache(CountryCode.FR, "the-apothecary-diaries"), CrunchyrollPlatform.CrunchyrollAnimeContent("https://www.crunchyroll.com/imgsrv/display/thumbnail/480x720/catalog/crunchyroll/af8f1de4c1b2d5345294490a45fcb22d.jpe") + ) val episodes = platform.fetchEpisodes( zonedDateTime, @@ -210,7 +244,7 @@ class CrunchyrollPlatformTest { val s = "2024-01-24T18:45:00Z" val zonedDateTime = ZonedDateTime.parse(s) - platform.simulcasts[CountryCode.FR] = setOf("metallic rouge") + platform.simulcasts.set(CountryCode.FR, setOf("metallic rouge")) val episodes = platform.fetchEpisodes( zonedDateTime,