Deferred emote sync

This commit is contained in:
2026-01-30 08:51:55 +01:00
parent f9613c7c2f
commit d6271b1758
2 changed files with 56 additions and 2 deletions

View File

@@ -0,0 +1,25 @@
package dev.kruhlmann.imgfloat.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class ApplicationLifecycleLogger {
private static final Logger LOG = LoggerFactory.getLogger(ApplicationLifecycleLogger.class);
private final String serverPort;
public ApplicationLifecycleLogger(@Value("${server.port:8080}") String serverPort) {
this.serverPort = serverPort;
}
@EventListener(ApplicationReadyEvent.class)
public void logReady() {
LOG.info("Imgfloat ready to accept connections on port {}", serverPort);
}
}

View File

@@ -12,7 +12,9 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -22,6 +24,8 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
@@ -41,6 +45,7 @@ public class TwitchEmoteService {
private final Map<String, CachedEmote> emoteCache = new ConcurrentHashMap<>(); private final Map<String, CachedEmote> emoteCache = new ConcurrentHashMap<>();
private final Map<String, List<CachedEmote>> channelEmoteCache = new ConcurrentHashMap<>(); private final Map<String, List<CachedEmote>> channelEmoteCache = new ConcurrentHashMap<>();
private volatile List<CachedEmote> globalEmotes = List.of(); private volatile List<CachedEmote> globalEmotes = List.of();
private final AtomicBoolean initialGlobalSyncScheduled = new AtomicBoolean();
public TwitchEmoteService( public TwitchEmoteService(
RestTemplateBuilder builder, RestTemplateBuilder builder,
@@ -61,12 +66,11 @@ public class TwitchEmoteService {
} catch (IOException ex) { } catch (IOException ex) {
throw new IllegalStateException("Failed to create Twitch emote cache directory", ex); throw new IllegalStateException("Failed to create Twitch emote cache directory", ex);
} }
warmGlobalEmotes();
} }
public List<EmoteDescriptor> getGlobalEmotes() { public List<EmoteDescriptor> getGlobalEmotes() {
if (globalEmotes.isEmpty()) { if (globalEmotes.isEmpty()) {
warmGlobalEmotes(); ensureInitialGlobalSyncScheduled();
} }
return globalEmotes.stream().map(CachedEmote::descriptor).toList(); return globalEmotes.stream().map(CachedEmote::descriptor).toList();
} }
@@ -127,6 +131,31 @@ public class TwitchEmoteService {
LOG.info("Loaded {} global Twitch emotes", cached.size()); LOG.info("Loaded {} global Twitch emotes", cached.size());
} }
private void ensureInitialGlobalSyncScheduled() {
if (initialGlobalSyncScheduled.compareAndSet(false, true)) {
LOG.info("Scheduling initial global Twitch emote sync in the background");
CompletableFuture.runAsync(this::safeWarmGlobalEmotes);
}
}
private void safeWarmGlobalEmotes() {
LOG.info("Initial global Twitch emote sync started");
try {
warmGlobalEmotes();
LOG.info(
"Initial global Twitch emote sync completed (cached {} emotes)",
globalEmotes.size()
);
} catch (Exception ex) {
LOG.warn("Initial global Twitch emote sync failed", ex);
}
}
@EventListener(ApplicationReadyEvent.class)
public void startInitialGlobalEmoteSync() {
ensureInitialGlobalSyncScheduled();
}
private List<CachedEmote> fetchChannelEmotes(String channelLogin) { private List<CachedEmote> fetchChannelEmotes(String channelLogin) {
String broadcasterId = fetchBroadcasterId(channelLogin).orElse(null); String broadcasterId = fetchBroadcasterId(channelLogin).orElse(null);
if (broadcasterId == null) { if (broadcasterId == null) {