diff --git a/src/main/kotlin/fr/shikkanime/Application.kt b/src/main/kotlin/fr/shikkanime/Application.kt index dc9d4b71..199a3029 100644 --- a/src/main/kotlin/fr/shikkanime/Application.kt +++ b/src/main/kotlin/fr/shikkanime/Application.kt @@ -47,8 +47,6 @@ fun main() { JobManager.scheduleJob("0 0 0 * * ?", DeleteOldMetricsJob::class.java) // Every day at 3pm JobManager.scheduleJob("0 0 15 * * ?", FetchOldEpisodesJob::class.java) - // Every day at 9am - JobManager.scheduleJob("0 0 9 * * ?", FetchCalendarJob::class.java) JobManager.start() Constant.injector.getInstance(DiscordSocialNetwork::class.java).login() diff --git a/src/main/kotlin/fr/shikkanime/entities/enums/ConfigPropertyKey.kt b/src/main/kotlin/fr/shikkanime/entities/enums/ConfigPropertyKey.kt index 4f7d57e5..20eb637d 100644 --- a/src/main/kotlin/fr/shikkanime/entities/enums/ConfigPropertyKey.kt +++ b/src/main/kotlin/fr/shikkanime/entities/enums/ConfigPropertyKey.kt @@ -27,7 +27,6 @@ enum class ConfigPropertyKey(val key: String) { ANIME_EPISODES_SIZE_LIMIT("anime_episodes_size_limit"), DISNEY_PLUS_AUTHORIZATION("disney_plus_authorization"), DISNEY_PLUS_REFRESH_TOKEN("disney_plus_refresh_token"), - TRANSLATE_CALENDAR("translate_calendar"), LAST_FETCH_OLD_EPISODES("last_fetch_old_episodes"), FETCH_OLD_EPISODES_RANGE("fetch_old_episodes_range"), FETCH_OLD_EPISODES_LIMIT("fetch_old_episodes_limit"), diff --git a/src/main/kotlin/fr/shikkanime/jobs/FetchCalendarJob.kt b/src/main/kotlin/fr/shikkanime/jobs/FetchCalendarJob.kt deleted file mode 100644 index e9022824..00000000 --- a/src/main/kotlin/fr/shikkanime/jobs/FetchCalendarJob.kt +++ /dev/null @@ -1,308 +0,0 @@ -package fr.shikkanime.jobs - -import com.google.inject.Inject -import fr.shikkanime.dtos.CalendarEpisodeDto -import fr.shikkanime.entities.enums.ConfigPropertyKey -import fr.shikkanime.entities.enums.Platform -import fr.shikkanime.services.ImageService -import fr.shikkanime.services.ImageService.drawStringRect -import fr.shikkanime.services.ImageService.setRenderingHints -import fr.shikkanime.services.caches.ConfigCacheService -import fr.shikkanime.services.caches.LanguageCacheService -import fr.shikkanime.utils.* -import fr.shikkanime.utils.StringUtils.capitalizeWords -import io.ktor.client.statement.* -import io.ktor.http.* -import kotlinx.coroutines.runBlocking -import org.jsoup.Jsoup -import org.jsoup.nodes.Element -import java.awt.Color -import java.awt.Font -import java.awt.Graphics2D -import java.awt.image.BufferedImage -import java.io.ByteArrayOutputStream -import java.io.File -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.util.* -import java.util.logging.Level -import javax.imageio.ImageIO - -class FetchCalendarJob : AbstractJob { - private val logger = LoggerFactory.getLogger(javaClass) - - @Inject - private lateinit var languageCacheService: LanguageCacheService - - @Inject - private lateinit var configCacheService: ConfigCacheService - - override fun run() { - runBlocking { - HttpRequest().use { httpRequest -> - val response = httpRequest.get("https://anime.icotaku.com/calendrier_diffusion.html") - - if (response.status != HttpStatusCode.OK) { - logger.log(Level.SEVERE, "Error: ${response.status}") - return@use - } - - val body = Jsoup.parse(response.bodyAsText()) - val todayElement = requireNotNull(body.select(".calendrier_diffusion")[0]) { "Today not found" } - val elements = todayElement.select("tr").toMutableList() - elements.removeAt(0) - - val episodes = getEpisodes(elements, httpRequest) - - val backgroundImage = getBackgroundImage() ?: return@use - val calendarImage = BufferedImage(backgroundImage.width, 900, BufferedImage.TYPE_INT_RGB) - val graphics = calendarImage.createGraphics() - graphics.setRenderingHints() - - val font = FileManager.getInputStreamFromResource("assets/fonts/Satoshi-Regular.ttf").run { - val font = Font.createFont(Font.TRUETYPE_FONT, this) - close() - font - } - requireNotNull(font) { "Font not found" } - graphics.font = font.deriveFont(22f) - - graphics.drawImage(backgroundImage, 0, 0, null) - graphics.color = Color(0xFFFFFF) - graphics.font = graphics.font.deriveFont(24f) - graphics.font = graphics.font.deriveFont(graphics.font.style or Font.BOLD) - - val date = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("EEEE dd MMMM", Locale.FRENCH)) - - val dateString = "Les animés du $date".uppercase() - val dateWidth = graphics.fontMetrics.stringWidth(dateString) - graphics.drawString(dateString, 25, 60) - graphics.fillRect(25, 75, dateWidth, 2) - graphics.drawStringRect("CALENDRIER", 800, 25, 20, 20, backgroundColor = Color(0x2F2F2F)) - val episodesString = drawEpisodes(episodes, graphics, 150, 800) - graphics.dispose() - - val calendarImageByteArray = try { - val byteArrayOutputStream = ByteArrayOutputStream() - ImageIO.write(calendarImage, "jpg", byteArrayOutputStream) - byteArrayOutputStream.toByteArray() - } catch (e: Exception) { - logger.log(Level.SEVERE, "Error while converting calendar image for social networks", e) - return@use - } - - val message = "\uD83C\uDF05 Voici les sorties animés du ${date.lowercase()} : \n" + - "\n" + - "${episodesString.shuffled().take(4).joinToString("\n") { "• $it" }}\n" + - "\n" + - "Bonne journée à tous !" - - Constant.abstractSocialNetworks.parallelStream().forEach { socialNetwork -> - try { - socialNetwork.sendCalendar(message, calendarImageByteArray) - } catch (e: Exception) { - logger.log( - Level.SEVERE, - "Error while sending calendar for ${ - socialNetwork.javaClass.simpleName.replace( - "SocialNetwork", - "" - ) - }", - e - ) - } - } - } - } - } - - private fun drawEpisodes( - episodes: List, - graphics: Graphics2D, - baseY: Int, - episodeX: Int - ): MutableList { - var y = baseY - val episodesString = mutableListOf() - - episodes.sortedBy { it.platform.lowercase() }.groupBy { it.platform }.forEach { (platformName, episodes) -> - graphics.color = Color(0xFFFFFF) - graphics.font = graphics.font.deriveFont(22f) - graphics.font = graphics.font.deriveFont(graphics.font.style or Font.BOLD) - val platform = Platform.findByName(platformName) ?: return@forEach - - val platformImage = getPlatformImage(platform) - platformImage?.let { graphics.drawImage(it, 25, y - 25, null) } - graphics.drawString(platformName, 65, y) - graphics.fillRect( - 25, - y + 15, - (platformImage?.width ?: 0) + 10 + graphics.fontMetrics.stringWidth(platformName), - 2 - ) - - episodes.sortedBy { it.anime.lowercase() }.groupBy { it.anime }.forEach { (anime, episodes) -> - val twoLines = if (episodes.size == 1) { - drawSingleEpisode(episodes.first(), graphics, anime, y, episodeX, episodesString) - } else { - drawMinMaxEpisodes(episodes, graphics, anime, y, episodeX, episodesString) - } - - y += if (twoLines) 70 else 35 - } - - y += 100 - } - return episodesString - } - - private fun drawMinMaxEpisodes( - episodes: List, - graphics: Graphics2D, - anime: String, - y: Int, - episodeX: Int, - episodesString: MutableList - ) = episodes.groupBy { it.season }.all { (season, episodes) -> - val numbers = episodes.mapNotNull { Regex("\\d+").find(it.episode)?.value?.toIntOrNull() } - val min = numbers.minOrNull() ?: 0 - val max = numbers.maxOrNull() ?: 0 - - drawAnimeLine(graphics, anime, season, y).also { - graphics.drawString("Épisodes $min-$max", episodeX, y + 50) - episodesString.add("$anime${if (season > 1) " S${season}" else ""} Épisodes $min-$max") - } - } - - private fun drawSingleEpisode( - episode: CalendarEpisodeDto, - graphics: Graphics2D, - anime: String, - y: Int, - episodeX: Int, - episodesString: MutableList - ) = drawAnimeLine(graphics, anime, episode.season, y).also { - graphics.drawString(episode.episode, episodeX, y + 50) - episodesString.add( - "$anime${if (episode.season > 1) " S${episode.season}" else ""} ${ - episode.episode.replace( - "Épisode ", - "EP" - ) - }" - ) - } - - private fun getPlatformImage(platform: Platform): BufferedImage? { - val platformImage = runCatching { - ImageService.makeRoundedCorner( - ImageIO.read( - ClassLoader.getSystemClassLoader().getResourceAsStream("assets/img/platforms/${platform.image}") - ).resize(32, 32), 360 - ) - }.getOrNull() - return platformImage - } - - private fun getBackgroundImage(): BufferedImage? { - val calendarFolder = - ClassLoader.getSystemClassLoader() - .getResource("calendar")?.file?.let { File(it).takeIf { file -> file.exists() } } - ?: File( - Constant.dataFolder, - "calendar" - ) - require(calendarFolder.exists()) { "Calendar folder not found" } - val backgroundsFolder = File(calendarFolder, "backgrounds") - require(backgroundsFolder.exists()) { "Background folder not found" } - require(backgroundsFolder.listFiles()!!.isNotEmpty()) { "Backgrounds not found" } - val backgroundImage = ImageIO.read(backgroundsFolder.listFiles()!!.random()) - return backgroundImage - } - - private suspend fun getEpisodes( - elements: List, - httpRequest: HttpRequest - ) = elements.mapNotNull { element -> - val animePageElement = element.selectFirst("a") ?: return@mapNotNull null - val url = animePageElement.attr("href").let { "https://anime.icotaku.com$it" } - val episode = element.selectFirst(".calendrier_episode")?.text() ?: return@mapNotNull null - var title = animePageElement.text().trim() - val season = Regex("Saison (\\d+)").find(title)?.groupValues?.get(1)?.toIntOrNull() ?: 1 - - // Get the anime page - val animePage = httpRequest.get(url) - - if (animePage.status != HttpStatusCode.OK) { - return@mapNotNull null - } - - val animeBody = Jsoup.parse(animePage.bodyAsText()) - val list = animeBody.select(".info_fiche > div") ?: return@mapNotNull null - val licencePlatforms = - list.find { it.text().contains("Licence VOD") }?.select("a")?.map { it.text() }?.toMutableList() - licencePlatforms?.removeIf { it.contains("TF1") } - - if (licencePlatforms.isNullOrEmpty()) { - return@mapNotNull null - } - - if (configCacheService.getValueAsBoolean(ConfigPropertyKey.TRANSLATE_CALENDAR)) { - val alternativeTitles = - list.find { it.text().contains("Titre alternatif") }?.text()?.replace("Titre alternatif :", "") - ?.split("/") - ?: emptyList() - val detectedLanguage = languageCacheService.detectLanguage(title) - - if (detectedLanguage != null && detectedLanguage == "fr" && alternativeTitles.isNotEmpty()) { - title = alternativeTitles.first().trim() - } - } - - title = title.replace(Regex(" - Saison \\d+"), "").trim() - - licencePlatforms.map { - CalendarEpisodeDto( - anime = StringUtils.getShortName(title), - season = season, - episode = episode.capitalizeWords(), - platform = it - ) - } - }.flatten() - - private fun drawAnimeLine(graphics: Graphics2D, anime: String, season: Int, y: Int): Boolean { - val x = 65 - - val s = "${anime}${if (season > 1) " S${season}" else ""}" - val width = graphics.fontMetrics.stringWidth(s) - - // Split the anime name on two lines if it's too long - if (width > 750) { - val words = s.split(" ") - var separator = words.size / 2 - val (firstHalf, secondHalf) = words.withIndex().partition { (index, _) -> index < separator } - var first = firstHalf.joinToString(" ") { it.value } - var second = secondHalf.joinToString(" ") { it.value } - - // If the last word of the first line and the first word of the second line is the same, we move the separator - // Or if the first word of the first line and the first word of the second line is the same, we move the separator - if (first.split(" ").last() == second.split(" ").first() || first.split(" ").first() == second.split(" ") - .first() - ) { - separator++ - val (firstHalfTry, secondHalfTry) = words.withIndex().partition { (index, _) -> index < separator } - first = firstHalfTry.joinToString(" ") { it.value } - second = secondHalfTry.joinToString(" ") { it.value } - } - - graphics.drawString(first, x, y + 50) - graphics.drawString(second, x, y + 80) - return true - } - - graphics.drawString(s, x, y + 50) - return false - } -} \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/platforms/CrunchyrollPlatform.kt b/src/main/kotlin/fr/shikkanime/platforms/CrunchyrollPlatform.kt index e454268a..8387e2b2 100644 --- a/src/main/kotlin/fr/shikkanime/platforms/CrunchyrollPlatform.kt +++ b/src/main/kotlin/fr/shikkanime/platforms/CrunchyrollPlatform.kt @@ -286,7 +286,7 @@ class CrunchyrollPlatform : ) } - fun getNumberAndEpisodeType(episode: CrunchyrollWrapper.Episode): Pair { + private fun getNumberAndEpisodeType(episode: CrunchyrollWrapper.Episode): Pair { var number = episode.number ?: -1 val specialEpisodeRegex = "SP(\\d*)".toRegex() diff --git a/src/main/kotlin/fr/shikkanime/services/ImageService.kt b/src/main/kotlin/fr/shikkanime/services/ImageService.kt index a9f307a0..8fe58efd 100644 --- a/src/main/kotlin/fr/shikkanime/services/ImageService.kt +++ b/src/main/kotlin/fr/shikkanime/services/ImageService.kt @@ -388,7 +388,7 @@ object ImageService { return Color(redAverage.toInt(), greenAverage.toInt(), blueAverage.toInt()) } - fun makeRoundedCorner(image: BufferedImage, cornerRadius: Int): BufferedImage { + private fun makeRoundedCorner(image: BufferedImage, cornerRadius: Int): BufferedImage { return BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_ARGB).apply { createGraphics().apply { composite = AlphaComposite.Src @@ -411,7 +411,7 @@ object ImageService { } } - fun Graphics2D.setRenderingHints() { + private fun Graphics2D.setRenderingHints() { setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR) setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY) setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB) @@ -652,31 +652,4 @@ object ImageService { graphics.dispose() return finalImage } - - fun Graphics2D.drawStringRect( - text: String, - x: Int, - y: Int, - marginX: Int, - marginY: Int, - borderWidth: Int = 2, - backgroundColor: Color = Color(0x000), - ) { - val fontMetrics = fontMetrics - val textWidth = fontMetrics.stringWidth(text) - val textHeight = fontMetrics.height - - fillRoundRect(x, y, textWidth + marginX, textHeight + marginY, 10, 10) - color = backgroundColor - fillRoundRect( - x + borderWidth, - y + borderWidth, - textWidth + marginX - (borderWidth * 2), - textHeight + marginY - (borderWidth * 2), - 10, - 10 - ) - color = Color.WHITE - drawString(text, x + marginX / 2, y + (marginY / 2) + textHeight - 5) - } } \ 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 20c0b547..87435d0e 100644 --- a/src/main/kotlin/fr/shikkanime/socialnetworks/AbstractSocialNetwork.kt +++ b/src/main/kotlin/fr/shikkanime/socialnetworks/AbstractSocialNetwork.kt @@ -56,8 +56,4 @@ abstract class AbstractSocialNetwork { "${Constant.baseUrl}/animes/${episodeDto.mapping.anime.slug}/season-${episodeDto.mapping.season}/${episodeDto.mapping.episodeType.slug}-${episodeDto.mapping.number}?utm_campaign=episode_post&utm_medium=social&utm_source=${utmSource()}&utm_content=${episodeDto.uuid}" abstract fun sendEpisodeRelease(episodeDto: EpisodeVariantDto, mediaImage: ByteArray?) - - open fun sendCalendar(message: String, calendarImage: ByteArray) { - // Default implementation - } } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/socialnetworks/BskySocialNetwork.kt b/src/main/kotlin/fr/shikkanime/socialnetworks/BskySocialNetwork.kt index 0c9e6953..14abd071 100644 --- a/src/main/kotlin/fr/shikkanime/socialnetworks/BskySocialNetwork.kt +++ b/src/main/kotlin/fr/shikkanime/socialnetworks/BskySocialNetwork.kt @@ -86,25 +86,4 @@ class BskySocialNetwork : AbstractSocialNetwork() { ) } } - - override fun sendCalendar(message: String, calendarImage: ByteArray) { - checkSession() - if (!isInitialized) return - runBlocking { - BskyWrapper.createRecord( - accessJwt!!, - did!!, - message, - listOf( - BskyWrapper.Image( - BskyWrapper.uploadBlob( - accessJwt!!, - ContentType.Image.JPEG, - calendarImage - ) - ) - ) - ) - } - } } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/socialnetworks/ThreadsSocialNetwork.kt b/src/main/kotlin/fr/shikkanime/socialnetworks/ThreadsSocialNetwork.kt index 5e6464b3..44c93993 100644 --- a/src/main/kotlin/fr/shikkanime/socialnetworks/ThreadsSocialNetwork.kt +++ b/src/main/kotlin/fr/shikkanime/socialnetworks/ThreadsSocialNetwork.kt @@ -83,10 +83,4 @@ class ThreadsSocialNetwork : AbstractSocialNetwork() { getEpisodeMessage(episodeDto, configCacheService.getValueAsString(ConfigPropertyKey.THREADS_MESSAGE) ?: "") runBlocking { threadsWrapper.publish(username!!, deviceId!!, userId!!, token!!, message, mediaImage) } } - - override fun sendCalendar(message: String, calendarImage: ByteArray) { - checkSession() - if (!isInitialized) return - runBlocking { threadsWrapper.publish(username!!, deviceId!!, userId!!, token!!, message, calendarImage) } - } } \ No newline at end of file diff --git a/src/main/kotlin/fr/shikkanime/socialnetworks/TwitterSocialNetwork.kt b/src/main/kotlin/fr/shikkanime/socialnetworks/TwitterSocialNetwork.kt index 9023384a..66f872ec 100644 --- a/src/main/kotlin/fr/shikkanime/socialnetworks/TwitterSocialNetwork.kt +++ b/src/main/kotlin/fr/shikkanime/socialnetworks/TwitterSocialNetwork.kt @@ -94,17 +94,4 @@ class TwitterSocialNetwork : AbstractSocialNetwork() { ) } } - - override fun sendCalendar(message: String, calendarImage: ByteArray) { - login() - if (!isInitialized) return - if (twitter == null) return - - val uploadMedia = twitter!!.tweets().uploadMedia( - UUID.randomUUID().toString(), - ByteArrayInputStream(calendarImage) - ) - - twitter!!.v2.createTweet(mediaIds = arrayOf(uploadMedia.mediaId), text = message) - } } \ No newline at end of file diff --git a/src/main/resources/db/changelog/2024/09/03-changelog.xml b/src/main/resources/db/changelog/2024/09/03-changelog.xml new file mode 100644 index 00000000..9086245e --- /dev/null +++ b/src/main/resources/db/changelog/2024/09/03-changelog.xml @@ -0,0 +1,16 @@ + + + + + + + + property_key IN ('translate_calendar') + + + \ 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 952eacee..8d79eed8 100644 --- a/src/main/resources/db/changelog/db.changelog-master.xml +++ b/src/main/resources/db/changelog/db.changelog-master.xml @@ -61,4 +61,5 @@ + \ No newline at end of file