mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Setup scheduler
This commit is contained in:
@@ -3,8 +3,10 @@ package dev.kruhlmann.imgfloat;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
|
@EnableScheduling
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class ImgfloatApplication {
|
public class ImgfloatApplication {
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,9 @@ public class Settings {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private int canvasFramesPerSecond;
|
private int canvasFramesPerSecond;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private int emoteSyncIntervalMinutes;
|
||||||
|
|
||||||
@Column(name = "created_at", nullable = false, updatable = false)
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
private Instant createdAt;
|
private Instant createdAt;
|
||||||
|
|
||||||
@@ -58,6 +61,7 @@ public class Settings {
|
|||||||
s.setMaxAssetVolumeFraction(5.0);
|
s.setMaxAssetVolumeFraction(5.0);
|
||||||
s.setMaxCanvasSideLengthPixels(7680);
|
s.setMaxCanvasSideLengthPixels(7680);
|
||||||
s.setCanvasFramesPerSecond(60);
|
s.setCanvasFramesPerSecond(60);
|
||||||
|
s.setEmoteSyncIntervalMinutes(60);
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +137,14 @@ public class Settings {
|
|||||||
this.canvasFramesPerSecond = canvasFramesPerSecond;
|
this.canvasFramesPerSecond = canvasFramesPerSecond;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getEmoteSyncIntervalMinutes() {
|
||||||
|
return emoteSyncIntervalMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmoteSyncIntervalMinutes(int emoteSyncIntervalMinutes) {
|
||||||
|
this.emoteSyncIntervalMinutes = emoteSyncIntervalMinutes;
|
||||||
|
}
|
||||||
|
|
||||||
@PrePersist
|
@PrePersist
|
||||||
public void initializeTimestamps() {
|
public void initializeTimestamps() {
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
|
|||||||
@@ -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<Channel> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,14 @@ public class SevenTvEmoteService {
|
|||||||
return emotes.stream().map(CachedEmote::descriptor).toList();
|
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<EmoteAsset> loadEmoteAsset(String emoteId) {
|
public Optional<EmoteAsset> loadEmoteAsset(String emoteId) {
|
||||||
if (emoteId == null || emoteId.isBlank()) {
|
if (emoteId == null || emoteId.isBlank()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ public class TwitchEmoteService {
|
|||||||
return globalEmotes.stream().map(CachedEmote::descriptor).toList();
|
return globalEmotes.stream().map(CachedEmote::descriptor).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void refreshGlobalEmotes() {
|
||||||
|
warmGlobalEmotes();
|
||||||
|
}
|
||||||
|
|
||||||
public List<EmoteDescriptor> getChannelEmotes(String channelLogin) {
|
public List<EmoteDescriptor> getChannelEmotes(String channelLogin) {
|
||||||
if (channelLogin == null || channelLogin.isBlank()) {
|
if (channelLogin == null || channelLogin.isBlank()) {
|
||||||
return List.of();
|
return List.of();
|
||||||
@@ -80,6 +84,14 @@ public class TwitchEmoteService {
|
|||||||
return emotes.stream().map(CachedEmote::descriptor).toList();
|
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<EmoteAsset> loadEmoteAsset(String emoteId) {
|
public Optional<EmoteAsset> loadEmoteAsset(String emoteId) {
|
||||||
if (emoteId == null || emoteId.isBlank()) {
|
if (emoteId == null || emoteId.isBlank()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE settings ADD COLUMN emote_sync_interval_minutes INTEGER NOT NULL DEFAULT 60;
|
||||||
@@ -8,12 +8,14 @@ const minPitchElement = document.getElementById("min-audio-pitch");
|
|||||||
const maxPitchElement = document.getElementById("max-audio-pitch");
|
const maxPitchElement = document.getElementById("max-audio-pitch");
|
||||||
const minVolumeElement = document.getElementById("min-volume");
|
const minVolumeElement = document.getElementById("min-volume");
|
||||||
const maxVolumeElement = document.getElementById("max-volume");
|
const maxVolumeElement = document.getElementById("max-volume");
|
||||||
|
const emoteSyncIntervalElement = document.getElementById("emote-sync-interval");
|
||||||
const statusElement = document.getElementById("settings-status");
|
const statusElement = document.getElementById("settings-status");
|
||||||
const statCanvasFpsElement = document.getElementById("stat-canvas-fps");
|
const statCanvasFpsElement = document.getElementById("stat-canvas-fps");
|
||||||
const statCanvasSizeElement = document.getElementById("stat-canvas-size");
|
const statCanvasSizeElement = document.getElementById("stat-canvas-size");
|
||||||
const statPlaybackRangeElement = document.getElementById("stat-playback-range");
|
const statPlaybackRangeElement = document.getElementById("stat-playback-range");
|
||||||
const statAudioRangeElement = document.getElementById("stat-audio-range");
|
const statAudioRangeElement = document.getElementById("stat-audio-range");
|
||||||
const statVolumeRangeElement = document.getElementById("stat-volume-range");
|
const statVolumeRangeElement = document.getElementById("stat-volume-range");
|
||||||
|
const statEmoteSyncElement = document.getElementById("stat-emote-sync");
|
||||||
const sysadminListElement = document.getElementById("sysadmin-list");
|
const sysadminListElement = document.getElementById("sysadmin-list");
|
||||||
const sysadminInputElement = document.getElementById("new-sysadmin");
|
const sysadminInputElement = document.getElementById("new-sysadmin");
|
||||||
const addSysadminButtonElement = document.getElementById("add-sysadmin-button");
|
const addSysadminButtonElement = document.getElementById("add-sysadmin-button");
|
||||||
@@ -55,6 +57,7 @@ function setFormSettings(s) {
|
|||||||
maxPitchElement.value = s.maxAssetAudioPitchFraction;
|
maxPitchElement.value = s.maxAssetAudioPitchFraction;
|
||||||
minVolumeElement.value = s.minAssetVolumeFraction;
|
minVolumeElement.value = s.minAssetVolumeFraction;
|
||||||
maxVolumeElement.value = s.maxAssetVolumeFraction;
|
maxVolumeElement.value = s.maxAssetVolumeFraction;
|
||||||
|
emoteSyncIntervalElement.value = s.emoteSyncIntervalMinutes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatCards(settings) {
|
function updateStatCards(settings) {
|
||||||
@@ -64,6 +67,7 @@ function updateStatCards(settings) {
|
|||||||
statPlaybackRangeElement.textContent = `${settings.minAssetPlaybackSpeedFraction ?? "--"} – ${settings.maxAssetPlaybackSpeedFraction ?? "--"}x`;
|
statPlaybackRangeElement.textContent = `${settings.minAssetPlaybackSpeedFraction ?? "--"} – ${settings.maxAssetPlaybackSpeedFraction ?? "--"}x`;
|
||||||
statAudioRangeElement.textContent = `${settings.minAssetAudioPitchFraction ?? "--"} – ${settings.maxAssetAudioPitchFraction ?? "--"}x`;
|
statAudioRangeElement.textContent = `${settings.minAssetAudioPitchFraction ?? "--"} – ${settings.maxAssetAudioPitchFraction ?? "--"}x`;
|
||||||
statVolumeRangeElement.textContent = `${settings.minAssetVolumeFraction ?? "--"} – ${settings.maxAssetVolumeFraction ?? "--"}x`;
|
statVolumeRangeElement.textContent = `${settings.minAssetVolumeFraction ?? "--"} – ${settings.maxAssetVolumeFraction ?? "--"}x`;
|
||||||
|
statEmoteSyncElement.textContent = `${settings.emoteSyncIntervalMinutes ?? "--"} min`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readInt(input) {
|
function readInt(input) {
|
||||||
@@ -83,6 +87,7 @@ function loadUserSettingsFromDom() {
|
|||||||
userSettings.maxAssetAudioPitchFraction = readFloat(maxPitchElement);
|
userSettings.maxAssetAudioPitchFraction = readFloat(maxPitchElement);
|
||||||
userSettings.minAssetVolumeFraction = readFloat(minVolumeElement);
|
userSettings.minAssetVolumeFraction = readFloat(minVolumeElement);
|
||||||
userSettings.maxAssetVolumeFraction = readFloat(maxVolumeElement);
|
userSettings.maxAssetVolumeFraction = readFloat(maxVolumeElement);
|
||||||
|
userSettings.emoteSyncIntervalMinutes = readInt(emoteSyncIntervalElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSubmitButtonDisabledState() {
|
function updateSubmitButtonDisabledState() {
|
||||||
|
|||||||
@@ -61,6 +61,11 @@
|
|||||||
<p class="stat-value" id="stat-volume-range">--</p>
|
<p class="stat-value" id="stat-volume-range">--</p>
|
||||||
<p class="stat-subtitle">Keeps alerts comfortable</p>
|
<p class="stat-subtitle">Keeps alerts comfortable</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<p class="stat-label">Emote sync</p>
|
||||||
|
<p class="stat-value" id="stat-emote-sync">--</p>
|
||||||
|
<p class="stat-subtitle">Minutes between refreshes</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -224,6 +229,31 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-heading">
|
||||||
|
<p class="eyebrow subtle">Emotes</p>
|
||||||
|
<h3>Emote sync interval</h3>
|
||||||
|
<p class="muted tiny">
|
||||||
|
Choose how often Imgfloat refreshes Twitch and 7TV emote catalogs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="control-grid">
|
||||||
|
<label for="emote-sync-interval"
|
||||||
|
>Emote sync interval (minutes)
|
||||||
|
<input
|
||||||
|
id="emote-sync-interval"
|
||||||
|
name="emote-sync-interval"
|
||||||
|
class="text-input"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
pattern="^[1-9]\\d*$"
|
||||||
|
placeholder="60"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="field-hint">Set to 60 for hourly refreshes.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
<p id="settings-status" class="status-chip">No changes yet.</p>
|
<p id="settings-status" class="status-chip">No changes yet.</p>
|
||||||
<button id="settings-submit-button" type="submit" class="button" disabled>
|
<button id="settings-submit-button" type="submit" class="button" disabled>
|
||||||
|
|||||||
Reference in New Issue
Block a user