Add default custom asset

This commit is contained in:
2026-01-10 01:48:33 +01:00
parent c3736e682b
commit 6cba63d025
5 changed files with 315 additions and 31 deletions

View File

@@ -41,9 +41,11 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
@@ -424,7 +426,20 @@ public class ChannelDirectoryService {
public List<ScriptMarketplaceEntry> listMarketplaceScripts(String query) {
String q = normalizeDescription(query);
String normalizedQuery = q == null ? null : q.toLowerCase(Locale.ROOT);
List<ScriptAsset> scripts = scriptAssetRepository.findByIsPublicTrue();
List<ScriptMarketplaceEntry> entries = new ArrayList<>();
DefaultMarketplaceScript.entryForQuery(normalizedQuery).ifPresent(entries::add);
List<ScriptAsset> scripts;
try {
scripts = scriptAssetRepository.findByIsPublicTrue();
} catch (DataAccessException ex) {
logger.warn("Unable to load marketplace scripts", ex);
return entries
.stream()
.sorted(
Comparator.comparing(ScriptMarketplaceEntry::name, Comparator.nullsLast(String::compareToIgnoreCase))
)
.toList();
}
if (normalizedQuery != null && !normalizedQuery.isBlank()) {
scripts =
scripts
@@ -442,42 +457,62 @@ public class ChannelDirectoryService {
.stream()
.collect(Collectors.toMap(Asset::getId, (asset) -> asset));
return scripts
entries.addAll(
scripts
.stream()
.map((script) -> {
Asset asset = assets.get(script.getId());
String broadcaster = asset != null ? asset.getBroadcaster() : "";
String logoUrl = script.getLogoFileId() == null
? null
: "/api/marketplace/scripts/" + script.getId() + "/logo";
return new ScriptMarketplaceEntry(
script.getId(),
script.getName(),
script.getDescription(),
logoUrl,
broadcaster
);
})
.toList()
);
return entries
.stream()
.map((script) -> {
Asset asset = assets.get(script.getId());
String broadcaster = asset != null ? asset.getBroadcaster() : "";
String logoUrl = script.getLogoFileId() == null
? null
: "/api/marketplace/scripts/" + script.getId() + "/logo";
return new ScriptMarketplaceEntry(
script.getId(),
script.getName(),
script.getDescription(),
logoUrl,
broadcaster
);
})
.sorted(Comparator.comparing(ScriptMarketplaceEntry::name, Comparator.nullsLast(String::compareToIgnoreCase)))
.toList();
}
public Optional<AssetContent> getMarketplaceLogo(String scriptId) {
return scriptAssetRepository
.findById(scriptId)
.filter(ScriptAsset::isPublic)
.map(ScriptAsset::getLogoFileId)
.flatMap((logoFileId) -> scriptAssetFileRepository.findById(logoFileId))
.flatMap((file) ->
assetStorageService.loadAssetFileSafely(file.getBroadcaster(), file.getId(), file.getMediaType())
);
if (DefaultMarketplaceScript.matches(scriptId)) {
return DefaultMarketplaceScript.logoContent();
}
try {
return scriptAssetRepository
.findById(scriptId)
.filter(ScriptAsset::isPublic)
.map(ScriptAsset::getLogoFileId)
.flatMap((logoFileId) -> scriptAssetFileRepository.findById(logoFileId))
.flatMap((file) ->
assetStorageService.loadAssetFileSafely(file.getBroadcaster(), file.getId(), file.getMediaType())
);
} catch (DataAccessException ex) {
logger.warn("Unable to load marketplace logo", ex);
return Optional.empty();
}
}
public Optional<AssetView> importMarketplaceScript(String targetBroadcaster, String scriptId) {
ScriptAsset sourceScript = scriptAssetRepository
.findById(scriptId)
.filter(ScriptAsset::isPublic)
.orElse(null);
if (DefaultMarketplaceScript.matches(scriptId)) {
return importDefaultMarketplaceScript(targetBroadcaster);
}
ScriptAsset sourceScript;
try {
sourceScript = scriptAssetRepository.findById(scriptId).filter(ScriptAsset::isPublic).orElse(null);
} catch (DataAccessException ex) {
logger.warn("Unable to import marketplace script {}", scriptId, ex);
return Optional.empty();
}
Asset sourceAsset = sourceScript == null ? null : assetRepository.findById(scriptId).orElse(null);
if (sourceScript == null || sourceAsset == null) {
return Optional.empty();
@@ -538,6 +573,69 @@ public class ChannelDirectoryService {
return Optional.of(view);
}
private Optional<AssetView> importDefaultMarketplaceScript(String targetBroadcaster) {
AssetContent sourceContent = DefaultMarketplaceScript.sourceContent().orElse(null);
AssetContent attachmentContent = DefaultMarketplaceScript.attachmentContent().orElse(null);
if (sourceContent == null || attachmentContent == null) {
return Optional.empty();
}
Asset asset = new Asset(targetBroadcaster, AssetType.SCRIPT);
ScriptAssetFile sourceFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT);
sourceFile.setId(asset.getId());
sourceFile.setMediaType(sourceContent.mediaType());
sourceFile.setOriginalMediaType(sourceContent.mediaType());
try {
assetStorageService.storeAsset(
sourceFile.getBroadcaster(),
sourceFile.getId(),
sourceContent.bytes(),
sourceContent.mediaType()
);
} catch (IOException e) {
throw new ResponseStatusException(BAD_REQUEST, "Unable to store custom script", e);
}
assetRepository.save(asset);
scriptAssetFileRepository.save(sourceFile);
ScriptAssetFile attachmentFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.IMAGE);
attachmentFile.setMediaType(attachmentContent.mediaType());
attachmentFile.setOriginalMediaType(attachmentContent.mediaType());
try {
assetStorageService.storeAsset(
attachmentFile.getBroadcaster(),
attachmentFile.getId(),
attachmentContent.bytes(),
attachmentContent.mediaType()
);
} catch (IOException e) {
throw new ResponseStatusException(BAD_REQUEST, "Unable to store script attachment", e);
}
scriptAssetFileRepository.save(attachmentFile);
ScriptAsset script = new ScriptAsset(asset.getId(), DefaultMarketplaceScript.SCRIPT_NAME);
script.setDescription(DefaultMarketplaceScript.SCRIPT_DESCRIPTION);
script.setPublic(false);
script.setMediaType(sourceContent.mediaType());
script.setOriginalMediaType(sourceContent.mediaType());
script.setSourceFileId(sourceFile.getId());
script.setLogoFileId(attachmentFile.getId());
script.setAttachments(List.of());
scriptAssetRepository.save(script);
ScriptAssetAttachment attachment = new ScriptAssetAttachment(asset.getId(), DefaultMarketplaceScript.ATTACHMENT_NAME);
attachment.setFileId(attachmentFile.getId());
attachment.setMediaType(attachmentContent.mediaType());
attachment.setOriginalMediaType(attachmentContent.mediaType());
attachment.setAssetType(AssetType.IMAGE);
scriptAssetAttachmentRepository.save(attachment);
script.setAttachments(loadScriptAttachments(asset.getBroadcaster(), asset.getId(), null));
AssetView view = AssetView.fromScript(asset.getBroadcaster(), asset, script);
messagingTemplate.convertAndSend(topicFor(targetBroadcaster), AssetEvent.created(targetBroadcaster, view));
return Optional.of(view);
}
private String sanitizeFilename(String original) {
String stripped = original.replaceAll("^.*[/\\\\]", "");
return SAFE_FILENAME.matcher(stripped).replaceAll("_");
@@ -750,6 +848,7 @@ public class ChannelDirectoryService {
});
}
@Transactional
public boolean deleteAsset(String assetId) {
return assetRepository
.findById(assetId)

View File

@@ -0,0 +1,105 @@
package dev.kruhlmann.imgfloat.service;
import dev.kruhlmann.imgfloat.model.ScriptMarketplaceEntry;
import dev.kruhlmann.imgfloat.service.media.AssetContent;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
public final class DefaultMarketplaceScript {
public static final String SCRIPT_ID = "imgfloat-default-rotating-logo";
public static final String SCRIPT_NAME = "Rotating Imgfloat logo";
public static final String SCRIPT_DESCRIPTION =
"Renders the Imgfloat logo and rotates it every tick.";
public static final String SCRIPT_BROADCASTER = "Imgfloat";
public static final String ATTACHMENT_NAME = "Imgfloat logo";
public static final String LOGO_URL = "/api/marketplace/scripts/" + SCRIPT_ID + "/logo";
public static final String SOURCE_MEDIA_TYPE = "application/javascript";
public static final String LOGO_MEDIA_TYPE = "image/png";
private static final Logger logger = LoggerFactory.getLogger(DefaultMarketplaceScript.class);
private static final String LOGO_RESOURCE = "static/img/brand.png";
private static final String SOURCE_RESOURCE = "assets/default-marketplace-script.js";
private static final AtomicReference<byte[]> LOGO_BYTES = new AtomicReference<>();
private static final AtomicReference<byte[]> SOURCE_BYTES = new AtomicReference<>();
private DefaultMarketplaceScript() {}
public static boolean matches(String scriptId) {
return SCRIPT_ID.equals(scriptId);
}
public static Optional<ScriptMarketplaceEntry> entryForQuery(String query) {
if (query == null || query.isBlank()) {
return Optional.of(entry());
}
String normalized = query.toLowerCase(Locale.ROOT);
if (
SCRIPT_NAME.toLowerCase(Locale.ROOT).contains(normalized) ||
SCRIPT_DESCRIPTION.toLowerCase(Locale.ROOT).contains(normalized)
) {
return Optional.of(entry());
}
return Optional.empty();
}
public static ScriptMarketplaceEntry entry() {
return new ScriptMarketplaceEntry(
SCRIPT_ID,
SCRIPT_NAME,
SCRIPT_DESCRIPTION,
LOGO_URL,
SCRIPT_BROADCASTER
);
}
public static Optional<AssetContent> logoContent() {
return loadContent(LOGO_BYTES, LOGO_RESOURCE, LOGO_MEDIA_TYPE);
}
public static Optional<AssetContent> sourceContent() {
return loadContent(SOURCE_BYTES, SOURCE_RESOURCE, SOURCE_MEDIA_TYPE);
}
public static Optional<AssetContent> attachmentContent() {
return logoContent();
}
private static Optional<AssetContent> loadContent(
AtomicReference<byte[]> cache,
String resourcePath,
String mediaType
) {
byte[] bytes = cache.get();
if (bytes == null) {
bytes = readBytes(resourcePath).orElse(null);
if (bytes != null) {
cache.set(bytes);
}
}
if (bytes == null) {
return Optional.empty();
}
return Optional.of(new AssetContent(bytes, mediaType));
}
private static Optional<byte[]> readBytes(String resourcePath) {
ClassPathResource resource = new ClassPathResource(resourcePath);
if (!resource.exists()) {
logger.warn("Default marketplace resource {} is missing", resourcePath);
return Optional.empty();
}
try (InputStream input = resource.getInputStream()) {
return Optional.of(input.readAllBytes());
} catch (IOException ex) {
logger.warn("Failed to read default marketplace resource {}", resourcePath, ex);
return Optional.empty();
}
}
}