mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Add seeding to marketplace
This commit is contained in:
@@ -35,10 +35,13 @@ import dev.kruhlmann.imgfloat.service.media.MediaDetectionService;
|
||||
import dev.kruhlmann.imgfloat.service.media.MediaOptimizationService;
|
||||
import dev.kruhlmann.imgfloat.service.media.OptimizedAsset;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
@@ -69,6 +72,7 @@ public class ChannelDirectoryService {
|
||||
private final MediaOptimizationService mediaOptimizationService;
|
||||
private final SettingsService settingsService;
|
||||
private final long uploadLimitBytes;
|
||||
private final MarketplaceScriptSeedLoader marketplaceScriptSeedLoader;
|
||||
|
||||
@Autowired
|
||||
public ChannelDirectoryService(
|
||||
@@ -84,7 +88,8 @@ public class ChannelDirectoryService {
|
||||
MediaDetectionService mediaDetectionService,
|
||||
MediaOptimizationService mediaOptimizationService,
|
||||
SettingsService settingsService,
|
||||
long uploadLimitBytes
|
||||
long uploadLimitBytes,
|
||||
MarketplaceScriptSeedLoader marketplaceScriptSeedLoader
|
||||
) {
|
||||
this.channelRepository = channelRepository;
|
||||
this.assetRepository = assetRepository;
|
||||
@@ -99,6 +104,7 @@ public class ChannelDirectoryService {
|
||||
this.mediaOptimizationService = mediaOptimizationService;
|
||||
this.settingsService = settingsService;
|
||||
this.uploadLimitBytes = uploadLimitBytes;
|
||||
this.marketplaceScriptSeedLoader = marketplaceScriptSeedLoader;
|
||||
}
|
||||
|
||||
public Channel getOrCreateChannel(String broadcaster) {
|
||||
@@ -426,8 +432,7 @@ public class ChannelDirectoryService {
|
||||
public List<ScriptMarketplaceEntry> listMarketplaceScripts(String query) {
|
||||
String q = normalizeDescription(query);
|
||||
String normalizedQuery = q == null ? null : q.toLowerCase(Locale.ROOT);
|
||||
List<ScriptMarketplaceEntry> entries = new ArrayList<>();
|
||||
DefaultMarketplaceScript.entryForQuery(normalizedQuery).ifPresent(entries::add);
|
||||
List<ScriptMarketplaceEntry> entries = new ArrayList<>(marketplaceScriptSeedLoader.listEntriesForQuery(normalizedQuery));
|
||||
List<ScriptAsset> scripts;
|
||||
try {
|
||||
scripts = scriptAssetRepository.findByIsPublicTrue();
|
||||
@@ -484,8 +489,9 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
|
||||
public Optional<AssetContent> getMarketplaceLogo(String scriptId) {
|
||||
if (DefaultMarketplaceScript.matches(scriptId)) {
|
||||
return DefaultMarketplaceScript.logoContent();
|
||||
Optional<MarketplaceScriptSeedLoader.SeedScript> seedScript = marketplaceScriptSeedLoader.findById(scriptId);
|
||||
if (seedScript.isPresent()) {
|
||||
return seedScript.get().loadLogo();
|
||||
}
|
||||
try {
|
||||
return scriptAssetRepository
|
||||
@@ -503,8 +509,9 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
|
||||
public Optional<AssetView> importMarketplaceScript(String targetBroadcaster, String scriptId) {
|
||||
if (DefaultMarketplaceScript.matches(scriptId)) {
|
||||
return importDefaultMarketplaceScript(targetBroadcaster);
|
||||
Optional<MarketplaceScriptSeedLoader.SeedScript> seedScript = marketplaceScriptSeedLoader.findById(scriptId);
|
||||
if (seedScript.isPresent()) {
|
||||
return importSeedMarketplaceScript(targetBroadcaster, seedScript.get());
|
||||
}
|
||||
ScriptAsset sourceScript;
|
||||
try {
|
||||
@@ -573,10 +580,12 @@ 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) {
|
||||
private Optional<AssetView> importSeedMarketplaceScript(
|
||||
String targetBroadcaster,
|
||||
MarketplaceScriptSeedLoader.SeedScript seedScript
|
||||
) {
|
||||
AssetContent sourceContent = seedScript.loadSource().orElse(null);
|
||||
if (sourceContent == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -598,6 +607,72 @@ public class ChannelDirectoryService {
|
||||
assetRepository.save(asset);
|
||||
scriptAssetFileRepository.save(sourceFile);
|
||||
|
||||
ScriptAsset script = new ScriptAsset(asset.getId(), seedScript.name());
|
||||
script.setDescription(seedScript.description());
|
||||
script.setPublic(false);
|
||||
script.setMediaType(sourceContent.mediaType());
|
||||
script.setOriginalMediaType(sourceContent.mediaType());
|
||||
script.setSourceFileId(sourceFile.getId());
|
||||
script.setAttachments(List.of());
|
||||
scriptAssetRepository.save(script);
|
||||
|
||||
String logoFileId = seedScript
|
||||
.loadLogo()
|
||||
.map((logoContent) -> storeScriptAttachmentFile(asset, logoContent))
|
||||
.orElse(null);
|
||||
|
||||
List<ScriptAssetAttachment> attachments = new ArrayList<>();
|
||||
if (logoFileId != null) {
|
||||
script.setLogoFileId(logoFileId);
|
||||
}
|
||||
for (MarketplaceScriptSeedLoader.SeedAttachment attachment : seedScript.attachments()) {
|
||||
AssetContent attachmentContent = loadSeedAttachment(attachment).orElse(null);
|
||||
if (attachmentContent == null) {
|
||||
continue;
|
||||
}
|
||||
String attachmentFileId = storeScriptAttachmentFile(asset, attachmentContent);
|
||||
ScriptAssetAttachment scriptAttachment = new ScriptAssetAttachment(asset.getId(), attachment.name());
|
||||
scriptAttachment.setFileId(attachmentFileId);
|
||||
scriptAttachment.setMediaType(attachmentContent.mediaType());
|
||||
scriptAttachment.setOriginalMediaType(attachmentContent.mediaType());
|
||||
scriptAttachment.setAssetType(AssetType.IMAGE);
|
||||
attachments.add(scriptAttachment);
|
||||
}
|
||||
if (!attachments.isEmpty()) {
|
||||
scriptAssetAttachmentRepository.saveAll(attachments);
|
||||
}
|
||||
|
||||
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 Optional<AssetContent> loadSeedAttachment(MarketplaceScriptSeedLoader.SeedAttachment attachment) {
|
||||
AtomicReference<byte[]> cache = attachment.bytes();
|
||||
byte[] bytes = cache.get();
|
||||
if (bytes == null) {
|
||||
bytes = readSeedAttachment(attachment.path()).orElse(null);
|
||||
if (bytes != null) {
|
||||
cache.set(bytes);
|
||||
}
|
||||
}
|
||||
if (bytes == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new AssetContent(bytes, attachment.mediaType()));
|
||||
}
|
||||
|
||||
private Optional<byte[]> readSeedAttachment(Path path) {
|
||||
try {
|
||||
return Optional.of(Files.readAllBytes(path));
|
||||
} catch (IOException ex) {
|
||||
logger.warn("Failed to read marketplace attachment {}", path, ex);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private String storeScriptAttachmentFile(Asset asset, AssetContent attachmentContent) {
|
||||
ScriptAssetFile attachmentFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.IMAGE);
|
||||
attachmentFile.setMediaType(attachmentContent.mediaType());
|
||||
attachmentFile.setOriginalMediaType(attachmentContent.mediaType());
|
||||
@@ -612,28 +687,7 @@ public class ChannelDirectoryService {
|
||||
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);
|
||||
return attachmentFile.getId();
|
||||
}
|
||||
|
||||
private String sanitizeFilename(String original) {
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package dev.kruhlmann.imgfloat.service;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.ScriptMarketplaceEntry;
|
||||
import dev.kruhlmann.imgfloat.service.media.AssetContent;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class MarketplaceScriptSeedLoader {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MarketplaceScriptSeedLoader.class);
|
||||
private static final String METADATA_FILENAME = "metadata.json";
|
||||
private static final String SOURCE_FILENAME = "source.js";
|
||||
private static final String LOGO_FILENAME = "logo.png";
|
||||
private static final String ATTACHMENTS_DIR = "attachments";
|
||||
private static final String DEFAULT_SOURCE_MEDIA_TYPE = "application/javascript";
|
||||
private static final String DEFAULT_LOGO_MEDIA_TYPE = "image/png";
|
||||
|
||||
private final List<SeedScript> scripts;
|
||||
|
||||
public MarketplaceScriptSeedLoader(@Value("${IMGFLOAT_MARKETPLACE_SCRIPTS_PATH:#{null}}") String rootPath) {
|
||||
this.scripts = loadScripts(resolveRootPath(rootPath));
|
||||
}
|
||||
|
||||
public List<ScriptMarketplaceEntry> listEntriesForQuery(String query) {
|
||||
if (scripts.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
String normalized = query == null ? null : query.toLowerCase(Locale.ROOT);
|
||||
return scripts
|
||||
.stream()
|
||||
.filter((script) -> script.matchesQuery(normalized))
|
||||
.map(SeedScript::entry)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public Optional<SeedScript> findById(String scriptId) {
|
||||
return scripts.stream().filter((script) -> script.id().equals(scriptId)).findFirst();
|
||||
}
|
||||
|
||||
public record SeedScript(
|
||||
String id,
|
||||
String name,
|
||||
String description,
|
||||
String broadcaster,
|
||||
String sourceMediaType,
|
||||
String logoMediaType,
|
||||
Optional<Path> sourcePath,
|
||||
Optional<Path> logoPath,
|
||||
List<SeedAttachment> attachments,
|
||||
AtomicReference<byte[]> sourceBytes,
|
||||
AtomicReference<byte[]> logoBytes
|
||||
) {
|
||||
ScriptMarketplaceEntry entry() {
|
||||
return new ScriptMarketplaceEntry(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
logoPath.isPresent() ? "/api/marketplace/scripts/" + id + "/logo" : null,
|
||||
broadcaster
|
||||
);
|
||||
}
|
||||
|
||||
boolean matchesQuery(String normalized) {
|
||||
if (normalized == null || normalized.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
String entryName = Optional.ofNullable(name).orElse("").toLowerCase(Locale.ROOT);
|
||||
String entryDescription = Optional.ofNullable(description).orElse("").toLowerCase(Locale.ROOT);
|
||||
return entryName.contains(normalized) || entryDescription.contains(normalized);
|
||||
}
|
||||
|
||||
Optional<AssetContent> loadSource() {
|
||||
return sourcePath.flatMap((path) -> loadContent(sourceBytes, path, sourceMediaType));
|
||||
}
|
||||
|
||||
Optional<AssetContent> loadLogo() {
|
||||
return logoPath.flatMap((path) -> loadContent(logoBytes, path, logoMediaType));
|
||||
}
|
||||
}
|
||||
|
||||
public record SeedAttachment(String name, String mediaType, Path path, AtomicReference<byte[]> bytes) {}
|
||||
|
||||
private List<SeedScript> loadScripts(Path rootPath) {
|
||||
if (rootPath == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (!Files.isDirectory(rootPath)) {
|
||||
logger.warn("Marketplace script path {} is not a directory", rootPath);
|
||||
return List.of();
|
||||
}
|
||||
List<SeedScript> loaded = new ArrayList<>();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(rootPath)) {
|
||||
for (Path scriptDir : stream) {
|
||||
if (!Files.isDirectory(scriptDir)) {
|
||||
continue;
|
||||
}
|
||||
SeedScript script = loadScriptDirectory(scriptDir).orElse(null);
|
||||
if (script != null) {
|
||||
loaded.add(script);
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
logger.warn("Failed to read marketplace script directory {}", rootPath, ex);
|
||||
}
|
||||
return List.copyOf(loaded);
|
||||
}
|
||||
|
||||
private Optional<SeedScript> loadScriptDirectory(Path scriptDir) {
|
||||
ScriptSeedMetadata metadata = ScriptSeedMetadata.read(scriptDir.resolve(METADATA_FILENAME));
|
||||
if (metadata == null) {
|
||||
logger.warn("Skipping marketplace script {}, missing {}", scriptDir, METADATA_FILENAME);
|
||||
return Optional.empty();
|
||||
}
|
||||
if (metadata.name() == null || metadata.name().isBlank()) {
|
||||
logger.warn("Skipping marketplace script {}, missing name", scriptDir);
|
||||
return Optional.empty();
|
||||
}
|
||||
String sourceMediaType = detectMediaType(scriptDir.resolve(SOURCE_FILENAME), DEFAULT_SOURCE_MEDIA_TYPE);
|
||||
String logoMediaType = detectMediaType(scriptDir.resolve(LOGO_FILENAME), DEFAULT_LOGO_MEDIA_TYPE);
|
||||
String broadcaster = normalizeBroadcaster(metadata.broadcaster());
|
||||
|
||||
Path sourcePath = resolveOptionalFile(scriptDir.resolve(SOURCE_FILENAME));
|
||||
Path logoPath = resolveOptionalFile(scriptDir.resolve(LOGO_FILENAME));
|
||||
if (sourcePath == null) {
|
||||
logger.warn("Skipping marketplace script {}, missing {}", scriptDir, SOURCE_FILENAME);
|
||||
return Optional.empty();
|
||||
}
|
||||
List<SeedAttachment> attachments = loadAttachments(scriptDir.resolve(ATTACHMENTS_DIR)).orElse(null);
|
||||
if (attachments == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(
|
||||
new SeedScript(
|
||||
scriptDir.getFileName().toString(),
|
||||
metadata.name(),
|
||||
metadata.description(),
|
||||
broadcaster,
|
||||
sourceMediaType,
|
||||
logoMediaType,
|
||||
Optional.ofNullable(sourcePath),
|
||||
Optional.ofNullable(logoPath),
|
||||
attachments,
|
||||
new AtomicReference<>(),
|
||||
new AtomicReference<>()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private Optional<List<SeedAttachment>> loadAttachments(Path attachmentsDir) {
|
||||
if (!Files.isDirectory(attachmentsDir)) {
|
||||
return Optional.of(List.of());
|
||||
}
|
||||
List<SeedAttachment> attachments = new ArrayList<>();
|
||||
Set<String> seenNames = new HashSet<>();
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(attachmentsDir)) {
|
||||
for (Path attachment : stream) {
|
||||
if (Files.isRegularFile(attachment)) {
|
||||
String name = attachment.getFileName().toString();
|
||||
if (!seenNames.add(name)) {
|
||||
logger.warn("Duplicate marketplace attachment name {}", name);
|
||||
return Optional.empty();
|
||||
}
|
||||
String mediaType = Files.probeContentType(attachment);
|
||||
attachments.add(
|
||||
new SeedAttachment(
|
||||
name,
|
||||
mediaType == null ? "application/octet-stream" : mediaType,
|
||||
attachment,
|
||||
new AtomicReference<>()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
logger.warn("Failed to read marketplace attachments in {}", attachmentsDir, ex);
|
||||
}
|
||||
return Optional.of(List.copyOf(attachments));
|
||||
}
|
||||
|
||||
private Path resolveRootPath(String rootPath) {
|
||||
if (rootPath != null && !rootPath.isBlank()) {
|
||||
return Path.of(rootPath);
|
||||
}
|
||||
Path devPath = Path.of("dev", "marketplace-scripts");
|
||||
if (Files.isDirectory(devPath)) {
|
||||
return devPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String normalizeBroadcaster(String broadcaster) {
|
||||
if (broadcaster == null || broadcaster.isBlank()) {
|
||||
return "System";
|
||||
}
|
||||
return broadcaster;
|
||||
}
|
||||
|
||||
private static Path resolveOptionalFile(Path path) {
|
||||
if (Files.isRegularFile(path)) {
|
||||
return path;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Optional<AssetContent> loadContent(
|
||||
AtomicReference<byte[]> cache,
|
||||
Path filePath,
|
||||
String mediaType
|
||||
) {
|
||||
byte[] bytes = cache.get();
|
||||
if (bytes == null) {
|
||||
bytes = readBytes(filePath).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(Path filePath) {
|
||||
if (!Files.isRegularFile(filePath)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.of(Files.readAllBytes(filePath));
|
||||
} catch (IOException ex) {
|
||||
logger.warn("Failed to read marketplace script asset {}", filePath, ex);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
record ScriptSeedMetadata(String name, String description, String broadcaster) {
|
||||
static ScriptSeedMetadata read(Path path) {
|
||||
if (!Files.isRegularFile(path)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String content = Files.readString(path);
|
||||
return JsonSupport.read(content, ScriptSeedMetadata.class);
|
||||
} catch (IOException ex) {
|
||||
logger.warn("Failed to read marketplace metadata {}", path, ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class JsonSupport {
|
||||
private static final AtomicReference<com.fasterxml.jackson.databind.ObjectMapper> OBJECT_MAPPER =
|
||||
new AtomicReference<>();
|
||||
|
||||
private JsonSupport() {}
|
||||
|
||||
static <T> T read(String payload, Class<T> type) throws IOException {
|
||||
return mapper().readValue(payload, type);
|
||||
}
|
||||
|
||||
private static com.fasterxml.jackson.databind.ObjectMapper mapper() {
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper = OBJECT_MAPPER.get();
|
||||
if (mapper == null) {
|
||||
mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
OBJECT_MAPPER.set(mapper);
|
||||
}
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
|
||||
private String detectMediaType(Path path, String fallback) {
|
||||
if (!Files.isRegularFile(path)) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
String mediaType = Files.probeContentType(path);
|
||||
return mediaType == null ? fallback : mediaType;
|
||||
} catch (IOException ex) {
|
||||
logger.warn("Failed to detect media type for {}", path, ex);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user