mirror of
https://github.com/imgfloat/server.git
synced 2026-05-08 10:19:35 +00:00
fix: update test to use MarketplaceService.listScripts after method was moved
This commit is contained in:
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
if [[ ! -d "/home/ges/doc/src/github.com/imgfloat/server" ]]; then
|
||||||
|
echo "Cannot find source directory; Did you move it?"
|
||||||
|
echo "(Looking for "/home/ges/doc/src/github.com/imgfloat/server")"
|
||||||
|
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# rebuild the cache forcefully
|
||||||
|
_nix_direnv_force_reload=1 direnv exec "/home/ges/doc/src/github.com/imgfloat/server" true
|
||||||
|
|
||||||
|
# Update the mtime for .envrc.
|
||||||
|
# This will cause direnv to reload again - but without re-building.
|
||||||
|
touch "/home/ges/doc/src/github.com/imgfloat/server/.envrc"
|
||||||
|
|
||||||
|
# Also update the timestamp of whatever profile_rc we have.
|
||||||
|
# This makes sure that we know we are up to date.
|
||||||
|
touch -r "/home/ges/doc/src/github.com/imgfloat/server/.envrc" "/home/ges/doc/src/github.com/imgfloat/server/.direnv"/*.rc
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
/nix/store/p2i4wywz453chyxw93dp1iq3f0gbskx0-nix-shell-env
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import dev.kruhlmann.imgfloat.model.api.response.ScriptMarketplaceEntry;
|
|||||||
import dev.kruhlmann.imgfloat.model.api.request.ScriptMarketplaceImportRequest;
|
import dev.kruhlmann.imgfloat.model.api.request.ScriptMarketplaceImportRequest;
|
||||||
import dev.kruhlmann.imgfloat.service.AuthorizationService;
|
import dev.kruhlmann.imgfloat.service.AuthorizationService;
|
||||||
import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
|
import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
|
||||||
|
import dev.kruhlmann.imgfloat.service.MarketplaceService;
|
||||||
import dev.kruhlmann.imgfloat.util.LogSanitizer;
|
import dev.kruhlmann.imgfloat.util.LogSanitizer;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -34,13 +35,16 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
public class ScriptMarketplaceController {
|
public class ScriptMarketplaceController {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(ScriptMarketplaceController.class);
|
private static final Logger LOG = LoggerFactory.getLogger(ScriptMarketplaceController.class);
|
||||||
|
private final MarketplaceService marketplaceService;
|
||||||
private final ChannelDirectoryService channelDirectoryService;
|
private final ChannelDirectoryService channelDirectoryService;
|
||||||
private final AuthorizationService authorizationService;
|
private final AuthorizationService authorizationService;
|
||||||
|
|
||||||
public ScriptMarketplaceController(
|
public ScriptMarketplaceController(
|
||||||
|
MarketplaceService marketplaceService,
|
||||||
ChannelDirectoryService channelDirectoryService,
|
ChannelDirectoryService channelDirectoryService,
|
||||||
AuthorizationService authorizationService
|
AuthorizationService authorizationService
|
||||||
) {
|
) {
|
||||||
|
this.marketplaceService = marketplaceService;
|
||||||
this.channelDirectoryService = channelDirectoryService;
|
this.channelDirectoryService = channelDirectoryService;
|
||||||
this.authorizationService = authorizationService;
|
this.authorizationService = authorizationService;
|
||||||
}
|
}
|
||||||
@@ -51,15 +55,15 @@ public class ScriptMarketplaceController {
|
|||||||
OAuth2AuthenticationToken oauthToken
|
OAuth2AuthenticationToken oauthToken
|
||||||
) {
|
) {
|
||||||
String sessionUsername = oauthToken == null ? null : OauthSessionUser.from(oauthToken).login();
|
String sessionUsername = oauthToken == null ? null : OauthSessionUser.from(oauthToken).login();
|
||||||
return channelDirectoryService.listMarketplaceScripts(query, sessionUsername);
|
return marketplaceService.listScripts(query, sessionUsername);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/scripts/{scriptId}/logo")
|
@GetMapping("/scripts/{scriptId}/logo")
|
||||||
public ResponseEntity<byte[]> getMarketplaceLogo(@PathVariable("scriptId") String scriptId) {
|
public ResponseEntity<byte[]> getMarketplaceLogo(@PathVariable("scriptId") String scriptId) {
|
||||||
String logScriptId = LogSanitizer.sanitize(scriptId);
|
String logScriptId = LogSanitizer.sanitize(scriptId);
|
||||||
LOG.debug("Serving marketplace logo for script {}", logScriptId);
|
LOG.debug("Serving marketplace logo for script {}", logScriptId);
|
||||||
return channelDirectoryService
|
return marketplaceService
|
||||||
.getMarketplaceLogo(scriptId)
|
.getLogo(scriptId)
|
||||||
.map((content) ->
|
.map((content) ->
|
||||||
ResponseEntity.ok()
|
ResponseEntity.ok()
|
||||||
.header("X-Content-Type-Options", "nosniff")
|
.header("X-Content-Type-Options", "nosniff")
|
||||||
@@ -96,8 +100,8 @@ public class ScriptMarketplaceController {
|
|||||||
OAuth2AuthenticationToken oauthToken
|
OAuth2AuthenticationToken oauthToken
|
||||||
) {
|
) {
|
||||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||||
return channelDirectoryService
|
return marketplaceService
|
||||||
.toggleMarketplaceHeart(scriptId, sessionUsername)
|
.toggleHeart(scriptId, sessionUsername)
|
||||||
.map(ResponseEntity::ok)
|
.map(ResponseEntity::ok)
|
||||||
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Marketplace script not found"));
|
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Marketplace script not found"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -483,215 +483,6 @@ public class ChannelDirectoryService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ScriptMarketplaceEntry> listMarketplaceScripts(String query, String sessionUsername) {
|
|
||||||
String q = normalizeDescription(query);
|
|
||||||
String normalizedQuery = q == null ? null : q.toLowerCase(Locale.ROOT);
|
|
||||||
List<ScriptMarketplaceEntry> entries = new ArrayList<>(marketplaceScriptSeedLoader.listEntriesForQuery(normalizedQuery));
|
|
||||||
List<ScriptAsset> scripts;
|
|
||||||
try {
|
|
||||||
scripts = scriptAssetRepository.findByIsPublicTrue();
|
|
||||||
} catch (DataAccessException ex) {
|
|
||||||
logger.warn("Unable to load marketplace scripts", ex);
|
|
||||||
return applyMarketplaceHearts(entries, sessionUsername);
|
|
||||||
}
|
|
||||||
if (normalizedQuery != null && !normalizedQuery.isBlank()) {
|
|
||||||
scripts =
|
|
||||||
scripts
|
|
||||||
.stream()
|
|
||||||
.filter((script) -> {
|
|
||||||
String name = Optional.ofNullable(script.getName()).orElse("");
|
|
||||||
String description = Optional.ofNullable(script.getDescription()).orElse("");
|
|
||||||
return name.toLowerCase(Locale.ROOT).contains(normalizedQuery) ||
|
|
||||||
description.toLowerCase(Locale.ROOT).contains(normalizedQuery);
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
Map<String, Asset> assets = assetRepository
|
|
||||||
.findAllById(scripts.stream().map(ScriptAsset::getId).toList())
|
|
||||||
.stream()
|
|
||||||
.collect(Collectors.toMap(Asset::getId, (asset) -> asset));
|
|
||||||
|
|
||||||
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,
|
|
||||||
normalizeAllowedDomainsLenient(script.getAllowedDomains()),
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.toList()
|
|
||||||
);
|
|
||||||
|
|
||||||
return applyMarketplaceHearts(entries, sessionUsername);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public Optional<ScriptMarketplaceEntry> toggleMarketplaceHeart(String scriptId, String sessionUsername) {
|
|
||||||
if (scriptId == null || scriptId.isBlank() || sessionUsername == null || sessionUsername.isBlank()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (marketplaceScriptHeartRepository.existsByScriptIdAndUsername(scriptId, sessionUsername)) {
|
|
||||||
marketplaceScriptHeartRepository.deleteByScriptIdAndUsername(scriptId, sessionUsername);
|
|
||||||
} else {
|
|
||||||
marketplaceScriptHeartRepository.save(new MarketplaceScriptHeart(scriptId, sessionUsername));
|
|
||||||
}
|
|
||||||
} catch (DataAccessException ex) {
|
|
||||||
logger.warn("Unable to update marketplace heart for {}", scriptId, ex);
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return loadMarketplaceEntryWithHearts(scriptId, sessionUsername);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<ScriptMarketplaceEntry> loadMarketplaceEntryWithHearts(String scriptId, String sessionUsername) {
|
|
||||||
Optional<MarketplaceScriptSeedLoader.SeedScript> seedScript = marketplaceScriptSeedLoader.findById(scriptId);
|
|
||||||
if (seedScript.isPresent()) {
|
|
||||||
return Optional.of(applyMarketplaceHearts(seedScript.get().entry(), sessionUsername));
|
|
||||||
}
|
|
||||||
ScriptAsset script;
|
|
||||||
try {
|
|
||||||
script = scriptAssetRepository.findById(scriptId).filter(ScriptAsset::isPublic).orElse(null);
|
|
||||||
} catch (DataAccessException ex) {
|
|
||||||
logger.warn("Unable to load marketplace script {}", scriptId, ex);
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
if (script == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Asset asset = assetRepository.findById(scriptId).orElse(null);
|
|
||||||
String broadcaster = asset != null ? asset.getBroadcaster() : "";
|
|
||||||
String logoUrl = script.getLogoFileId() == null ? null : "/api/marketplace/scripts/" + script.getId() + "/logo";
|
|
||||||
ScriptMarketplaceEntry entry = new ScriptMarketplaceEntry(
|
|
||||||
script.getId(),
|
|
||||||
script.getName(),
|
|
||||||
script.getDescription(),
|
|
||||||
logoUrl,
|
|
||||||
broadcaster,
|
|
||||||
normalizeAllowedDomainsLenient(script.getAllowedDomains()),
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
return Optional.of(applyMarketplaceHearts(entry, sessionUsername));
|
|
||||||
}
|
|
||||||
|
|
||||||
private ScriptMarketplaceEntry applyMarketplaceHearts(ScriptMarketplaceEntry entry, String sessionUsername) {
|
|
||||||
if (entry == null || entry.id() == null) {
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
long heartCount;
|
|
||||||
boolean hearted;
|
|
||||||
try {
|
|
||||||
heartCount = marketplaceScriptHeartRepository.countByScriptId(entry.id());
|
|
||||||
hearted =
|
|
||||||
sessionUsername != null &&
|
|
||||||
marketplaceScriptHeartRepository.existsByScriptIdAndUsername(entry.id(), sessionUsername);
|
|
||||||
} catch (DataAccessException ex) {
|
|
||||||
logger.warn("Unable to load marketplace heart summary for {}", entry.id(), ex);
|
|
||||||
heartCount = 0;
|
|
||||||
hearted = false;
|
|
||||||
}
|
|
||||||
return new ScriptMarketplaceEntry(
|
|
||||||
entry.id(),
|
|
||||||
entry.name(),
|
|
||||||
entry.description(),
|
|
||||||
entry.logoUrl(),
|
|
||||||
entry.broadcaster(),
|
|
||||||
entry.allowedDomains(),
|
|
||||||
heartCount,
|
|
||||||
hearted
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ScriptMarketplaceEntry> applyMarketplaceHearts(
|
|
||||||
List<ScriptMarketplaceEntry> entries,
|
|
||||||
String sessionUsername
|
|
||||||
) {
|
|
||||||
if (entries == null || entries.isEmpty()) {
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
List<String> scriptIds = entries.stream().map(ScriptMarketplaceEntry::id).filter(Objects::nonNull).toList();
|
|
||||||
Map<String, Long> counts = new HashMap<>();
|
|
||||||
Set<String> heartedIds = new HashSet<>();
|
|
||||||
try {
|
|
||||||
if (!scriptIds.isEmpty()) {
|
|
||||||
counts.putAll(
|
|
||||||
marketplaceScriptHeartRepository
|
|
||||||
.countByScriptIds(scriptIds)
|
|
||||||
.stream()
|
|
||||||
.collect(
|
|
||||||
Collectors.toMap(
|
|
||||||
MarketplaceScriptHeartRepository.ScriptHeartCount::getScriptId,
|
|
||||||
MarketplaceScriptHeartRepository.ScriptHeartCount::getHeartCount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (sessionUsername != null && !sessionUsername.isBlank()) {
|
|
||||||
heartedIds.addAll(
|
|
||||||
marketplaceScriptHeartRepository
|
|
||||||
.findByUsernameAndScriptIdIn(sessionUsername, scriptIds)
|
|
||||||
.stream()
|
|
||||||
.map(MarketplaceScriptHeart::getScriptId)
|
|
||||||
.collect(Collectors.toSet())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (DataAccessException ex) {
|
|
||||||
logger.warn("Unable to load marketplace heart summaries", ex);
|
|
||||||
}
|
|
||||||
Comparator<ScriptMarketplaceEntry> comparator = Comparator
|
|
||||||
.comparingLong((ScriptMarketplaceEntry entry) -> counts.getOrDefault(entry.id(), 0L))
|
|
||||||
.reversed()
|
|
||||||
.thenComparing(ScriptMarketplaceEntry::name, Comparator.nullsLast(String::compareToIgnoreCase));
|
|
||||||
return entries
|
|
||||||
.stream()
|
|
||||||
.map((entry) ->
|
|
||||||
new ScriptMarketplaceEntry(
|
|
||||||
entry.id(),
|
|
||||||
entry.name(),
|
|
||||||
entry.description(),
|
|
||||||
entry.logoUrl(),
|
|
||||||
entry.broadcaster(),
|
|
||||||
entry.allowedDomains(),
|
|
||||||
counts.getOrDefault(entry.id(), 0L),
|
|
||||||
heartedIds.contains(entry.id())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.sorted(comparator)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Optional<AssetContent> getMarketplaceLogo(String scriptId) {
|
|
||||||
Optional<MarketplaceScriptSeedLoader.SeedScript> seedScript = marketplaceScriptSeedLoader.findById(scriptId);
|
|
||||||
if (seedScript.isPresent()) {
|
|
||||||
return seedScript.get().loadLogo();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return scriptAssetRepository
|
|
||||||
.findById(scriptId)
|
|
||||||
.filter(ScriptAsset::isPublic)
|
|
||||||
.map(ScriptAsset::getLogoFileId)
|
|
||||||
.flatMap(scriptAssetFileRepository::findById)
|
|
||||||
.flatMap((file) ->
|
|
||||||
assetStorageService.loadAssetFileSafely(file.getBroadcaster(), file.getId(), file.getMediaType())
|
|
||||||
);
|
|
||||||
} catch (DataAccessException ex) {
|
|
||||||
logger.warn("Unable to load marketplace logo", ex);
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Optional<AssetView> importMarketplaceScript(String targetBroadcaster, String scriptId, String actor) {
|
public Optional<AssetView> importMarketplaceScript(String targetBroadcaster, String scriptId, String actor) {
|
||||||
Optional<MarketplaceScriptSeedLoader.SeedScript> seedScript = marketplaceScriptSeedLoader.findById(scriptId);
|
Optional<MarketplaceScriptSeedLoader.SeedScript> seedScript = marketplaceScriptSeedLoader.findById(scriptId);
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package dev.kruhlmann.imgfloat.service;
|
||||||
|
|
||||||
|
import dev.kruhlmann.imgfloat.model.api.response.ScriptMarketplaceEntry;
|
||||||
|
import dev.kruhlmann.imgfloat.model.db.imgfloat.Asset;
|
||||||
|
import dev.kruhlmann.imgfloat.model.db.imgfloat.MarketplaceScriptHeart;
|
||||||
|
import dev.kruhlmann.imgfloat.model.db.imgfloat.ScriptAsset;
|
||||||
|
import dev.kruhlmann.imgfloat.model.db.imgfloat.ScriptAssetFile;
|
||||||
|
import dev.kruhlmann.imgfloat.repository.AssetRepository;
|
||||||
|
import dev.kruhlmann.imgfloat.repository.MarketplaceScriptHeartRepository;
|
||||||
|
import dev.kruhlmann.imgfloat.repository.ScriptAssetFileRepository;
|
||||||
|
import dev.kruhlmann.imgfloat.repository.ScriptAssetRepository;
|
||||||
|
import dev.kruhlmann.imgfloat.service.media.AssetContent;
|
||||||
|
import dev.kruhlmann.imgfloat.util.AllowedDomainNormalizer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.dao.DataAccessException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the public marketplace: listing scripts, toggling hearts, and
|
||||||
|
* serving logos. The import workflow (which creates new channel assets)
|
||||||
|
* remains in {@link ChannelDirectoryService} because it shares asset-creation
|
||||||
|
* infrastructure with the rest of that class.
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class MarketplaceService {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MarketplaceService.class);
|
||||||
|
private static final String LOGO_URL_PREFIX = "/api/marketplace/scripts/";
|
||||||
|
|
||||||
|
private final MarketplaceScriptSeedLoader seedLoader;
|
||||||
|
private final ScriptAssetRepository scriptAssetRepository;
|
||||||
|
private final AssetRepository assetRepository;
|
||||||
|
private final ScriptAssetFileRepository scriptAssetFileRepository;
|
||||||
|
private final AssetStorageService assetStorageService;
|
||||||
|
private final MarketplaceScriptHeartRepository heartRepository;
|
||||||
|
|
||||||
|
public MarketplaceService(
|
||||||
|
MarketplaceScriptSeedLoader seedLoader,
|
||||||
|
ScriptAssetRepository scriptAssetRepository,
|
||||||
|
AssetRepository assetRepository,
|
||||||
|
ScriptAssetFileRepository scriptAssetFileRepository,
|
||||||
|
AssetStorageService assetStorageService,
|
||||||
|
MarketplaceScriptHeartRepository heartRepository
|
||||||
|
) {
|
||||||
|
this.seedLoader = seedLoader;
|
||||||
|
this.scriptAssetRepository = scriptAssetRepository;
|
||||||
|
this.assetRepository = assetRepository;
|
||||||
|
this.scriptAssetFileRepository = scriptAssetFileRepository;
|
||||||
|
this.assetStorageService = assetStorageService;
|
||||||
|
this.heartRepository = heartRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ScriptMarketplaceEntry> listScripts(String query, String sessionUsername) {
|
||||||
|
String normalized = query == null ? null : query.strip().toLowerCase(Locale.ROOT);
|
||||||
|
if (normalized != null && normalized.isBlank()) {
|
||||||
|
normalized = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ScriptMarketplaceEntry> entries = new ArrayList<>(seedLoader.listEntriesForQuery(normalized));
|
||||||
|
|
||||||
|
List<ScriptAsset> publicScripts;
|
||||||
|
try {
|
||||||
|
publicScripts = scriptAssetRepository.findByIsPublicTrue();
|
||||||
|
} catch (DataAccessException ex) {
|
||||||
|
LOG.warn("Unable to load marketplace scripts from database", ex);
|
||||||
|
return applyHearts(entries, sessionUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String queryFilter = normalized;
|
||||||
|
if (queryFilter != null) {
|
||||||
|
publicScripts = publicScripts.stream()
|
||||||
|
.filter(script -> {
|
||||||
|
String name = Optional.ofNullable(script.getName()).orElse("");
|
||||||
|
String desc = Optional.ofNullable(script.getDescription()).orElse("");
|
||||||
|
return name.toLowerCase(Locale.ROOT).contains(queryFilter)
|
||||||
|
|| desc.toLowerCase(Locale.ROOT).contains(queryFilter);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Asset> assets = assetRepository
|
||||||
|
.findAllById(publicScripts.stream().map(ScriptAsset::getId).toList())
|
||||||
|
.stream()
|
||||||
|
.collect(Collectors.toMap(Asset::getId, a -> a));
|
||||||
|
|
||||||
|
entries.addAll(
|
||||||
|
publicScripts.stream()
|
||||||
|
.map(script -> toEntry(script, assets.get(script.getId())))
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
|
||||||
|
return applyHearts(entries, sessionUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Optional<ScriptMarketplaceEntry> toggleHeart(String scriptId, String sessionUsername) {
|
||||||
|
if (scriptId == null || scriptId.isBlank() || sessionUsername == null || sessionUsername.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (heartRepository.existsByScriptIdAndUsername(scriptId, sessionUsername)) {
|
||||||
|
heartRepository.deleteByScriptIdAndUsername(scriptId, sessionUsername);
|
||||||
|
} else {
|
||||||
|
heartRepository.save(new MarketplaceScriptHeart(scriptId, sessionUsername));
|
||||||
|
}
|
||||||
|
} catch (DataAccessException ex) {
|
||||||
|
LOG.warn("Unable to update marketplace heart for {}", scriptId, ex);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return loadEntryWithHearts(scriptId, sessionUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<AssetContent> getLogo(String scriptId) {
|
||||||
|
Optional<MarketplaceScriptSeedLoader.SeedScript> seed = seedLoader.findById(scriptId);
|
||||||
|
if (seed.isPresent()) {
|
||||||
|
return seed.get().loadLogo();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return scriptAssetRepository.findById(scriptId)
|
||||||
|
.filter(ScriptAsset::isPublic)
|
||||||
|
.map(ScriptAsset::getLogoFileId)
|
||||||
|
.flatMap(scriptAssetFileRepository::findById)
|
||||||
|
.flatMap(file -> assetStorageService.loadAssetFileSafely(
|
||||||
|
file.getBroadcaster(), file.getId(), file.getMediaType()));
|
||||||
|
} catch (DataAccessException ex) {
|
||||||
|
LOG.warn("Unable to load marketplace logo for script {}", scriptId, ex);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers ----
|
||||||
|
|
||||||
|
private Optional<ScriptMarketplaceEntry> loadEntryWithHearts(String scriptId, String sessionUsername) {
|
||||||
|
Optional<MarketplaceScriptSeedLoader.SeedScript> seed = seedLoader.findById(scriptId);
|
||||||
|
if (seed.isPresent()) {
|
||||||
|
return Optional.of(applyHearts(seed.get().entry(), sessionUsername));
|
||||||
|
}
|
||||||
|
ScriptAsset script;
|
||||||
|
try {
|
||||||
|
script = scriptAssetRepository.findById(scriptId).filter(ScriptAsset::isPublic).orElse(null);
|
||||||
|
} catch (DataAccessException ex) {
|
||||||
|
LOG.warn("Unable to load marketplace script {}", scriptId, ex);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
if (script == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
Asset asset = assetRepository.findById(scriptId).orElse(null);
|
||||||
|
return Optional.of(applyHearts(toEntry(script, asset), sessionUsername));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScriptMarketplaceEntry toEntry(ScriptAsset script, Asset asset) {
|
||||||
|
String broadcaster = asset != null ? asset.getBroadcaster() : "";
|
||||||
|
String logoUrl = script.getLogoFileId() == null
|
||||||
|
? null
|
||||||
|
: LOGO_URL_PREFIX + script.getId() + "/logo";
|
||||||
|
return new ScriptMarketplaceEntry(
|
||||||
|
script.getId(),
|
||||||
|
script.getName(),
|
||||||
|
script.getDescription(),
|
||||||
|
logoUrl,
|
||||||
|
broadcaster,
|
||||||
|
AllowedDomainNormalizer.normalizeLenient(script.getAllowedDomains()),
|
||||||
|
0,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScriptMarketplaceEntry applyHearts(ScriptMarketplaceEntry entry, String sessionUsername) {
|
||||||
|
if (entry == null || entry.id() == null) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
long count;
|
||||||
|
boolean hearted;
|
||||||
|
try {
|
||||||
|
count = heartRepository.countByScriptId(entry.id());
|
||||||
|
hearted = sessionUsername != null
|
||||||
|
&& heartRepository.existsByScriptIdAndUsername(entry.id(), sessionUsername);
|
||||||
|
} catch (DataAccessException ex) {
|
||||||
|
LOG.warn("Unable to load heart summary for script {}", entry.id(), ex);
|
||||||
|
count = 0;
|
||||||
|
hearted = false;
|
||||||
|
}
|
||||||
|
return withHearts(entry, count, hearted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ScriptMarketplaceEntry> applyHearts(List<ScriptMarketplaceEntry> entries, String sessionUsername) {
|
||||||
|
if (entries == null || entries.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<String> ids = entries.stream().map(ScriptMarketplaceEntry::id).filter(Objects::nonNull).toList();
|
||||||
|
Map<String, Long> counts = new HashMap<>();
|
||||||
|
Set<String> heartedIds = new HashSet<>();
|
||||||
|
try {
|
||||||
|
if (!ids.isEmpty()) {
|
||||||
|
counts.putAll(
|
||||||
|
heartRepository.countByScriptIds(ids).stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
MarketplaceScriptHeartRepository.ScriptHeartCount::getScriptId,
|
||||||
|
MarketplaceScriptHeartRepository.ScriptHeartCount::getHeartCount
|
||||||
|
))
|
||||||
|
);
|
||||||
|
if (sessionUsername != null && !sessionUsername.isBlank()) {
|
||||||
|
heartedIds.addAll(
|
||||||
|
heartRepository.findByUsernameAndScriptIdIn(sessionUsername, ids).stream()
|
||||||
|
.map(MarketplaceScriptHeart::getScriptId)
|
||||||
|
.collect(Collectors.toSet())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (DataAccessException ex) {
|
||||||
|
LOG.warn("Unable to load bulk heart summaries", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Comparator<ScriptMarketplaceEntry> byHeartsThenName = Comparator
|
||||||
|
.comparingLong((ScriptMarketplaceEntry e) -> counts.getOrDefault(e.id(), 0L))
|
||||||
|
.reversed()
|
||||||
|
.thenComparing(ScriptMarketplaceEntry::name, Comparator.nullsLast(String::compareToIgnoreCase));
|
||||||
|
|
||||||
|
return entries.stream()
|
||||||
|
.map(e -> withHearts(e, counts.getOrDefault(e.id(), 0L), heartedIds.contains(e.id())))
|
||||||
|
.sorted(byHeartsThenName)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ScriptMarketplaceEntry withHearts(ScriptMarketplaceEntry e, long count, boolean hearted) {
|
||||||
|
return new ScriptMarketplaceEntry(
|
||||||
|
e.id(), e.name(), e.description(), e.logoUrl(), e.broadcaster(),
|
||||||
|
e.allowedDomains(), count, hearted
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,6 +58,7 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
class ChannelDirectoryServiceTest {
|
class ChannelDirectoryServiceTest {
|
||||||
|
|
||||||
private ChannelDirectoryService service;
|
private ChannelDirectoryService service;
|
||||||
|
private dev.kruhlmann.imgfloat.service.MarketplaceService marketplaceService;
|
||||||
private SimpMessagingTemplate messagingTemplate;
|
private SimpMessagingTemplate messagingTemplate;
|
||||||
private ChannelRepository channelRepository;
|
private ChannelRepository channelRepository;
|
||||||
private AssetRepository assetRepository;
|
private AssetRepository assetRepository;
|
||||||
@@ -126,6 +127,14 @@ class ChannelDirectoryServiceTest {
|
|||||||
marketplaceScriptSeedLoader,
|
marketplaceScriptSeedLoader,
|
||||||
auditLogService
|
auditLogService
|
||||||
);
|
);
|
||||||
|
marketplaceService = new dev.kruhlmann.imgfloat.service.MarketplaceService(
|
||||||
|
marketplaceScriptSeedLoader,
|
||||||
|
scriptAssetRepository,
|
||||||
|
assetRepository,
|
||||||
|
scriptAssetFileRepository,
|
||||||
|
assetStorageService,
|
||||||
|
marketplaceScriptHeartRepository
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -215,7 +224,7 @@ class ChannelDirectoryServiceTest {
|
|||||||
void includesDefaultMarketplaceScript() {
|
void includesDefaultMarketplaceScript() {
|
||||||
when(scriptAssetRepository.findByIsPublicTrue()).thenReturn(List.of());
|
when(scriptAssetRepository.findByIsPublicTrue()).thenReturn(List.of());
|
||||||
|
|
||||||
List<dev.kruhlmann.imgfloat.model.api.response.ScriptMarketplaceEntry> entries = service.listMarketplaceScripts(null, null);
|
List<dev.kruhlmann.imgfloat.model.api.response.ScriptMarketplaceEntry> entries = marketplaceService.listScripts(null, null);
|
||||||
|
|
||||||
assertThat(entries)
|
assertThat(entries)
|
||||||
.anyMatch((entry) -> "rotating-logo".equals(entry.id()));
|
.anyMatch((entry) -> "rotating-logo".equals(entry.id()));
|
||||||
|
|||||||
Reference in New Issue
Block a user