From 5f8691e1af7a3662f156700aafc03acbd3e82d2f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?=
Date: Wed, 14 Jan 2026 01:18:39 +0100
Subject: [PATCH] Setup scheduler
---
.../imgfloat/ImgfloatApplication.java | 2 +
.../imgfloat/config/SchedulingConfig.java | 19 +++++
.../kruhlmann/imgfloat/model/Settings.java | 12 +++
.../imgfloat/service/EmoteSyncScheduler.java | 81 +++++++++++++++++++
.../imgfloat/service/SevenTvEmoteService.java | 8 ++
.../imgfloat/service/TwitchEmoteService.java | 12 +++
.../V5__settings_emote_sync_interval.sql | 1 +
src/main/resources/static/js/settings.js | 5 ++
src/main/resources/templates/settings.html | 30 +++++++
9 files changed, 170 insertions(+)
create mode 100644 src/main/java/dev/kruhlmann/imgfloat/config/SchedulingConfig.java
create mode 100644 src/main/java/dev/kruhlmann/imgfloat/service/EmoteSyncScheduler.java
create mode 100644 src/main/resources/db/migration/V5__settings_emote_sync_interval.sql
diff --git a/src/main/java/dev/kruhlmann/imgfloat/ImgfloatApplication.java b/src/main/java/dev/kruhlmann/imgfloat/ImgfloatApplication.java
index 97b9fd2..a5dae60 100644
--- a/src/main/java/dev/kruhlmann/imgfloat/ImgfloatApplication.java
+++ b/src/main/java/dev/kruhlmann/imgfloat/ImgfloatApplication.java
@@ -3,8 +3,10 @@ package dev.kruhlmann.imgfloat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
@EnableAsync
+@EnableScheduling
@SpringBootApplication
public class ImgfloatApplication {
diff --git a/src/main/java/dev/kruhlmann/imgfloat/config/SchedulingConfig.java b/src/main/java/dev/kruhlmann/imgfloat/config/SchedulingConfig.java
new file mode 100644
index 0000000..358ba4b
--- /dev/null
+++ b/src/main/java/dev/kruhlmann/imgfloat/config/SchedulingConfig.java
@@ -0,0 +1,19 @@
+package dev.kruhlmann.imgfloat.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+
+@Configuration
+public class SchedulingConfig {
+
+ @Bean
+ public TaskScheduler emoteSyncTaskScheduler() {
+ ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
+ scheduler.setPoolSize(1);
+ scheduler.setThreadNamePrefix("emote-sync-");
+ scheduler.initialize();
+ return scheduler;
+ }
+}
diff --git a/src/main/java/dev/kruhlmann/imgfloat/model/Settings.java b/src/main/java/dev/kruhlmann/imgfloat/model/Settings.java
index b50bd58..e99bb9f 100644
--- a/src/main/java/dev/kruhlmann/imgfloat/model/Settings.java
+++ b/src/main/java/dev/kruhlmann/imgfloat/model/Settings.java
@@ -40,6 +40,9 @@ public class Settings {
@Column(nullable = false)
private int canvasFramesPerSecond;
+ @Column(nullable = false)
+ private int emoteSyncIntervalMinutes;
+
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@@ -58,6 +61,7 @@ public class Settings {
s.setMaxAssetVolumeFraction(5.0);
s.setMaxCanvasSideLengthPixels(7680);
s.setCanvasFramesPerSecond(60);
+ s.setEmoteSyncIntervalMinutes(60);
return s;
}
@@ -133,6 +137,14 @@ public class Settings {
this.canvasFramesPerSecond = canvasFramesPerSecond;
}
+ public int getEmoteSyncIntervalMinutes() {
+ return emoteSyncIntervalMinutes;
+ }
+
+ public void setEmoteSyncIntervalMinutes(int emoteSyncIntervalMinutes) {
+ this.emoteSyncIntervalMinutes = emoteSyncIntervalMinutes;
+ }
+
@PrePersist
public void initializeTimestamps() {
Instant now = Instant.now();
diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/EmoteSyncScheduler.java b/src/main/java/dev/kruhlmann/imgfloat/service/EmoteSyncScheduler.java
new file mode 100644
index 0000000..dfc0316
--- /dev/null
+++ b/src/main/java/dev/kruhlmann/imgfloat/service/EmoteSyncScheduler.java
@@ -0,0 +1,81 @@
+package dev.kruhlmann.imgfloat.service;
+
+import dev.kruhlmann.imgfloat.model.Channel;
+import dev.kruhlmann.imgfloat.model.Settings;
+import dev.kruhlmann.imgfloat.repository.ChannelRepository;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.Trigger;
+import org.springframework.scheduling.TriggerContext;
+import org.springframework.scheduling.annotation.SchedulingConfigurer;
+import org.springframework.scheduling.config.ScheduledTaskRegistrar;
+import org.springframework.stereotype.Service;
+
+@Service
+public class EmoteSyncScheduler implements SchedulingConfigurer {
+
+ private static final Logger LOG = LoggerFactory.getLogger(EmoteSyncScheduler.class);
+ private static final int DEFAULT_INTERVAL_MINUTES = 60;
+
+ private final SettingsService settingsService;
+ private final ChannelRepository channelRepository;
+ private final TwitchEmoteService twitchEmoteService;
+ private final SevenTvEmoteService sevenTvEmoteService;
+ private final TaskScheduler taskScheduler;
+
+ public EmoteSyncScheduler(
+ SettingsService settingsService,
+ ChannelRepository channelRepository,
+ TwitchEmoteService twitchEmoteService,
+ SevenTvEmoteService sevenTvEmoteService,
+ @Qualifier("emoteSyncTaskScheduler") TaskScheduler taskScheduler
+ ) {
+ this.settingsService = settingsService;
+ this.channelRepository = channelRepository;
+ this.twitchEmoteService = twitchEmoteService;
+ this.sevenTvEmoteService = sevenTvEmoteService;
+ this.taskScheduler = taskScheduler;
+ }
+
+ @Override
+ public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
+ taskRegistrar.setScheduler(taskScheduler);
+ taskRegistrar.addTriggerTask(this::syncEmotes, buildTrigger());
+ }
+
+ private Trigger buildTrigger() {
+ return (TriggerContext triggerContext) -> {
+ Instant lastCompletion = triggerContext.lastCompletionTime() == null
+ ? Instant.now()
+ : triggerContext.lastCompletionTime().toInstant();
+ return Date.from(lastCompletion.plus(Duration.ofMinutes(resolveIntervalMinutes())));
+ };
+ }
+
+ private int resolveIntervalMinutes() {
+ Settings settings = settingsService.get();
+ int interval = settings.getEmoteSyncIntervalMinutes();
+ return interval > 0 ? interval : DEFAULT_INTERVAL_MINUTES;
+ }
+
+ private void syncEmotes() {
+ int interval = resolveIntervalMinutes();
+ LOG.info("Synchronizing emotes (interval {} minutes)", interval);
+
+ twitchEmoteService.refreshGlobalEmotes();
+ List channels = channelRepository.findAll();
+ for (Channel channel : channels) {
+ String broadcaster = channel.getBroadcaster();
+ twitchEmoteService.refreshChannelEmotes(broadcaster);
+ sevenTvEmoteService.refreshChannelEmotes(broadcaster);
+ }
+
+ LOG.info("Completed emote sync for {} channels", channels.size());
+ }
+}
diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/SevenTvEmoteService.java b/src/main/java/dev/kruhlmann/imgfloat/service/SevenTvEmoteService.java
index 8008280..5c8e6c8 100644
--- a/src/main/java/dev/kruhlmann/imgfloat/service/SevenTvEmoteService.java
+++ b/src/main/java/dev/kruhlmann/imgfloat/service/SevenTvEmoteService.java
@@ -70,6 +70,14 @@ public class SevenTvEmoteService {
return emotes.stream().map(CachedEmote::descriptor).toList();
}
+ public void refreshChannelEmotes(String channelLogin) {
+ if (channelLogin == null || channelLogin.isBlank()) {
+ return;
+ }
+ String normalized = channelLogin.toLowerCase(Locale.ROOT);
+ channelEmoteCache.put(normalized, fetchChannelEmotes(normalized));
+ }
+
public Optional loadEmoteAsset(String emoteId) {
if (emoteId == null || emoteId.isBlank()) {
return Optional.empty();
diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/TwitchEmoteService.java b/src/main/java/dev/kruhlmann/imgfloat/service/TwitchEmoteService.java
index 2c9c44c..852ec95 100644
--- a/src/main/java/dev/kruhlmann/imgfloat/service/TwitchEmoteService.java
+++ b/src/main/java/dev/kruhlmann/imgfloat/service/TwitchEmoteService.java
@@ -71,6 +71,10 @@ public class TwitchEmoteService {
return globalEmotes.stream().map(CachedEmote::descriptor).toList();
}
+ public void refreshGlobalEmotes() {
+ warmGlobalEmotes();
+ }
+
public List getChannelEmotes(String channelLogin) {
if (channelLogin == null || channelLogin.isBlank()) {
return List.of();
@@ -80,6 +84,14 @@ public class TwitchEmoteService {
return emotes.stream().map(CachedEmote::descriptor).toList();
}
+ public void refreshChannelEmotes(String channelLogin) {
+ if (channelLogin == null || channelLogin.isBlank()) {
+ return;
+ }
+ String normalized = channelLogin.toLowerCase(Locale.ROOT);
+ channelEmoteCache.put(normalized, fetchChannelEmotes(normalized));
+ }
+
public Optional loadEmoteAsset(String emoteId) {
if (emoteId == null || emoteId.isBlank()) {
return Optional.empty();
diff --git a/src/main/resources/db/migration/V5__settings_emote_sync_interval.sql b/src/main/resources/db/migration/V5__settings_emote_sync_interval.sql
new file mode 100644
index 0000000..d36b284
--- /dev/null
+++ b/src/main/resources/db/migration/V5__settings_emote_sync_interval.sql
@@ -0,0 +1 @@
+ALTER TABLE settings ADD COLUMN emote_sync_interval_minutes INTEGER NOT NULL DEFAULT 60;
diff --git a/src/main/resources/static/js/settings.js b/src/main/resources/static/js/settings.js
index 995b560..77e91a9 100644
--- a/src/main/resources/static/js/settings.js
+++ b/src/main/resources/static/js/settings.js
@@ -8,12 +8,14 @@ const minPitchElement = document.getElementById("min-audio-pitch");
const maxPitchElement = document.getElementById("max-audio-pitch");
const minVolumeElement = document.getElementById("min-volume");
const maxVolumeElement = document.getElementById("max-volume");
+const emoteSyncIntervalElement = document.getElementById("emote-sync-interval");
const statusElement = document.getElementById("settings-status");
const statCanvasFpsElement = document.getElementById("stat-canvas-fps");
const statCanvasSizeElement = document.getElementById("stat-canvas-size");
const statPlaybackRangeElement = document.getElementById("stat-playback-range");
const statAudioRangeElement = document.getElementById("stat-audio-range");
const statVolumeRangeElement = document.getElementById("stat-volume-range");
+const statEmoteSyncElement = document.getElementById("stat-emote-sync");
const sysadminListElement = document.getElementById("sysadmin-list");
const sysadminInputElement = document.getElementById("new-sysadmin");
const addSysadminButtonElement = document.getElementById("add-sysadmin-button");
@@ -55,6 +57,7 @@ function setFormSettings(s) {
maxPitchElement.value = s.maxAssetAudioPitchFraction;
minVolumeElement.value = s.minAssetVolumeFraction;
maxVolumeElement.value = s.maxAssetVolumeFraction;
+ emoteSyncIntervalElement.value = s.emoteSyncIntervalMinutes;
}
function updateStatCards(settings) {
@@ -64,6 +67,7 @@ function updateStatCards(settings) {
statPlaybackRangeElement.textContent = `${settings.minAssetPlaybackSpeedFraction ?? "--"} – ${settings.maxAssetPlaybackSpeedFraction ?? "--"}x`;
statAudioRangeElement.textContent = `${settings.minAssetAudioPitchFraction ?? "--"} – ${settings.maxAssetAudioPitchFraction ?? "--"}x`;
statVolumeRangeElement.textContent = `${settings.minAssetVolumeFraction ?? "--"} – ${settings.maxAssetVolumeFraction ?? "--"}x`;
+ statEmoteSyncElement.textContent = `${settings.emoteSyncIntervalMinutes ?? "--"} min`;
}
function readInt(input) {
@@ -83,6 +87,7 @@ function loadUserSettingsFromDom() {
userSettings.maxAssetAudioPitchFraction = readFloat(maxPitchElement);
userSettings.minAssetVolumeFraction = readFloat(minVolumeElement);
userSettings.maxAssetVolumeFraction = readFloat(maxVolumeElement);
+ userSettings.emoteSyncIntervalMinutes = readInt(emoteSyncIntervalElement);
}
function updateSubmitButtonDisabledState() {
diff --git a/src/main/resources/templates/settings.html b/src/main/resources/templates/settings.html
index 8279c70..a733180 100644
--- a/src/main/resources/templates/settings.html
+++ b/src/main/resources/templates/settings.html
@@ -61,6 +61,11 @@
--
Keeps alerts comfortable
+
+
Emote sync
+
--
+
Minutes between refreshes
+
@@ -224,6 +229,31 @@
+
+