mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Asset marketplace
This commit is contained in:
@@ -129,8 +129,12 @@ public class SchemaMigration implements ApplicationRunner {
|
||||
CREATE TABLE IF NOT EXISTS script_assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
is_public BOOLEAN,
|
||||
media_type TEXT,
|
||||
original_media_type TEXT
|
||||
original_media_type TEXT,
|
||||
logo_file_id TEXT,
|
||||
source_file_id TEXT
|
||||
)
|
||||
"""
|
||||
);
|
||||
@@ -139,6 +143,7 @@ public class SchemaMigration implements ApplicationRunner {
|
||||
CREATE TABLE IF NOT EXISTS script_asset_attachments (
|
||||
id TEXT PRIMARY KEY,
|
||||
script_asset_id TEXT NOT NULL,
|
||||
file_id TEXT,
|
||||
name TEXT NOT NULL,
|
||||
media_type TEXT,
|
||||
original_media_type TEXT,
|
||||
@@ -146,12 +151,75 @@ public class SchemaMigration implements ApplicationRunner {
|
||||
)
|
||||
"""
|
||||
);
|
||||
jdbcTemplate.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS script_asset_files (
|
||||
id TEXT PRIMARY KEY,
|
||||
broadcaster TEXT NOT NULL,
|
||||
media_type TEXT,
|
||||
original_media_type TEXT,
|
||||
asset_type TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
);
|
||||
ensureScriptAssetColumns();
|
||||
backfillAssetTypes(assetColumns);
|
||||
} catch (DataAccessException ex) {
|
||||
logger.warn("Unable to ensure asset type tables", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureScriptAssetColumns() {
|
||||
List<String> scriptColumns;
|
||||
List<String> attachmentColumns;
|
||||
try {
|
||||
scriptColumns = jdbcTemplate.query("PRAGMA table_info(script_assets)", (rs, rowNum) -> rs.getString("name"));
|
||||
attachmentColumns =
|
||||
jdbcTemplate.query("PRAGMA table_info(script_asset_attachments)", (rs, rowNum) -> rs.getString("name"));
|
||||
} catch (DataAccessException ex) {
|
||||
logger.warn("Unable to inspect script asset tables", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scriptColumns.isEmpty()) {
|
||||
addColumnIfMissing("script_assets", scriptColumns, "description", "TEXT", "NULL");
|
||||
addColumnIfMissing("script_assets", scriptColumns, "is_public", "BOOLEAN", "0");
|
||||
addColumnIfMissing("script_assets", scriptColumns, "logo_file_id", "TEXT", "NULL");
|
||||
addColumnIfMissing("script_assets", scriptColumns, "source_file_id", "TEXT", "NULL");
|
||||
}
|
||||
|
||||
if (!attachmentColumns.isEmpty()) {
|
||||
addColumnIfMissing("script_asset_attachments", attachmentColumns, "file_id", "TEXT", "NULL");
|
||||
}
|
||||
|
||||
try {
|
||||
jdbcTemplate.execute("UPDATE script_assets SET source_file_id = id WHERE source_file_id IS NULL");
|
||||
jdbcTemplate.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO script_asset_files (
|
||||
id, broadcaster, media_type, original_media_type, asset_type
|
||||
)
|
||||
SELECT s.id, a.broadcaster, s.media_type, s.original_media_type, 'SCRIPT'
|
||||
FROM script_assets s
|
||||
JOIN assets a ON a.id = s.id
|
||||
"""
|
||||
);
|
||||
jdbcTemplate.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO script_asset_files (
|
||||
id, broadcaster, media_type, original_media_type, asset_type
|
||||
)
|
||||
SELECT sa.id, a.broadcaster, sa.media_type, sa.original_media_type, sa.asset_type
|
||||
FROM script_asset_attachments sa
|
||||
JOIN assets a ON a.id = sa.script_asset_id
|
||||
"""
|
||||
);
|
||||
jdbcTemplate.execute("UPDATE script_asset_attachments SET file_id = id WHERE file_id IS NULL");
|
||||
} catch (DataAccessException ex) {
|
||||
logger.warn("Unable to backfill script asset files", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void backfillAssetTypes(List<String> assetColumns) {
|
||||
if (!assetColumns.contains("media_type")) {
|
||||
return;
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -415,6 +416,29 @@ public class ChannelApiController {
|
||||
.orElseThrow(() -> createAsset404());
|
||||
}
|
||||
|
||||
@GetMapping("/assets/{assetId}/logo")
|
||||
public ResponseEntity<byte[]> getScriptLogo(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@PathVariable("assetId") String assetId,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
return channelDirectoryService
|
||||
.getScriptLogoContent(broadcaster, assetId)
|
||||
.map((content) ->
|
||||
ResponseEntity.ok()
|
||||
.header("X-Content-Type-Options", "nosniff")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, contentDispositionFor(content.mediaType()))
|
||||
.contentType(MediaType.parseMediaType(content.mediaType()))
|
||||
.body(content.bytes())
|
||||
)
|
||||
.orElseThrow(() -> createAsset404());
|
||||
}
|
||||
|
||||
@GetMapping("/assets/{assetId}/preview")
|
||||
public ResponseEntity<byte[]> getAssetPreview(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@@ -485,7 +509,7 @@ public class ChannelApiController {
|
||||
public ResponseEntity<ScriptAssetAttachmentView> createScriptAttachment(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@PathVariable("assetId") String assetId,
|
||||
@org.springframework.web.bind.annotation.RequestPart("file") MultipartFile file,
|
||||
@RequestPart("file") MultipartFile file,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
@@ -507,6 +531,47 @@ public class ChannelApiController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/assets/{assetId}/logo", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<AssetView> updateScriptLogo(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@PathVariable("assetId") String assetId,
|
||||
@RequestPart("file") MultipartFile file,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Logo file is required");
|
||||
}
|
||||
try {
|
||||
return channelDirectoryService
|
||||
.updateScriptLogo(broadcaster, assetId, file)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to save logo"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to process logo upload for {} by {}", broadcaster, sessionUsername, e);
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Failed to process logo", e);
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/assets/{assetId}/logo")
|
||||
public ResponseEntity<Void> deleteScriptLogo(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
@PathVariable("assetId") String assetId,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||
broadcaster,
|
||||
sessionUsername
|
||||
);
|
||||
channelDirectoryService.clearScriptLogo(broadcaster, assetId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@DeleteMapping("/assets/{assetId}/attachments/{attachmentId}")
|
||||
public ResponseEntity<Void> deleteScriptAttachment(
|
||||
@PathVariable("broadcaster") String broadcaster,
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package dev.kruhlmann.imgfloat.controller;
|
||||
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.AssetView;
|
||||
import dev.kruhlmann.imgfloat.model.OauthSessionUser;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptMarketplaceEntry;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptMarketplaceImportRequest;
|
||||
import dev.kruhlmann.imgfloat.service.AuthorizationService;
|
||||
import dev.kruhlmann.imgfloat.service.ChannelDirectoryService;
|
||||
import dev.kruhlmann.imgfloat.util.LogSanitizer;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/marketplace")
|
||||
@SecurityRequirement(name = "twitchOAuth")
|
||||
public class ScriptMarketplaceController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ScriptMarketplaceController.class);
|
||||
private final ChannelDirectoryService channelDirectoryService;
|
||||
private final AuthorizationService authorizationService;
|
||||
|
||||
public ScriptMarketplaceController(
|
||||
ChannelDirectoryService channelDirectoryService,
|
||||
AuthorizationService authorizationService
|
||||
) {
|
||||
this.channelDirectoryService = channelDirectoryService;
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
@GetMapping("/scripts")
|
||||
public List<ScriptMarketplaceEntry> listMarketplaceScripts(@RequestParam(value = "query", required = false) String query) {
|
||||
return channelDirectoryService.listMarketplaceScripts(query);
|
||||
}
|
||||
|
||||
@GetMapping("/scripts/{scriptId}/logo")
|
||||
public ResponseEntity<byte[]> getMarketplaceLogo(@PathVariable("scriptId") String scriptId) {
|
||||
String logScriptId = LogSanitizer.sanitize(scriptId);
|
||||
LOG.debug("Serving marketplace logo for script {}", logScriptId);
|
||||
return channelDirectoryService
|
||||
.getMarketplaceLogo(scriptId)
|
||||
.map((content) ->
|
||||
ResponseEntity.ok()
|
||||
.header("X-Content-Type-Options", "nosniff")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "inline")
|
||||
.contentType(MediaType.parseMediaType(content.mediaType()))
|
||||
.body(content.bytes())
|
||||
)
|
||||
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Logo not found"));
|
||||
}
|
||||
|
||||
@PostMapping("/scripts/{scriptId}/import")
|
||||
public ResponseEntity<AssetView> importMarketplaceScript(
|
||||
@PathVariable("scriptId") String scriptId,
|
||||
@Valid @RequestBody ScriptMarketplaceImportRequest request,
|
||||
OAuth2AuthenticationToken oauthToken
|
||||
) {
|
||||
String sessionUsername = OauthSessionUser.from(oauthToken).login();
|
||||
authorizationService.userIsBroadcasterOrChannelAdminForBroadcasterOrThrowHttpError(
|
||||
request.getTargetBroadcaster(),
|
||||
sessionUsername
|
||||
);
|
||||
String logScriptId = LogSanitizer.sanitize(scriptId);
|
||||
String logTarget = LogSanitizer.sanitize(request.getTargetBroadcaster());
|
||||
LOG.info("Importing marketplace script {} into {}", logScriptId, logTarget);
|
||||
return channelDirectoryService
|
||||
.importMarketplaceScript(request.getTargetBroadcaster(), scriptId)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to import script"));
|
||||
}
|
||||
}
|
||||
@@ -136,6 +136,7 @@ public class ViewController {
|
||||
Settings settings = settingsService.get();
|
||||
model.addAttribute("broadcaster", broadcaster.toLowerCase());
|
||||
model.addAttribute("username", sessionUsername);
|
||||
model.addAttribute("adminChannels", channelDirectoryService.adminChannelsFor(sessionUsername));
|
||||
model.addAttribute("uploadLimitBytes", uploadLimitBytes);
|
||||
try {
|
||||
model.addAttribute("settingsJson", objectMapper.writeValueAsString(settings));
|
||||
|
||||
@@ -7,6 +7,9 @@ public record AssetView(
|
||||
String id,
|
||||
String broadcaster,
|
||||
String name,
|
||||
String description,
|
||||
String logoUrl,
|
||||
Boolean isPublic,
|
||||
String url,
|
||||
String previewUrl,
|
||||
double x,
|
||||
@@ -37,6 +40,9 @@ public record AssetView(
|
||||
asset.getId(),
|
||||
asset.getBroadcaster(),
|
||||
visual.getName(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/content",
|
||||
hasPreview ? "/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/preview" : null,
|
||||
visual.getX(),
|
||||
@@ -68,6 +74,9 @@ public record AssetView(
|
||||
asset.getId(),
|
||||
asset.getBroadcaster(),
|
||||
audio.getName(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/content",
|
||||
null,
|
||||
0,
|
||||
@@ -99,6 +108,11 @@ public record AssetView(
|
||||
asset.getId(),
|
||||
asset.getBroadcaster(),
|
||||
script.getName(),
|
||||
script.getDescription(),
|
||||
script.getLogoFileId() == null
|
||||
? null
|
||||
: "/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/logo",
|
||||
script.isPublic(),
|
||||
"/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/content",
|
||||
null,
|
||||
0,
|
||||
|
||||
@@ -10,6 +10,10 @@ public class CodeAssetRequest {
|
||||
@NotBlank
|
||||
private String source;
|
||||
|
||||
private String description;
|
||||
|
||||
private Boolean isPublic;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
@@ -25,4 +29,20 @@ public class CodeAssetRequest {
|
||||
public void setSource(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Boolean getIsPublic() {
|
||||
return isPublic;
|
||||
}
|
||||
|
||||
public void setIsPublic(Boolean isPublic) {
|
||||
this.isPublic = isPublic;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,20 @@ public class ScriptAsset {
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
@Column(name = "is_public")
|
||||
private boolean isPublic;
|
||||
|
||||
private String mediaType;
|
||||
private String originalMediaType;
|
||||
|
||||
@Column(name = "logo_file_id")
|
||||
private String logoFileId;
|
||||
|
||||
@Column(name = "source_file_id")
|
||||
private String sourceFileId;
|
||||
|
||||
@Transient
|
||||
private List<ScriptAssetAttachmentView> attachments = List.of();
|
||||
|
||||
@@ -72,6 +83,38 @@ public class ScriptAsset {
|
||||
this.originalMediaType = originalMediaType;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public boolean isPublic() {
|
||||
return isPublic;
|
||||
}
|
||||
|
||||
public void setPublic(boolean isPublic) {
|
||||
this.isPublic = isPublic;
|
||||
}
|
||||
|
||||
public String getLogoFileId() {
|
||||
return logoFileId;
|
||||
}
|
||||
|
||||
public void setLogoFileId(String logoFileId) {
|
||||
this.logoFileId = logoFileId;
|
||||
}
|
||||
|
||||
public String getSourceFileId() {
|
||||
return sourceFileId;
|
||||
}
|
||||
|
||||
public void setSourceFileId(String sourceFileId) {
|
||||
this.sourceFileId = sourceFileId;
|
||||
}
|
||||
|
||||
public List<ScriptAssetAttachmentView> getAttachments() {
|
||||
return attachments == null ? List.of() : attachments;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ public class ScriptAssetAttachment {
|
||||
@Column(name = "script_asset_id", nullable = false)
|
||||
private String scriptAssetId;
|
||||
|
||||
@Column(name = "file_id")
|
||||
private String fileId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@@ -69,6 +72,14 @@ public class ScriptAssetAttachment {
|
||||
this.scriptAssetId = scriptAssetId;
|
||||
}
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public void setFileId(String fileId) {
|
||||
this.fileId = fileId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "script_asset_files")
|
||||
public class ScriptAssetFile {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String broadcaster;
|
||||
|
||||
private String mediaType;
|
||||
private String originalMediaType;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "asset_type", nullable = false)
|
||||
private AssetType assetType;
|
||||
|
||||
public ScriptAssetFile() {}
|
||||
|
||||
public ScriptAssetFile(String broadcaster, AssetType assetType) {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.broadcaster = normalize(broadcaster);
|
||||
this.assetType = assetType == null ? AssetType.OTHER : assetType;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
@PreUpdate
|
||||
public void prepare() {
|
||||
if (this.id == null) {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
}
|
||||
this.broadcaster = normalize(broadcaster);
|
||||
if (this.assetType == null) {
|
||||
this.assetType = AssetType.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getBroadcaster() {
|
||||
return broadcaster;
|
||||
}
|
||||
|
||||
public void setBroadcaster(String broadcaster) {
|
||||
this.broadcaster = normalize(broadcaster);
|
||||
}
|
||||
|
||||
public String getMediaType() {
|
||||
return mediaType;
|
||||
}
|
||||
|
||||
public void setMediaType(String mediaType) {
|
||||
this.mediaType = mediaType;
|
||||
}
|
||||
|
||||
public String getOriginalMediaType() {
|
||||
return originalMediaType;
|
||||
}
|
||||
|
||||
public void setOriginalMediaType(String originalMediaType) {
|
||||
this.originalMediaType = originalMediaType;
|
||||
}
|
||||
|
||||
public AssetType getAssetType() {
|
||||
return assetType == null ? AssetType.OTHER : assetType;
|
||||
}
|
||||
|
||||
public void setAssetType(AssetType assetType) {
|
||||
this.assetType = assetType == null ? AssetType.OTHER : assetType;
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null ? null : value.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
public record ScriptMarketplaceEntry(
|
||||
String id,
|
||||
String name,
|
||||
String description,
|
||||
String logoUrl,
|
||||
String broadcaster
|
||||
) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class ScriptMarketplaceImportRequest {
|
||||
|
||||
@NotBlank
|
||||
private String targetBroadcaster;
|
||||
|
||||
public String getTargetBroadcaster() {
|
||||
return targetBroadcaster;
|
||||
}
|
||||
|
||||
public void setTargetBroadcaster(String targetBroadcaster) {
|
||||
this.targetBroadcaster = targetBroadcaster;
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,6 @@ public interface ScriptAssetAttachmentRepository extends JpaRepository<ScriptAss
|
||||
List<ScriptAssetAttachment> findByScriptAssetIdIn(Collection<String> scriptAssetIds);
|
||||
|
||||
void deleteByScriptAssetId(String scriptAssetId);
|
||||
|
||||
long countByFileId(String fileId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package dev.kruhlmann.imgfloat.repository;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetFile;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ScriptAssetFileRepository extends JpaRepository<ScriptAssetFile, String> {}
|
||||
@@ -7,4 +7,10 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ScriptAssetRepository extends JpaRepository<ScriptAsset, String> {
|
||||
List<ScriptAsset> findByIdIn(Collection<String> ids);
|
||||
|
||||
List<ScriptAsset> findByIsPublicTrue();
|
||||
|
||||
long countBySourceFileId(String sourceFileId);
|
||||
|
||||
long countByLogoFileId(String logoFileId);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package dev.kruhlmann.imgfloat.service;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.Asset;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetAttachment;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAsset;
|
||||
import dev.kruhlmann.imgfloat.repository.AssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ScriptAssetAttachmentRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ScriptAssetRepository;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
@@ -22,15 +24,18 @@ public class AssetCleanupService {
|
||||
private final AssetRepository assetRepository;
|
||||
private final AssetStorageService assetStorageService;
|
||||
private final ScriptAssetAttachmentRepository scriptAssetAttachmentRepository;
|
||||
private final ScriptAssetRepository scriptAssetRepository;
|
||||
|
||||
public AssetCleanupService(
|
||||
AssetRepository assetRepository,
|
||||
AssetStorageService assetStorageService,
|
||||
ScriptAssetAttachmentRepository scriptAssetAttachmentRepository
|
||||
ScriptAssetAttachmentRepository scriptAssetAttachmentRepository,
|
||||
ScriptAssetRepository scriptAssetRepository
|
||||
) {
|
||||
this.assetRepository = assetRepository;
|
||||
this.assetStorageService = assetStorageService;
|
||||
this.scriptAssetAttachmentRepository = scriptAssetAttachmentRepository;
|
||||
this.scriptAssetRepository = scriptAssetRepository;
|
||||
}
|
||||
|
||||
@Async
|
||||
@@ -48,7 +53,23 @@ public class AssetCleanupService {
|
||||
scriptAssetAttachmentRepository
|
||||
.findAll()
|
||||
.stream()
|
||||
.map(ScriptAssetAttachment::getId)
|
||||
.map((attachment) -> attachment.getFileId() != null ? attachment.getFileId() : attachment.getId())
|
||||
.collect(Collectors.toSet())
|
||||
);
|
||||
referencedIds.addAll(
|
||||
scriptAssetRepository
|
||||
.findAll()
|
||||
.stream()
|
||||
.map(ScriptAsset::getSourceFileId)
|
||||
.filter((id) -> id != null && !id.isBlank())
|
||||
.collect(Collectors.toSet())
|
||||
);
|
||||
referencedIds.addAll(
|
||||
scriptAssetRepository
|
||||
.findAll()
|
||||
.stream()
|
||||
.map(ScriptAsset::getLogoFileId)
|
||||
.filter((id) -> id != null && !id.isBlank())
|
||||
.collect(Collectors.toSet())
|
||||
);
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ import dev.kruhlmann.imgfloat.model.PlaybackRequest;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAsset;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetAttachment;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetAttachmentView;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAssetFile;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptMarketplaceEntry;
|
||||
import dev.kruhlmann.imgfloat.model.Settings;
|
||||
import dev.kruhlmann.imgfloat.model.TransformRequest;
|
||||
import dev.kruhlmann.imgfloat.model.VisibilityRequest;
|
||||
@@ -26,6 +28,7 @@ import dev.kruhlmann.imgfloat.repository.AudioAssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ChannelRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ScriptAssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ScriptAssetAttachmentRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ScriptAssetFileRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.VisualAssetRepository;
|
||||
import dev.kruhlmann.imgfloat.service.media.AssetContent;
|
||||
import dev.kruhlmann.imgfloat.service.media.MediaDetectionService;
|
||||
@@ -57,6 +60,7 @@ public class ChannelDirectoryService {
|
||||
private final AudioAssetRepository audioAssetRepository;
|
||||
private final ScriptAssetRepository scriptAssetRepository;
|
||||
private final ScriptAssetAttachmentRepository scriptAssetAttachmentRepository;
|
||||
private final ScriptAssetFileRepository scriptAssetFileRepository;
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final AssetStorageService assetStorageService;
|
||||
private final MediaDetectionService mediaDetectionService;
|
||||
@@ -72,6 +76,7 @@ public class ChannelDirectoryService {
|
||||
AudioAssetRepository audioAssetRepository,
|
||||
ScriptAssetRepository scriptAssetRepository,
|
||||
ScriptAssetAttachmentRepository scriptAssetAttachmentRepository,
|
||||
ScriptAssetFileRepository scriptAssetFileRepository,
|
||||
SimpMessagingTemplate messagingTemplate,
|
||||
AssetStorageService assetStorageService,
|
||||
MediaDetectionService mediaDetectionService,
|
||||
@@ -85,6 +90,7 @@ public class ChannelDirectoryService {
|
||||
this.audioAssetRepository = audioAssetRepository;
|
||||
this.scriptAssetRepository = scriptAssetRepository;
|
||||
this.scriptAssetAttachmentRepository = scriptAssetAttachmentRepository;
|
||||
this.scriptAssetFileRepository = scriptAssetFileRepository;
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
this.assetStorageService = assetStorageService;
|
||||
this.mediaDetectionService = mediaDetectionService;
|
||||
@@ -226,8 +232,14 @@ public class ChannelDirectoryService {
|
||||
ScriptAsset script = new ScriptAsset(asset.getId(), safeName);
|
||||
script.setMediaType(optimized.mediaType());
|
||||
script.setOriginalMediaType(mediaType);
|
||||
script.setSourceFileId(asset.getId());
|
||||
script.setAttachments(List.of());
|
||||
scriptAssetRepository.save(script);
|
||||
ScriptAssetFile sourceFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT);
|
||||
sourceFile.setId(asset.getId());
|
||||
sourceFile.setMediaType(optimized.mediaType());
|
||||
sourceFile.setOriginalMediaType(mediaType);
|
||||
scriptAssetFileRepository.save(sourceFile);
|
||||
view = AssetView.fromScript(channel.getBroadcaster(), asset, script);
|
||||
} else {
|
||||
double defaultWidth = 640;
|
||||
@@ -257,17 +269,30 @@ public class ChannelDirectoryService {
|
||||
enforceUploadLimit(bytes.length);
|
||||
|
||||
Asset asset = new Asset(channel.getBroadcaster(), AssetType.SCRIPT);
|
||||
ScriptAssetFile sourceFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT);
|
||||
sourceFile.setId(asset.getId());
|
||||
sourceFile.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
sourceFile.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
|
||||
try {
|
||||
assetStorageService.storeAsset(channel.getBroadcaster(), asset.getId(), bytes, DEFAULT_CODE_MEDIA_TYPE);
|
||||
assetStorageService.storeAsset(
|
||||
sourceFile.getBroadcaster(),
|
||||
sourceFile.getId(),
|
||||
bytes,
|
||||
DEFAULT_CODE_MEDIA_TYPE
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Unable to store custom script", e);
|
||||
}
|
||||
|
||||
asset = assetRepository.save(asset);
|
||||
scriptAssetFileRepository.save(sourceFile);
|
||||
ScriptAsset script = new ScriptAsset(asset.getId(), request.getName().trim());
|
||||
script.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
script.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
script.setSourceFileId(sourceFile.getId());
|
||||
script.setDescription(normalizeDescription(request.getDescription()));
|
||||
script.setPublic(Boolean.TRUE.equals(request.getIsPublic()));
|
||||
script.setAttachments(List.of());
|
||||
scriptAssetRepository.save(script);
|
||||
AssetView view = AssetView.fromScript(channel.getBroadcaster(), asset, script);
|
||||
@@ -291,12 +316,38 @@ public class ChannelDirectoryService {
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
||||
String sourceFileId = script.getSourceFileId();
|
||||
if (sourceFileId == null || sourceFileId.isBlank()) {
|
||||
sourceFileId = asset.getId();
|
||||
script.setSourceFileId(sourceFileId);
|
||||
}
|
||||
String resolvedSourceFileId = sourceFileId;
|
||||
ScriptAssetFile sourceFile = scriptAssetFileRepository
|
||||
.findById(resolvedSourceFileId)
|
||||
.orElseGet(() -> {
|
||||
ScriptAssetFile file = new ScriptAssetFile(asset.getBroadcaster(), AssetType.SCRIPT);
|
||||
file.setId(resolvedSourceFileId);
|
||||
file.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
file.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
return scriptAssetFileRepository.save(file);
|
||||
});
|
||||
script.setName(request.getName().trim());
|
||||
if (request.getDescription() != null) {
|
||||
script.setDescription(normalizeDescription(request.getDescription()));
|
||||
}
|
||||
if (request.getIsPublic() != null) {
|
||||
script.setPublic(request.getIsPublic());
|
||||
}
|
||||
script.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
script.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
script.setAttachments(loadScriptAttachments(normalized, asset.getId(), null));
|
||||
try {
|
||||
assetStorageService.storeAsset(broadcaster, asset.getId(), bytes, DEFAULT_CODE_MEDIA_TYPE);
|
||||
assetStorageService.storeAsset(
|
||||
sourceFile.getBroadcaster(),
|
||||
sourceFile.getId(),
|
||||
bytes,
|
||||
DEFAULT_CODE_MEDIA_TYPE
|
||||
);
|
||||
} catch (IOException e) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Unable to store custom script", e);
|
||||
}
|
||||
@@ -308,6 +359,185 @@ public class ChannelDirectoryService {
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<AssetView> updateScriptLogo(String broadcaster, String assetId, MultipartFile file)
|
||||
throws IOException {
|
||||
Asset asset = requireScriptAssetForBroadcaster(broadcaster, assetId);
|
||||
byte[] bytes = file.getBytes();
|
||||
enforceUploadLimit(bytes.length);
|
||||
String mediaType = mediaDetectionService
|
||||
.detectAllowedMediaType(file, bytes)
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unsupported media type"));
|
||||
OptimizedAsset optimized = mediaOptimizationService.optimizeAsset(bytes, mediaType);
|
||||
if (optimized == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
AssetType assetType = AssetType.fromMediaType(optimized.mediaType(), mediaType);
|
||||
if (assetType != AssetType.IMAGE) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Logo must be an image.");
|
||||
}
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
||||
|
||||
String previousLogoFileId = script.getLogoFileId();
|
||||
ScriptAssetFile logoFile = new ScriptAssetFile(asset.getBroadcaster(), AssetType.IMAGE);
|
||||
logoFile.setMediaType(optimized.mediaType());
|
||||
logoFile.setOriginalMediaType(mediaType);
|
||||
scriptAssetFileRepository.save(logoFile);
|
||||
|
||||
assetStorageService.storeAsset(
|
||||
logoFile.getBroadcaster(),
|
||||
logoFile.getId(),
|
||||
optimized.bytes(),
|
||||
optimized.mediaType()
|
||||
);
|
||||
|
||||
script.setLogoFileId(logoFile.getId());
|
||||
script.setAttachments(loadScriptAttachments(asset.getBroadcaster(), asset.getId(), null));
|
||||
scriptAssetRepository.save(script);
|
||||
|
||||
removeScriptAssetFileIfOrphaned(previousLogoFileId);
|
||||
|
||||
AssetView view = AssetView.fromScript(asset.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, view));
|
||||
return Optional.of(view);
|
||||
}
|
||||
|
||||
public Optional<AssetView> clearScriptLogo(String broadcaster, String assetId) {
|
||||
Asset asset = requireScriptAssetForBroadcaster(broadcaster, assetId);
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
||||
String previousLogoFileId = script.getLogoFileId();
|
||||
if (previousLogoFileId == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
script.setLogoFileId(null);
|
||||
script.setAttachments(loadScriptAttachments(asset.getBroadcaster(), asset.getId(), null));
|
||||
scriptAssetRepository.save(script);
|
||||
removeScriptAssetFileIfOrphaned(previousLogoFileId);
|
||||
AssetView view = AssetView.fromScript(asset.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, view));
|
||||
return Optional.of(view);
|
||||
}
|
||||
|
||||
public List<ScriptMarketplaceEntry> listMarketplaceScripts(String query) {
|
||||
String q = normalizeDescription(query);
|
||||
String normalizedQuery = q == null ? null : q.toLowerCase(Locale.ROOT);
|
||||
List<ScriptAsset> scripts = scriptAssetRepository.findByIsPublicTrue();
|
||||
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));
|
||||
|
||||
return 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
|
||||
);
|
||||
})
|
||||
.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())
|
||||
);
|
||||
}
|
||||
|
||||
public Optional<AssetView> importMarketplaceScript(String targetBroadcaster, String scriptId) {
|
||||
ScriptAsset sourceScript = scriptAssetRepository
|
||||
.findById(scriptId)
|
||||
.filter(ScriptAsset::isPublic)
|
||||
.orElse(null);
|
||||
Asset sourceAsset = sourceScript == null ? null : assetRepository.findById(scriptId).orElse(null);
|
||||
if (sourceScript == null || sourceAsset == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
AssetContent sourceContent = loadScriptSourceContent(sourceAsset, sourceScript).orElse(null);
|
||||
if (sourceContent == 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);
|
||||
|
||||
ScriptAsset script = new ScriptAsset(asset.getId(), sourceScript.getName());
|
||||
script.setDescription(sourceScript.getDescription());
|
||||
script.setPublic(false);
|
||||
script.setMediaType(sourceContent.mediaType());
|
||||
script.setOriginalMediaType(sourceContent.mediaType());
|
||||
script.setSourceFileId(sourceFile.getId());
|
||||
script.setLogoFileId(sourceScript.getLogoFileId());
|
||||
script.setAttachments(List.of());
|
||||
scriptAssetRepository.save(script);
|
||||
|
||||
List<ScriptAssetAttachment> sourceAttachments = scriptAssetAttachmentRepository
|
||||
.findByScriptAssetId(sourceScript.getId());
|
||||
List<ScriptAssetAttachment> newAttachments = sourceAttachments
|
||||
.stream()
|
||||
.map((attachment) -> {
|
||||
ScriptAssetAttachment copy = new ScriptAssetAttachment(asset.getId(), attachment.getName());
|
||||
String fileId = attachment.getFileId() != null ? attachment.getFileId() : attachment.getId();
|
||||
copy.setFileId(fileId);
|
||||
copy.setMediaType(attachment.getMediaType());
|
||||
copy.setOriginalMediaType(attachment.getOriginalMediaType());
|
||||
copy.setAssetType(attachment.getAssetType());
|
||||
return copy;
|
||||
})
|
||||
.toList();
|
||||
if (!newAttachments.isEmpty()) {
|
||||
scriptAssetAttachmentRepository.saveAll(newAttachments);
|
||||
}
|
||||
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("_");
|
||||
@@ -524,16 +754,30 @@ public class ChannelDirectoryService {
|
||||
return assetRepository
|
||||
.findById(assetId)
|
||||
.map((asset) -> {
|
||||
deleteAssetStorage(asset);
|
||||
switch (asset.getAssetType()) {
|
||||
case AUDIO -> audioAssetRepository.deleteById(asset.getId());
|
||||
case SCRIPT -> {
|
||||
scriptAssetAttachmentRepository.deleteByScriptAssetId(asset.getId());
|
||||
scriptAssetRepository.deleteById(asset.getId());
|
||||
if (asset.getAssetType() == AssetType.SCRIPT) {
|
||||
ScriptAsset script = scriptAssetRepository.findById(asset.getId()).orElse(null);
|
||||
List<String> attachmentFileIds = scriptAssetAttachmentRepository
|
||||
.findByScriptAssetId(asset.getId())
|
||||
.stream()
|
||||
.map((attachment) -> attachment.getFileId() != null ? attachment.getFileId() : attachment.getId())
|
||||
.toList();
|
||||
scriptAssetAttachmentRepository.deleteByScriptAssetId(asset.getId());
|
||||
scriptAssetRepository.deleteById(asset.getId());
|
||||
assetRepository.delete(asset);
|
||||
if (script != null) {
|
||||
removeScriptAssetFileIfOrphaned(script.getSourceFileId());
|
||||
removeScriptAssetFileIfOrphaned(script.getLogoFileId());
|
||||
}
|
||||
default -> visualAssetRepository.deleteById(asset.getId());
|
||||
attachmentFileIds.forEach(this::removeScriptAssetFileIfOrphaned);
|
||||
} else {
|
||||
deleteAssetStorage(asset);
|
||||
switch (asset.getAssetType()) {
|
||||
case AUDIO -> audioAssetRepository.deleteById(asset.getId());
|
||||
case SCRIPT -> scriptAssetRepository.deleteById(asset.getId());
|
||||
default -> visualAssetRepository.deleteById(asset.getId());
|
||||
}
|
||||
assetRepository.delete(asset);
|
||||
}
|
||||
assetRepository.delete(asset);
|
||||
messagingTemplate.convertAndSend(
|
||||
topicFor(asset.getBroadcaster()),
|
||||
AssetEvent.deleted(asset.getBroadcaster(), assetId)
|
||||
@@ -590,12 +834,23 @@ public class ChannelDirectoryService {
|
||||
.filter((s) -> !s.isBlank())
|
||||
.orElse("script_attachment_" + System.currentTimeMillis());
|
||||
|
||||
ScriptAssetFile attachmentFile = new ScriptAssetFile(asset.getBroadcaster(), assetType);
|
||||
attachmentFile.setMediaType(optimized.mediaType());
|
||||
attachmentFile.setOriginalMediaType(mediaType);
|
||||
scriptAssetFileRepository.save(attachmentFile);
|
||||
|
||||
ScriptAssetAttachment attachment = new ScriptAssetAttachment(asset.getId(), safeName);
|
||||
attachment.setFileId(attachmentFile.getId());
|
||||
attachment.setMediaType(optimized.mediaType());
|
||||
attachment.setOriginalMediaType(mediaType);
|
||||
attachment.setAssetType(assetType);
|
||||
|
||||
assetStorageService.storeAsset(asset.getBroadcaster(), attachment.getId(), optimized.bytes(), optimized.mediaType());
|
||||
assetStorageService.storeAsset(
|
||||
attachmentFile.getBroadcaster(),
|
||||
attachmentFile.getId(),
|
||||
optimized.bytes(),
|
||||
optimized.mediaType()
|
||||
);
|
||||
attachment = scriptAssetAttachmentRepository.save(attachment);
|
||||
ScriptAssetAttachmentView view = ScriptAssetAttachmentView.fromAttachment(asset.getBroadcaster(), attachment);
|
||||
|
||||
@@ -618,13 +873,9 @@ public class ChannelDirectoryService {
|
||||
if (attachment == null) {
|
||||
return false;
|
||||
}
|
||||
assetStorageService.deleteAsset(
|
||||
asset.getBroadcaster(),
|
||||
attachment.getId(),
|
||||
attachment.getMediaType(),
|
||||
false
|
||||
);
|
||||
String fileId = attachment.getFileId() != null ? attachment.getFileId() : attachment.getId();
|
||||
scriptAssetAttachmentRepository.deleteById(attachment.getId());
|
||||
removeScriptAssetFileIfOrphaned(fileId);
|
||||
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
@@ -651,12 +902,17 @@ public class ChannelDirectoryService {
|
||||
return scriptAssetAttachmentRepository
|
||||
.findById(attachmentId)
|
||||
.filter((item) -> item.getScriptAssetId().equals(scriptAssetId))
|
||||
.flatMap((attachment) ->
|
||||
assetStorageService.loadAssetFileSafely(
|
||||
asset.getBroadcaster(),
|
||||
attachment.getId(),
|
||||
attachment.getMediaType()
|
||||
)
|
||||
.flatMap((attachment) -> loadScriptAttachmentContent(asset, attachment));
|
||||
}
|
||||
|
||||
public Optional<AssetContent> getScriptLogoContent(String broadcaster, String scriptAssetId) {
|
||||
Asset asset = requireScriptAssetForBroadcaster(broadcaster, scriptAssetId);
|
||||
return scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.map(ScriptAsset::getLogoFileId)
|
||||
.flatMap((logoFileId) -> scriptAssetFileRepository.findById(logoFileId))
|
||||
.flatMap((file) ->
|
||||
assetStorageService.loadAssetFileSafely(file.getBroadcaster(), file.getId(), file.getMediaType())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -701,6 +957,32 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeDescription(String description) {
|
||||
if (description == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = description.trim();
|
||||
return trimmed.isBlank() ? null : trimmed;
|
||||
}
|
||||
|
||||
private void removeScriptAssetFileIfOrphaned(String fileId) {
|
||||
if (fileId == null || fileId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
long attachmentRefs = scriptAssetAttachmentRepository.countByFileId(fileId);
|
||||
long sourceRefs = scriptAssetRepository.countBySourceFileId(fileId);
|
||||
long logoRefs = scriptAssetRepository.countByLogoFileId(fileId);
|
||||
if (attachmentRefs + sourceRefs + logoRefs > 0) {
|
||||
return;
|
||||
}
|
||||
scriptAssetFileRepository
|
||||
.findById(fileId)
|
||||
.ifPresent((file) -> {
|
||||
assetStorageService.deleteAsset(file.getBroadcaster(), file.getId(), file.getMediaType(), false);
|
||||
scriptAssetFileRepository.delete(file);
|
||||
});
|
||||
}
|
||||
|
||||
private void enforceUploadLimit(long sizeBytes) {
|
||||
if (sizeBytes > uploadLimitBytes) {
|
||||
throw new ResponseStatusException(
|
||||
@@ -851,13 +1133,7 @@ public class ChannelDirectoryService {
|
||||
case SCRIPT -> {
|
||||
return scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.flatMap((script) ->
|
||||
assetStorageService.loadAssetFileSafely(
|
||||
asset.getBroadcaster(),
|
||||
asset.getId(),
|
||||
script.getMediaType()
|
||||
)
|
||||
);
|
||||
.flatMap((script) -> loadScriptSourceContent(asset, script));
|
||||
}
|
||||
default -> {
|
||||
return visualAssetRepository
|
||||
@@ -873,6 +1149,44 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<AssetContent> loadScriptSourceContent(Asset asset, ScriptAsset script) {
|
||||
if (script == null || asset == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String sourceFileId = script.getSourceFileId() != null ? script.getSourceFileId() : script.getId();
|
||||
return scriptAssetFileRepository
|
||||
.findById(sourceFileId)
|
||||
.flatMap((file) ->
|
||||
assetStorageService.loadAssetFileSafely(file.getBroadcaster(), file.getId(), file.getMediaType())
|
||||
)
|
||||
.or(() ->
|
||||
assetStorageService.loadAssetFileSafely(
|
||||
asset.getBroadcaster(),
|
||||
sourceFileId,
|
||||
script.getMediaType()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private Optional<AssetContent> loadScriptAttachmentContent(Asset asset, ScriptAssetAttachment attachment) {
|
||||
if (attachment == null || asset == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String fileId = attachment.getFileId() != null ? attachment.getFileId() : attachment.getId();
|
||||
return scriptAssetFileRepository
|
||||
.findById(fileId)
|
||||
.flatMap((file) ->
|
||||
assetStorageService.loadAssetFileSafely(file.getBroadcaster(), file.getId(), file.getMediaType())
|
||||
)
|
||||
.or(() ->
|
||||
assetStorageService.loadAssetFileSafely(
|
||||
asset.getBroadcaster(),
|
||||
fileId,
|
||||
attachment.getMediaType()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private List<ScriptAssetAttachmentView> loadScriptAttachments(
|
||||
String broadcaster,
|
||||
String scriptAssetId,
|
||||
@@ -930,15 +1244,13 @@ public class ChannelDirectoryService {
|
||||
case SCRIPT -> scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.ifPresent((script) -> {
|
||||
assetStorageService.deleteAsset(asset.getBroadcaster(), asset.getId(), script.getMediaType(), false);
|
||||
removeScriptAssetFileIfOrphaned(script.getSourceFileId());
|
||||
removeScriptAssetFileIfOrphaned(script.getLogoFileId());
|
||||
scriptAssetAttachmentRepository
|
||||
.findByScriptAssetId(asset.getId())
|
||||
.forEach((attachment) ->
|
||||
assetStorageService.deleteAsset(
|
||||
asset.getBroadcaster(),
|
||||
attachment.getId(),
|
||||
attachment.getMediaType(),
|
||||
false
|
||||
removeScriptAssetFileIfOrphaned(
|
||||
attachment.getFileId() != null ? attachment.getFileId() : attachment.getId()
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user