mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Explode assets class into subtypes
This commit is contained in:
@@ -24,7 +24,7 @@ public class SchemaMigration implements ApplicationRunner {
|
||||
public void run(ApplicationArguments args) {
|
||||
ensureSessionAttributeUpsertTrigger();
|
||||
ensureChannelCanvasColumns();
|
||||
ensureAssetMediaColumns();
|
||||
ensureAssetTables();
|
||||
ensureAuthorizedClientTable();
|
||||
normalizeAuthorizedClientTimestamps();
|
||||
}
|
||||
@@ -67,12 +67,12 @@ public class SchemaMigration implements ApplicationRunner {
|
||||
addColumnIfMissing("channels", columns, "canvas_height", "REAL", "1080");
|
||||
}
|
||||
|
||||
private void ensureAssetMediaColumns() {
|
||||
private void ensureAssetTables() {
|
||||
List<String> columns;
|
||||
try {
|
||||
columns = jdbcTemplate.query("PRAGMA table_info(assets)", (rs, rowNum) -> rs.getString("name"));
|
||||
} catch (DataAccessException ex) {
|
||||
logger.warn("Unable to inspect assets table for media columns", ex);
|
||||
logger.warn("Unable to inspect assets table for asset columns", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,15 +81,121 @@ public class SchemaMigration implements ApplicationRunner {
|
||||
}
|
||||
|
||||
String table = "assets";
|
||||
addColumnIfMissing(table, columns, "speed", "REAL", "1.0");
|
||||
addColumnIfMissing(table, columns, "muted", "BOOLEAN", "0");
|
||||
addColumnIfMissing(table, columns, "media_type", "TEXT", "'application/octet-stream'");
|
||||
addColumnIfMissing(table, columns, "audio_loop", "BOOLEAN", "0");
|
||||
addColumnIfMissing(table, columns, "audio_delay_millis", "INTEGER", "0");
|
||||
addColumnIfMissing(table, columns, "audio_speed", "REAL", "1.0");
|
||||
addColumnIfMissing(table, columns, "audio_pitch", "REAL", "1.0");
|
||||
addColumnIfMissing(table, columns, "audio_volume", "REAL", "1.0");
|
||||
addColumnIfMissing(table, columns, "preview", "TEXT", "NULL");
|
||||
addColumnIfMissing(table, columns, "asset_type", "TEXT", "'OTHER'");
|
||||
ensureAssetTypeTables(columns);
|
||||
}
|
||||
|
||||
private void ensureAssetTypeTables(List<String> assetColumns) {
|
||||
try {
|
||||
jdbcTemplate.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS visual_assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
preview TEXT,
|
||||
x REAL NOT NULL DEFAULT 0,
|
||||
y REAL NOT NULL DEFAULT 0,
|
||||
width REAL NOT NULL DEFAULT 0,
|
||||
height REAL NOT NULL DEFAULT 0,
|
||||
rotation REAL NOT NULL DEFAULT 0,
|
||||
speed REAL,
|
||||
muted BOOLEAN,
|
||||
media_type TEXT,
|
||||
original_media_type TEXT,
|
||||
z_index INTEGER,
|
||||
audio_volume REAL,
|
||||
hidden BOOLEAN
|
||||
)
|
||||
"""
|
||||
);
|
||||
jdbcTemplate.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS audio_assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
media_type TEXT,
|
||||
original_media_type TEXT,
|
||||
audio_loop BOOLEAN,
|
||||
audio_delay_millis INTEGER,
|
||||
audio_speed REAL,
|
||||
audio_pitch REAL,
|
||||
audio_volume REAL,
|
||||
hidden BOOLEAN
|
||||
)
|
||||
"""
|
||||
);
|
||||
jdbcTemplate.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS script_assets (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
media_type TEXT,
|
||||
original_media_type TEXT
|
||||
)
|
||||
"""
|
||||
);
|
||||
backfillAssetTypes(assetColumns);
|
||||
} catch (DataAccessException ex) {
|
||||
logger.warn("Unable to ensure asset type tables", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void backfillAssetTypes(List<String> assetColumns) {
|
||||
if (!assetColumns.contains("media_type")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
jdbcTemplate.execute(
|
||||
"""
|
||||
UPDATE assets
|
||||
SET asset_type = CASE
|
||||
WHEN media_type LIKE 'audio/%' THEN 'AUDIO'
|
||||
WHEN media_type LIKE 'video/%' THEN 'VIDEO'
|
||||
WHEN media_type LIKE 'image/%' THEN 'IMAGE'
|
||||
WHEN media_type LIKE 'application/javascript%' THEN 'SCRIPT'
|
||||
WHEN media_type LIKE 'text/javascript%' THEN 'SCRIPT'
|
||||
ELSE COALESCE(asset_type, 'OTHER')
|
||||
END
|
||||
WHERE asset_type IS NULL OR asset_type = '' OR asset_type = 'OTHER'
|
||||
"""
|
||||
);
|
||||
jdbcTemplate.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO visual_assets (
|
||||
id, name, preview, x, y, width, height, rotation, speed, muted, media_type,
|
||||
original_media_type, z_index, audio_volume, hidden
|
||||
)
|
||||
SELECT id, name, preview, x, y, width, height, rotation, speed, muted, media_type,
|
||||
original_media_type, z_index, audio_volume, hidden
|
||||
FROM assets
|
||||
WHERE asset_type IN ('IMAGE', 'VIDEO', 'OTHER')
|
||||
"""
|
||||
);
|
||||
jdbcTemplate.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO audio_assets (
|
||||
id, name, media_type, original_media_type, audio_loop, audio_delay_millis,
|
||||
audio_speed, audio_pitch, audio_volume, hidden
|
||||
)
|
||||
SELECT id, name, media_type, original_media_type, audio_loop, audio_delay_millis,
|
||||
audio_speed, audio_pitch, audio_volume, hidden
|
||||
FROM assets
|
||||
WHERE asset_type = 'AUDIO'
|
||||
"""
|
||||
);
|
||||
jdbcTemplate.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO script_assets (
|
||||
id, name, media_type, original_media_type
|
||||
)
|
||||
SELECT id, name, media_type, original_media_type
|
||||
FROM assets
|
||||
WHERE asset_type = 'SCRIPT'
|
||||
"""
|
||||
);
|
||||
} catch (DataAccessException ex) {
|
||||
logger.warn("Unable to backfill asset type tables", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void addColumnIfMissing(
|
||||
|
||||
@@ -2,6 +2,8 @@ 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;
|
||||
@@ -20,14 +22,9 @@ public class Asset {
|
||||
@Column(nullable = false)
|
||||
private String broadcaster;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(columnDefinition = "TEXT", nullable = false)
|
||||
private String url;
|
||||
|
||||
@Column(columnDefinition = "TEXT", nullable = false)
|
||||
private String preview;
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "asset_type", nullable = false)
|
||||
private AssetType assetType;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
@@ -35,39 +32,12 @@ public class Asset {
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Instant updatedAt;
|
||||
|
||||
private double x;
|
||||
private double y;
|
||||
private double width;
|
||||
private double height;
|
||||
private double rotation;
|
||||
private Double speed;
|
||||
private Boolean muted;
|
||||
private String mediaType;
|
||||
private String originalMediaType;
|
||||
private Integer zIndex;
|
||||
private Boolean audioLoop;
|
||||
private Integer audioDelayMillis;
|
||||
private Double audioSpeed;
|
||||
private Double audioPitch;
|
||||
private Double audioVolume;
|
||||
private boolean hidden;
|
||||
|
||||
public Asset() {}
|
||||
|
||||
public Asset(String broadcaster, String name, String url, double width, double height) {
|
||||
public Asset(String broadcaster, AssetType assetType) {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.broadcaster = normalize(broadcaster);
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.rotation = 0;
|
||||
this.speed = 1.0;
|
||||
this.muted = false;
|
||||
this.zIndex = 1;
|
||||
this.hidden = true;
|
||||
this.assetType = assetType == null ? AssetType.OTHER : assetType;
|
||||
this.createdAt = Instant.now();
|
||||
this.updatedAt = this.createdAt;
|
||||
}
|
||||
@@ -84,32 +54,8 @@ public class Asset {
|
||||
}
|
||||
this.updatedAt = now;
|
||||
this.broadcaster = normalize(broadcaster);
|
||||
if (this.name == null || this.name.isBlank()) {
|
||||
this.name = this.id;
|
||||
}
|
||||
if (this.speed == null) {
|
||||
this.speed = 1.0;
|
||||
}
|
||||
if (this.muted == null) {
|
||||
this.muted = Boolean.FALSE;
|
||||
}
|
||||
if (this.zIndex == null || this.zIndex < 1) {
|
||||
this.zIndex = 1;
|
||||
}
|
||||
if (this.audioLoop == null) {
|
||||
this.audioLoop = Boolean.FALSE;
|
||||
}
|
||||
if (this.audioDelayMillis == null) {
|
||||
this.audioDelayMillis = 0;
|
||||
}
|
||||
if (this.audioSpeed == null) {
|
||||
this.audioSpeed = 1.0;
|
||||
}
|
||||
if (this.audioPitch == null) {
|
||||
this.audioPitch = 1.0;
|
||||
}
|
||||
if (this.audioVolume == null) {
|
||||
this.audioVolume = 1.0;
|
||||
if (this.assetType == null) {
|
||||
this.assetType = AssetType.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,112 +71,12 @@ public class Asset {
|
||||
this.broadcaster = normalize(broadcaster);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
public AssetType getAssetType() {
|
||||
return assetType == null ? AssetType.OTHER : assetType;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public void setX(double x) {
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public void setY(double y) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(double width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(double height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public double getRotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public void setRotation(double rotation) {
|
||||
this.rotation = rotation;
|
||||
}
|
||||
|
||||
public double getSpeed() {
|
||||
return speed == null ? 1.0 : speed;
|
||||
}
|
||||
|
||||
public void setSpeed(double speed) {
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public boolean isMuted() {
|
||||
return muted != null && muted;
|
||||
}
|
||||
|
||||
public void setMuted(boolean muted) {
|
||||
this.muted = muted;
|
||||
}
|
||||
|
||||
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 String getPreview() {
|
||||
return preview;
|
||||
}
|
||||
|
||||
public void setPreview(String preview) {
|
||||
this.preview = preview;
|
||||
}
|
||||
|
||||
public boolean isVideo() {
|
||||
return mediaType != null && mediaType.toLowerCase(Locale.ROOT).startsWith("video/");
|
||||
}
|
||||
|
||||
public boolean isHidden() {
|
||||
return hidden;
|
||||
}
|
||||
|
||||
public void setHidden(boolean hidden) {
|
||||
this.hidden = hidden;
|
||||
public void setAssetType(AssetType assetType) {
|
||||
this.assetType = assetType == null ? AssetType.OTHER : assetType;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
@@ -249,54 +95,6 @@ public class Asset {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public Integer getZIndex() {
|
||||
return zIndex == null ? 1 : Math.max(1, zIndex);
|
||||
}
|
||||
|
||||
public void setZIndex(Integer zIndex) {
|
||||
this.zIndex = zIndex == null ? null : Math.max(1, zIndex);
|
||||
}
|
||||
|
||||
public boolean isAudioLoop() {
|
||||
return audioLoop != null && audioLoop;
|
||||
}
|
||||
|
||||
public void setAudioLoop(Boolean audioLoop) {
|
||||
this.audioLoop = audioLoop;
|
||||
}
|
||||
|
||||
public Integer getAudioDelayMillis() {
|
||||
return audioDelayMillis == null ? 0 : Math.max(0, audioDelayMillis);
|
||||
}
|
||||
|
||||
public void setAudioDelayMillis(Integer audioDelayMillis) {
|
||||
this.audioDelayMillis = audioDelayMillis;
|
||||
}
|
||||
|
||||
public double getAudioSpeed() {
|
||||
return audioSpeed == null ? 1.0 : Math.max(0.1, audioSpeed);
|
||||
}
|
||||
|
||||
public void setAudioSpeed(Double audioSpeed) {
|
||||
this.audioSpeed = audioSpeed;
|
||||
}
|
||||
|
||||
public double getAudioPitch() {
|
||||
return audioPitch == null ? 1.0 : Math.max(0.1, audioPitch);
|
||||
}
|
||||
|
||||
public void setAudioPitch(Double audioPitch) {
|
||||
this.audioPitch = audioPitch;
|
||||
}
|
||||
|
||||
public double getAudioVolume() {
|
||||
return audioVolume == null ? 1.0 : Math.max(0.0, Math.min(1.0, audioVolume));
|
||||
}
|
||||
|
||||
public void setAudioVolume(Double audioVolume) {
|
||||
this.audioVolume = audioVolume;
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
return value == null ? null : value.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@@ -24,39 +24,43 @@ public record AssetPatch(
|
||||
Double audioPitch,
|
||||
Double audioVolume
|
||||
) {
|
||||
public static TransformSnapshot capture(Asset asset) {
|
||||
return new TransformSnapshot(
|
||||
asset.getX(),
|
||||
asset.getY(),
|
||||
asset.getWidth(),
|
||||
asset.getHeight(),
|
||||
asset.getRotation(),
|
||||
asset.getSpeed(),
|
||||
asset.isMuted(),
|
||||
asset.getZIndex(),
|
||||
asset.isAudioLoop(),
|
||||
asset.getAudioDelayMillis(),
|
||||
asset.getAudioSpeed(),
|
||||
asset.getAudioPitch(),
|
||||
asset.getAudioVolume()
|
||||
/**
|
||||
* Produces a minimal patch from a visual transform operation.
|
||||
*/
|
||||
public static AssetPatch fromVisualTransform(VisualSnapshot before, VisualAsset asset, TransformRequest request) {
|
||||
return new AssetPatch(
|
||||
asset.getId(),
|
||||
request.getX() != null ? changed(before.x(), asset.getX()) : null,
|
||||
request.getY() != null ? changed(before.y(), asset.getY()) : null,
|
||||
request.getWidth() != null ? changed(before.width(), asset.getWidth()) : null,
|
||||
request.getHeight() != null ? changed(before.height(), asset.getHeight()) : null,
|
||||
request.getRotation() != null ? changed(before.rotation(), asset.getRotation()) : null,
|
||||
request.getSpeed() != null ? changed(before.speed(), asset.getSpeed()) : null,
|
||||
request.getMuted() != null ? changed(before.muted(), asset.isMuted()) : null,
|
||||
request.getZIndex() != null ? changed(before.zIndex(), asset.getZIndex()) : null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
request.getAudioVolume() != null ? changed(before.audioVolume(), asset.getAudioVolume()) : null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a minimal patch from a transform operation. Only fields that changed and were part of
|
||||
* the incoming request are populated to keep WebSocket payloads small.
|
||||
* Produces a minimal patch from an audio update operation.
|
||||
*/
|
||||
public static AssetPatch fromTransform(TransformSnapshot before, Asset asset, TransformRequest request) {
|
||||
public static AssetPatch fromAudioTransform(AudioSnapshot before, AudioAsset asset, TransformRequest request) {
|
||||
return new AssetPatch(
|
||||
asset.getId(),
|
||||
changed(before.x(), asset.getX()),
|
||||
changed(before.y(), asset.getY()),
|
||||
changed(before.width(), asset.getWidth()),
|
||||
changed(before.height(), asset.getHeight()),
|
||||
changed(before.rotation(), asset.getRotation()),
|
||||
request.getSpeed() != null ? changed(before.speed(), asset.getSpeed()) : null,
|
||||
request.getMuted() != null ? changed(before.muted(), asset.isMuted()) : null,
|
||||
request.getZIndex() != null ? changed(before.zIndex(), asset.getZIndex()) : null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
request.getAudioLoop() != null ? changed(before.audioLoop(), asset.isAudioLoop()) : null,
|
||||
request.getAudioDelayMillis() != null
|
||||
@@ -68,9 +72,9 @@ public record AssetPatch(
|
||||
);
|
||||
}
|
||||
|
||||
public static AssetPatch fromVisibility(Asset asset) {
|
||||
public static AssetPatch fromVisibility(String assetId, boolean hidden) {
|
||||
return new AssetPatch(
|
||||
asset.getId(),
|
||||
assetId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@@ -79,7 +83,7 @@ public record AssetPatch(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
asset.isHidden(),
|
||||
hidden,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
@@ -100,7 +104,7 @@ public record AssetPatch(
|
||||
return before == after ? null : after;
|
||||
}
|
||||
|
||||
public record TransformSnapshot(
|
||||
public record VisualSnapshot(
|
||||
double x,
|
||||
double y,
|
||||
double width,
|
||||
@@ -109,6 +113,10 @@ public record AssetPatch(
|
||||
double speed,
|
||||
boolean muted,
|
||||
int zIndex,
|
||||
double audioVolume
|
||||
) {}
|
||||
|
||||
public record AudioSnapshot(
|
||||
boolean audioLoop,
|
||||
int audioDelayMillis,
|
||||
double audioSpeed,
|
||||
|
||||
32
src/main/java/dev/kruhlmann/imgfloat/model/AssetType.java
Normal file
32
src/main/java/dev/kruhlmann/imgfloat/model/AssetType.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum AssetType {
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
SCRIPT,
|
||||
OTHER;
|
||||
|
||||
public static AssetType fromMediaType(String mediaType, String originalMediaType) {
|
||||
String raw = mediaType != null && !mediaType.isBlank() ? mediaType : originalMediaType;
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return OTHER;
|
||||
}
|
||||
String normalized = raw.toLowerCase(Locale.ROOT);
|
||||
if (normalized.startsWith("image/")) {
|
||||
return IMAGE;
|
||||
}
|
||||
if (normalized.startsWith("video/")) {
|
||||
return VIDEO;
|
||||
}
|
||||
if (normalized.startsWith("audio/")) {
|
||||
return AUDIO;
|
||||
}
|
||||
if (normalized.startsWith("application/javascript") || normalized.startsWith("text/javascript")) {
|
||||
return SCRIPT;
|
||||
}
|
||||
return OTHER;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ public record AssetView(
|
||||
Boolean muted,
|
||||
String mediaType,
|
||||
String originalMediaType,
|
||||
AssetType assetType,
|
||||
Integer zIndex,
|
||||
Boolean audioLoop,
|
||||
Integer audioDelayMillis,
|
||||
@@ -28,32 +29,92 @@ public record AssetView(
|
||||
Instant createdAt,
|
||||
Instant updatedAt
|
||||
) {
|
||||
public static AssetView from(String broadcaster, Asset asset) {
|
||||
public static AssetView fromVisual(String broadcaster, Asset asset, VisualAsset visual) {
|
||||
boolean hasPreview = visual.getPreview() != null && !visual.getPreview().isBlank();
|
||||
return new AssetView(
|
||||
asset.getId(),
|
||||
asset.getBroadcaster(),
|
||||
asset.getName(),
|
||||
visual.getName(),
|
||||
"/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/content",
|
||||
asset.getPreview() != null && !asset.getPreview().isBlank()
|
||||
? "/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/preview"
|
||||
: null,
|
||||
asset.getX(),
|
||||
asset.getY(),
|
||||
asset.getWidth(),
|
||||
asset.getHeight(),
|
||||
asset.getRotation(),
|
||||
asset.getSpeed(),
|
||||
asset.isMuted(),
|
||||
asset.getMediaType(),
|
||||
asset.getOriginalMediaType(),
|
||||
asset.getZIndex(),
|
||||
asset.isAudioLoop(),
|
||||
asset.getAudioDelayMillis(),
|
||||
asset.getAudioSpeed(),
|
||||
asset.getAudioPitch(),
|
||||
asset.getAudioVolume(),
|
||||
asset.isHidden(),
|
||||
asset.getPreview() != null && !asset.getPreview().isBlank(),
|
||||
hasPreview ? "/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/preview" : null,
|
||||
visual.getX(),
|
||||
visual.getY(),
|
||||
visual.getWidth(),
|
||||
visual.getHeight(),
|
||||
visual.getRotation(),
|
||||
visual.getSpeed(),
|
||||
visual.isMuted(),
|
||||
visual.getMediaType(),
|
||||
visual.getOriginalMediaType(),
|
||||
asset.getAssetType(),
|
||||
visual.getZIndex(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
visual.getAudioVolume(),
|
||||
visual.isHidden(),
|
||||
hasPreview,
|
||||
asset.getCreatedAt(),
|
||||
asset.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
public static AssetView fromAudio(String broadcaster, Asset asset, AudioAsset audio) {
|
||||
return new AssetView(
|
||||
asset.getId(),
|
||||
asset.getBroadcaster(),
|
||||
audio.getName(),
|
||||
"/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/content",
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
audio.getMediaType(),
|
||||
audio.getOriginalMediaType(),
|
||||
asset.getAssetType(),
|
||||
null,
|
||||
audio.isAudioLoop(),
|
||||
audio.getAudioDelayMillis(),
|
||||
audio.getAudioSpeed(),
|
||||
audio.getAudioPitch(),
|
||||
audio.getAudioVolume(),
|
||||
audio.isHidden(),
|
||||
false,
|
||||
asset.getCreatedAt(),
|
||||
asset.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
public static AssetView fromScript(String broadcaster, Asset asset, ScriptAsset script) {
|
||||
return new AssetView(
|
||||
asset.getId(),
|
||||
asset.getBroadcaster(),
|
||||
script.getName(),
|
||||
"/api/channels/" + broadcaster + "/assets/" + asset.getId() + "/content",
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
script.getMediaType(),
|
||||
script.getOriginalMediaType(),
|
||||
asset.getAssetType(),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
asset.getCreatedAt(),
|
||||
asset.getUpdatedAt()
|
||||
);
|
||||
|
||||
144
src/main/java/dev/kruhlmann/imgfloat/model/AudioAsset.java
Normal file
144
src/main/java/dev/kruhlmann/imgfloat/model/AudioAsset.java
Normal file
@@ -0,0 +1,144 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "audio_assets")
|
||||
public class AudioAsset {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
private String mediaType;
|
||||
private String originalMediaType;
|
||||
private Boolean audioLoop;
|
||||
private Integer audioDelayMillis;
|
||||
private Double audioSpeed;
|
||||
private Double audioPitch;
|
||||
private Double audioVolume;
|
||||
private boolean hidden;
|
||||
|
||||
public AudioAsset() {}
|
||||
|
||||
public AudioAsset(String assetId, String name) {
|
||||
this.id = assetId;
|
||||
this.name = name;
|
||||
this.audioLoop = Boolean.FALSE;
|
||||
this.audioDelayMillis = 0;
|
||||
this.audioSpeed = 1.0;
|
||||
this.audioPitch = 1.0;
|
||||
this.audioVolume = 1.0;
|
||||
this.hidden = true;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
@PreUpdate
|
||||
public void prepare() {
|
||||
if (this.audioLoop == null) {
|
||||
this.audioLoop = Boolean.FALSE;
|
||||
}
|
||||
if (this.audioDelayMillis == null) {
|
||||
this.audioDelayMillis = 0;
|
||||
}
|
||||
if (this.audioSpeed == null) {
|
||||
this.audioSpeed = 1.0;
|
||||
}
|
||||
if (this.audioPitch == null) {
|
||||
this.audioPitch = 1.0;
|
||||
}
|
||||
if (this.audioVolume == null) {
|
||||
this.audioVolume = 1.0;
|
||||
}
|
||||
if (this.name == null || this.name.isBlank()) {
|
||||
this.name = this.id;
|
||||
}
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
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 boolean isAudioLoop() {
|
||||
return audioLoop != null && audioLoop;
|
||||
}
|
||||
|
||||
public void setAudioLoop(Boolean audioLoop) {
|
||||
this.audioLoop = audioLoop;
|
||||
}
|
||||
|
||||
public Integer getAudioDelayMillis() {
|
||||
return audioDelayMillis == null ? 0 : Math.max(0, audioDelayMillis);
|
||||
}
|
||||
|
||||
public void setAudioDelayMillis(Integer audioDelayMillis) {
|
||||
this.audioDelayMillis = audioDelayMillis;
|
||||
}
|
||||
|
||||
public double getAudioSpeed() {
|
||||
return audioSpeed == null ? 1.0 : Math.max(0.1, audioSpeed);
|
||||
}
|
||||
|
||||
public void setAudioSpeed(Double audioSpeed) {
|
||||
this.audioSpeed = audioSpeed;
|
||||
}
|
||||
|
||||
public double getAudioPitch() {
|
||||
return audioPitch == null ? 1.0 : Math.max(0.1, audioPitch);
|
||||
}
|
||||
|
||||
public void setAudioPitch(Double audioPitch) {
|
||||
this.audioPitch = audioPitch;
|
||||
}
|
||||
|
||||
public double getAudioVolume() {
|
||||
return audioVolume == null ? 1.0 : Math.max(0.0, Math.min(1.0, audioVolume));
|
||||
}
|
||||
|
||||
public void setAudioVolume(Double audioVolume) {
|
||||
this.audioVolume = audioVolume;
|
||||
}
|
||||
|
||||
public boolean isHidden() {
|
||||
return hidden;
|
||||
}
|
||||
|
||||
public void setHidden(boolean hidden) {
|
||||
this.hidden = hidden;
|
||||
}
|
||||
}
|
||||
69
src/main/java/dev/kruhlmann/imgfloat/model/ScriptAsset.java
Normal file
69
src/main/java/dev/kruhlmann/imgfloat/model/ScriptAsset.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "script_assets")
|
||||
public class ScriptAsset {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
private String mediaType;
|
||||
private String originalMediaType;
|
||||
|
||||
public ScriptAsset() {}
|
||||
|
||||
public ScriptAsset(String assetId, String name) {
|
||||
this.id = assetId;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
@PreUpdate
|
||||
public void prepare() {
|
||||
if (this.name == null || this.name.isBlank()) {
|
||||
this.name = this.id;
|
||||
}
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,16 @@ import jakarta.validation.constraints.PositiveOrZero;
|
||||
|
||||
public class TransformRequest {
|
||||
|
||||
private double x;
|
||||
private double y;
|
||||
private Double x;
|
||||
private Double y;
|
||||
|
||||
@Positive(message = "Width must be greater than 0")
|
||||
private double width;
|
||||
private Double width;
|
||||
|
||||
@Positive(message = "Height must be greater than 0")
|
||||
private double height;
|
||||
private Double height;
|
||||
|
||||
private double rotation;
|
||||
private Double rotation;
|
||||
|
||||
@DecimalMin(value = "0.0", message = "Playback speed cannot be negative")
|
||||
@DecimalMax(value = "4.0", message = "Playback speed cannot exceed 4.0")
|
||||
@@ -44,43 +44,43 @@ public class TransformRequest {
|
||||
@DecimalMax(value = "1.0", message = "Audio volume cannot exceed 1.0")
|
||||
private Double audioVolume;
|
||||
|
||||
public double getX() {
|
||||
public Double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public void setX(double x) {
|
||||
public void setX(Double x) {
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
public Double getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public void setY(double y) {
|
||||
public void setY(Double y) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
public Double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(double width) {
|
||||
public void setWidth(Double width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
public Double getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(double height) {
|
||||
public void setHeight(Double height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public double getRotation() {
|
||||
public Double getRotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public void setRotation(double rotation) {
|
||||
public void setRotation(Double rotation) {
|
||||
this.rotation = rotation;
|
||||
}
|
||||
|
||||
|
||||
191
src/main/java/dev/kruhlmann/imgfloat/model/VisualAsset.java
Normal file
191
src/main/java/dev/kruhlmann/imgfloat/model/VisualAsset.java
Normal file
@@ -0,0 +1,191 @@
|
||||
package dev.kruhlmann.imgfloat.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "visual_assets")
|
||||
public class VisualAsset {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
private String preview;
|
||||
|
||||
private double x;
|
||||
private double y;
|
||||
private double width;
|
||||
private double height;
|
||||
private double rotation;
|
||||
private Double speed;
|
||||
private Boolean muted;
|
||||
private String mediaType;
|
||||
private String originalMediaType;
|
||||
private Integer zIndex;
|
||||
private Double audioVolume;
|
||||
private boolean hidden;
|
||||
|
||||
public VisualAsset() {}
|
||||
|
||||
public VisualAsset(String assetId, String name, double width, double height) {
|
||||
this.id = assetId;
|
||||
this.name = name;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.rotation = 0;
|
||||
this.speed = 1.0;
|
||||
this.muted = false;
|
||||
this.zIndex = 1;
|
||||
this.audioVolume = 1.0;
|
||||
this.hidden = true;
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
@PreUpdate
|
||||
public void prepare() {
|
||||
if (this.speed == null) {
|
||||
this.speed = 1.0;
|
||||
}
|
||||
if (this.muted == null) {
|
||||
this.muted = Boolean.FALSE;
|
||||
}
|
||||
if (this.zIndex == null || this.zIndex < 1) {
|
||||
this.zIndex = 1;
|
||||
}
|
||||
if (this.audioVolume == null) {
|
||||
this.audioVolume = 1.0;
|
||||
}
|
||||
if (this.name == null || this.name.isBlank()) {
|
||||
this.name = this.id;
|
||||
}
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getPreview() {
|
||||
return preview;
|
||||
}
|
||||
|
||||
public void setPreview(String preview) {
|
||||
this.preview = preview;
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public void setX(double x) {
|
||||
this.x = x;
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public void setY(double y) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(double width) {
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(double height) {
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public double getRotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public void setRotation(double rotation) {
|
||||
this.rotation = rotation;
|
||||
}
|
||||
|
||||
public double getSpeed() {
|
||||
return speed == null ? 1.0 : speed;
|
||||
}
|
||||
|
||||
public void setSpeed(Double speed) {
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public boolean isMuted() {
|
||||
return muted != null && muted;
|
||||
}
|
||||
|
||||
public void setMuted(Boolean muted) {
|
||||
this.muted = muted;
|
||||
}
|
||||
|
||||
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 Integer getZIndex() {
|
||||
return zIndex == null ? 1 : Math.max(1, zIndex);
|
||||
}
|
||||
|
||||
public void setZIndex(Integer zIndex) {
|
||||
this.zIndex = zIndex == null ? null : Math.max(1, zIndex);
|
||||
}
|
||||
|
||||
public double getAudioVolume() {
|
||||
return audioVolume == null ? 1.0 : Math.max(0.0, Math.min(1.0, audioVolume));
|
||||
}
|
||||
|
||||
public void setAudioVolume(Double audioVolume) {
|
||||
this.audioVolume = audioVolume;
|
||||
}
|
||||
|
||||
public boolean isHidden() {
|
||||
return hidden;
|
||||
}
|
||||
|
||||
public void setHidden(boolean hidden) {
|
||||
this.hidden = hidden;
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,4 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AssetRepository extends JpaRepository<Asset, String> {
|
||||
List<Asset> findByBroadcaster(String broadcaster);
|
||||
List<Asset> findByBroadcasterAndHiddenFalse(String broadcaster);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package dev.kruhlmann.imgfloat.repository;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.AudioAsset;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AudioAssetRepository extends JpaRepository<AudioAsset, String> {
|
||||
List<AudioAsset> findByIdIn(Collection<String> ids);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package dev.kruhlmann.imgfloat.repository;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAsset;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ScriptAssetRepository extends JpaRepository<ScriptAsset, String> {
|
||||
List<ScriptAsset> findByIdIn(Collection<String> ids);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dev.kruhlmann.imgfloat.repository;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.VisualAsset;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface VisualAssetRepository extends JpaRepository<VisualAsset, String> {
|
||||
List<VisualAsset> findByIdIn(Collection<String> ids);
|
||||
List<VisualAsset> findByIdInAndHiddenFalse(Collection<String> ids);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package dev.kruhlmann.imgfloat.service;
|
||||
|
||||
import dev.kruhlmann.imgfloat.model.Asset;
|
||||
import dev.kruhlmann.imgfloat.service.media.AssetContent;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
@@ -97,53 +96,57 @@ public class AssetStorageService {
|
||||
logger.info("Wrote asset to {}", file);
|
||||
}
|
||||
|
||||
public Optional<AssetContent> loadAssetFile(Asset asset) {
|
||||
public Optional<AssetContent> loadAssetFile(String broadcaster, String assetId, String mediaType) {
|
||||
try {
|
||||
Path file = assetPath(asset.getBroadcaster(), asset.getId(), asset.getMediaType());
|
||||
Path file = assetPath(broadcaster, assetId, mediaType);
|
||||
|
||||
if (!Files.exists(file)) return Optional.empty();
|
||||
|
||||
byte[] bytes = Files.readAllBytes(file);
|
||||
return Optional.of(new AssetContent(bytes, asset.getMediaType()));
|
||||
return Optional.of(new AssetContent(bytes, mediaType));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to load asset {}", asset.getId(), e);
|
||||
logger.warn("Failed to load asset {}", assetId, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<AssetContent> loadPreview(Asset asset) {
|
||||
public Optional<AssetContent> loadPreview(String broadcaster, String assetId) {
|
||||
try {
|
||||
Path file = previewPath(asset.getBroadcaster(), asset.getId());
|
||||
Path file = previewPath(broadcaster, assetId);
|
||||
if (!Files.exists(file)) return Optional.empty();
|
||||
|
||||
byte[] bytes = Files.readAllBytes(file);
|
||||
return Optional.of(new AssetContent(bytes, "image/png"));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to load preview {}", asset.getId(), e);
|
||||
logger.warn("Failed to load preview {}", assetId, e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<AssetContent> loadAssetFileSafely(Asset asset) {
|
||||
if (asset.getUrl() == null) {
|
||||
public Optional<AssetContent> loadAssetFileSafely(String broadcaster, String assetId, String mediaType) {
|
||||
if (mediaType == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return loadAssetFile(asset);
|
||||
return loadAssetFile(broadcaster, assetId, mediaType);
|
||||
}
|
||||
|
||||
public Optional<AssetContent> loadPreviewSafely(Asset asset) {
|
||||
if (asset.getPreview() == null) {
|
||||
public Optional<AssetContent> loadPreviewSafely(String broadcaster, String assetId, boolean hasPreview) {
|
||||
if (!hasPreview) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return loadPreview(asset);
|
||||
return loadPreview(broadcaster, assetId);
|
||||
}
|
||||
|
||||
public void deleteAsset(Asset asset) {
|
||||
public void deleteAsset(String broadcaster, String assetId, String mediaType, boolean hasPreview) {
|
||||
try {
|
||||
Files.deleteIfExists(assetPath(asset.getBroadcaster(), asset.getId(), asset.getMediaType()));
|
||||
Files.deleteIfExists(previewPath(asset.getBroadcaster(), asset.getId()));
|
||||
if (mediaType != null) {
|
||||
Files.deleteIfExists(assetPath(broadcaster, assetId, mediaType));
|
||||
}
|
||||
if (hasPreview) {
|
||||
Files.deleteIfExists(previewPath(broadcaster, assetId));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to delete asset {}", asset.getId(), e);
|
||||
logger.warn("Failed to delete asset {}", assetId, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,17 +6,24 @@ import static org.springframework.http.HttpStatus.PAYLOAD_TOO_LARGE;
|
||||
import dev.kruhlmann.imgfloat.model.Asset;
|
||||
import dev.kruhlmann.imgfloat.model.AssetEvent;
|
||||
import dev.kruhlmann.imgfloat.model.AssetPatch;
|
||||
import dev.kruhlmann.imgfloat.model.AssetType;
|
||||
import dev.kruhlmann.imgfloat.model.AssetView;
|
||||
import dev.kruhlmann.imgfloat.model.AudioAsset;
|
||||
import dev.kruhlmann.imgfloat.model.CanvasEvent;
|
||||
import dev.kruhlmann.imgfloat.model.CanvasSettingsRequest;
|
||||
import dev.kruhlmann.imgfloat.model.Channel;
|
||||
import dev.kruhlmann.imgfloat.model.CodeAssetRequest;
|
||||
import dev.kruhlmann.imgfloat.model.PlaybackRequest;
|
||||
import dev.kruhlmann.imgfloat.model.ScriptAsset;
|
||||
import dev.kruhlmann.imgfloat.model.Settings;
|
||||
import dev.kruhlmann.imgfloat.model.TransformRequest;
|
||||
import dev.kruhlmann.imgfloat.model.VisibilityRequest;
|
||||
import dev.kruhlmann.imgfloat.model.VisualAsset;
|
||||
import dev.kruhlmann.imgfloat.repository.AssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.AudioAssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ChannelRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.ScriptAssetRepository;
|
||||
import dev.kruhlmann.imgfloat.repository.VisualAssetRepository;
|
||||
import dev.kruhlmann.imgfloat.service.media.AssetContent;
|
||||
import dev.kruhlmann.imgfloat.service.media.MediaDetectionService;
|
||||
import dev.kruhlmann.imgfloat.service.media.MediaOptimizationService;
|
||||
@@ -25,6 +32,7 @@ import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -42,6 +50,9 @@ public class ChannelDirectoryService {
|
||||
|
||||
private final ChannelRepository channelRepository;
|
||||
private final AssetRepository assetRepository;
|
||||
private final VisualAssetRepository visualAssetRepository;
|
||||
private final AudioAssetRepository audioAssetRepository;
|
||||
private final ScriptAssetRepository scriptAssetRepository;
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final AssetStorageService assetStorageService;
|
||||
private final MediaDetectionService mediaDetectionService;
|
||||
@@ -53,6 +64,9 @@ public class ChannelDirectoryService {
|
||||
public ChannelDirectoryService(
|
||||
ChannelRepository channelRepository,
|
||||
AssetRepository assetRepository,
|
||||
VisualAssetRepository visualAssetRepository,
|
||||
AudioAssetRepository audioAssetRepository,
|
||||
ScriptAssetRepository scriptAssetRepository,
|
||||
SimpMessagingTemplate messagingTemplate,
|
||||
AssetStorageService assetStorageService,
|
||||
MediaDetectionService mediaDetectionService,
|
||||
@@ -62,6 +76,9 @@ public class ChannelDirectoryService {
|
||||
) {
|
||||
this.channelRepository = channelRepository;
|
||||
this.assetRepository = assetRepository;
|
||||
this.visualAssetRepository = visualAssetRepository;
|
||||
this.audioAssetRepository = audioAssetRepository;
|
||||
this.scriptAssetRepository = scriptAssetRepository;
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
this.assetStorageService = assetStorageService;
|
||||
this.mediaDetectionService = mediaDetectionService;
|
||||
@@ -111,7 +128,28 @@ public class ChannelDirectoryService {
|
||||
|
||||
public Collection<AssetView> getVisibleAssets(String broadcaster) {
|
||||
String normalized = normalize(broadcaster);
|
||||
return sortAndMapAssets(normalized, assetRepository.findByBroadcasterAndHiddenFalse(normalized));
|
||||
List<Asset> assets = assetRepository.findByBroadcaster(normalized);
|
||||
List<String> visualIds = assets
|
||||
.stream()
|
||||
.filter(
|
||||
(asset) ->
|
||||
asset.getAssetType() == AssetType.IMAGE ||
|
||||
asset.getAssetType() == AssetType.VIDEO ||
|
||||
asset.getAssetType() == AssetType.OTHER
|
||||
)
|
||||
.map(Asset::getId)
|
||||
.toList();
|
||||
Map<String, Asset> assetById = assets.stream().collect(Collectors.toMap(Asset::getId, (asset) -> asset));
|
||||
return visualAssetRepository
|
||||
.findByIdInAndHiddenFalse(visualIds)
|
||||
.stream()
|
||||
.map((visual) -> {
|
||||
Asset asset = assetById.get(visual.getId());
|
||||
return asset == null ? null : AssetView.fromVisual(normalized, asset, visual);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(Comparator.comparingInt(AssetView::zIndex))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public CanvasSettingsRequest getCanvasSettings(String broadcaster) {
|
||||
@@ -159,14 +197,8 @@ public class ChannelDirectoryService {
|
||||
|
||||
boolean isAudio = optimized.mediaType().startsWith("audio/");
|
||||
boolean isCode = isCodeMediaType(optimized.mediaType()) || isCodeMediaType(mediaType);
|
||||
double defaultWidth = isAudio ? 400 : isCode ? 480 : 640;
|
||||
double defaultHeight = isAudio ? 80 : isCode ? 270 : 360;
|
||||
double width = optimized.width() > 0 ? optimized.width() : defaultWidth;
|
||||
double height = optimized.height() > 0 ? optimized.height() : defaultHeight;
|
||||
|
||||
Asset asset = new Asset(channel.getBroadcaster(), safeName, "", width, height);
|
||||
asset.setOriginalMediaType(mediaType);
|
||||
asset.setMediaType(optimized.mediaType());
|
||||
AssetType assetType = AssetType.fromMediaType(optimized.mediaType(), mediaType);
|
||||
Asset asset = new Asset(channel.getBroadcaster(), assetType);
|
||||
|
||||
assetStorageService.storeAsset(
|
||||
channel.getBroadcaster(),
|
||||
@@ -175,21 +207,37 @@ public class ChannelDirectoryService {
|
||||
optimized.mediaType()
|
||||
);
|
||||
|
||||
assetStorageService.storePreview(channel.getBroadcaster(), asset.getId(), optimized.previewBytes());
|
||||
asset.setPreview(optimized.previewBytes() != null ? asset.getId() + ".png" : "");
|
||||
AssetView view;
|
||||
asset = assetRepository.save(asset);
|
||||
|
||||
asset.setSpeed(1.0);
|
||||
asset.setMuted(optimized.mediaType().startsWith("video/"));
|
||||
asset.setAudioLoop(false);
|
||||
asset.setAudioDelayMillis(0);
|
||||
asset.setAudioSpeed(1.0);
|
||||
asset.setAudioPitch(1.0);
|
||||
asset.setAudioVolume(1.0);
|
||||
asset.setZIndex(nextZIndex(channel.getBroadcaster()));
|
||||
if (isAudio) {
|
||||
AudioAsset audio = new AudioAsset(asset.getId(), safeName);
|
||||
audio.setMediaType(optimized.mediaType());
|
||||
audio.setOriginalMediaType(mediaType);
|
||||
audioAssetRepository.save(audio);
|
||||
view = AssetView.fromAudio(channel.getBroadcaster(), asset, audio);
|
||||
} else if (isCode) {
|
||||
ScriptAsset script = new ScriptAsset(asset.getId(), safeName);
|
||||
script.setMediaType(optimized.mediaType());
|
||||
script.setOriginalMediaType(mediaType);
|
||||
scriptAssetRepository.save(script);
|
||||
view = AssetView.fromScript(channel.getBroadcaster(), asset, script);
|
||||
} else {
|
||||
double defaultWidth = 640;
|
||||
double defaultHeight = 360;
|
||||
double width = optimized.width() > 0 ? optimized.width() : defaultWidth;
|
||||
double height = optimized.height() > 0 ? optimized.height() : defaultHeight;
|
||||
VisualAsset visual = new VisualAsset(asset.getId(), safeName, width, height);
|
||||
visual.setOriginalMediaType(mediaType);
|
||||
visual.setMediaType(optimized.mediaType());
|
||||
visual.setMuted(optimized.mediaType().startsWith("video/"));
|
||||
visual.setZIndex(nextZIndex(channel.getBroadcaster()));
|
||||
assetStorageService.storePreview(channel.getBroadcaster(), asset.getId(), optimized.previewBytes());
|
||||
visual.setPreview(optimized.previewBytes() != null ? asset.getId() + ".png" : "");
|
||||
visualAssetRepository.save(visual);
|
||||
view = AssetView.fromVisual(channel.getBroadcaster(), asset, visual);
|
||||
}
|
||||
|
||||
assetRepository.save(asset);
|
||||
|
||||
AssetView view = AssetView.from(channel.getBroadcaster(), asset);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.created(broadcaster, view));
|
||||
|
||||
return Optional.of(view);
|
||||
@@ -201,18 +249,7 @@ public class ChannelDirectoryService {
|
||||
byte[] bytes = request.getSource().getBytes(StandardCharsets.UTF_8);
|
||||
enforceUploadLimit(bytes.length);
|
||||
|
||||
Asset asset = new Asset(channel.getBroadcaster(), request.getName().trim(), "", 480, 270);
|
||||
asset.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
asset.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
asset.setPreview("");
|
||||
asset.setSpeed(1.0);
|
||||
asset.setMuted(false);
|
||||
asset.setAudioLoop(false);
|
||||
asset.setAudioDelayMillis(0);
|
||||
asset.setAudioSpeed(1.0);
|
||||
asset.setAudioPitch(1.0);
|
||||
asset.setAudioVolume(1.0);
|
||||
asset.setZIndex(nextZIndex(channel.getBroadcaster()));
|
||||
Asset asset = new Asset(channel.getBroadcaster(), AssetType.SCRIPT);
|
||||
|
||||
try {
|
||||
assetStorageService.storeAsset(channel.getBroadcaster(), asset.getId(), bytes, DEFAULT_CODE_MEDIA_TYPE);
|
||||
@@ -220,8 +257,12 @@ public class ChannelDirectoryService {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Unable to store custom script", e);
|
||||
}
|
||||
|
||||
assetRepository.save(asset);
|
||||
AssetView view = AssetView.from(channel.getBroadcaster(), asset);
|
||||
asset = assetRepository.save(asset);
|
||||
ScriptAsset script = new ScriptAsset(asset.getId(), request.getName().trim());
|
||||
script.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
script.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
scriptAssetRepository.save(script);
|
||||
AssetView view = AssetView.fromScript(channel.getBroadcaster(), asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.created(broadcaster, view));
|
||||
return Optional.of(view);
|
||||
}
|
||||
@@ -236,19 +277,23 @@ public class ChannelDirectoryService {
|
||||
.findById(assetId)
|
||||
.filter((asset) -> normalized.equals(asset.getBroadcaster()))
|
||||
.map((asset) -> {
|
||||
if (!isCodeMediaType(asset.getMediaType()) && !isCodeMediaType(asset.getOriginalMediaType())) {
|
||||
if (asset.getAssetType() != AssetType.SCRIPT) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Asset is not a script");
|
||||
}
|
||||
asset.setName(request.getName().trim());
|
||||
asset.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
asset.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
||||
script.setName(request.getName().trim());
|
||||
script.setOriginalMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
script.setMediaType(DEFAULT_CODE_MEDIA_TYPE);
|
||||
try {
|
||||
assetStorageService.storeAsset(broadcaster, asset.getId(), bytes, DEFAULT_CODE_MEDIA_TYPE);
|
||||
} catch (IOException e) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Unable to store custom script", e);
|
||||
}
|
||||
assetRepository.save(asset);
|
||||
AssetView view = AssetView.from(normalized, asset);
|
||||
scriptAssetRepository.save(script);
|
||||
AssetView view = AssetView.fromScript(normalized, asset, script);
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, view));
|
||||
return view;
|
||||
});
|
||||
@@ -266,28 +311,69 @@ public class ChannelDirectoryService {
|
||||
.findById(assetId)
|
||||
.filter((asset) -> normalized.equals(asset.getBroadcaster()))
|
||||
.map((asset) -> {
|
||||
AssetPatch.TransformSnapshot before = AssetPatch.capture(asset);
|
||||
validateTransform(req);
|
||||
if (asset.getAssetType() == AssetType.AUDIO) {
|
||||
AudioAsset audio = audioAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not audio"));
|
||||
AssetPatch.AudioSnapshot before = new AssetPatch.AudioSnapshot(
|
||||
audio.isAudioLoop(),
|
||||
audio.getAudioDelayMillis(),
|
||||
audio.getAudioSpeed(),
|
||||
audio.getAudioPitch(),
|
||||
audio.getAudioVolume()
|
||||
);
|
||||
validateAudioTransform(req);
|
||||
if (req.getAudioLoop() != null) audio.setAudioLoop(req.getAudioLoop());
|
||||
if (req.getAudioDelayMillis() != null) audio.setAudioDelayMillis(req.getAudioDelayMillis());
|
||||
if (req.getAudioSpeed() != null) audio.setAudioSpeed(req.getAudioSpeed());
|
||||
if (req.getAudioPitch() != null) audio.setAudioPitch(req.getAudioPitch());
|
||||
if (req.getAudioVolume() != null) audio.setAudioVolume(req.getAudioVolume());
|
||||
audioAssetRepository.save(audio);
|
||||
AssetView view = AssetView.fromAudio(normalized, asset, audio);
|
||||
AssetPatch patch = AssetPatch.fromAudioTransform(before, audio, req);
|
||||
if (hasPatchChanges(patch)) {
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, patch));
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
asset.setX(req.getX());
|
||||
asset.setY(req.getY());
|
||||
asset.setWidth(req.getWidth());
|
||||
asset.setHeight(req.getHeight());
|
||||
asset.setRotation(req.getRotation());
|
||||
if (asset.getAssetType() == AssetType.SCRIPT) {
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
||||
return AssetView.fromScript(normalized, asset, script);
|
||||
}
|
||||
|
||||
if (req.getZIndex() != null) asset.setZIndex(req.getZIndex());
|
||||
if (req.getSpeed() != null) asset.setSpeed(req.getSpeed());
|
||||
if (req.getMuted() != null && asset.isVideo()) asset.setMuted(req.getMuted());
|
||||
if (req.getAudioLoop() != null) asset.setAudioLoop(req.getAudioLoop());
|
||||
if (req.getAudioDelayMillis() != null) asset.setAudioDelayMillis(req.getAudioDelayMillis());
|
||||
if (req.getAudioSpeed() != null) asset.setAudioSpeed(req.getAudioSpeed());
|
||||
if (req.getAudioPitch() != null) asset.setAudioPitch(req.getAudioPitch());
|
||||
if (req.getAudioVolume() != null) asset.setAudioVolume(req.getAudioVolume());
|
||||
VisualAsset visual = visualAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not visual"));
|
||||
AssetPatch.VisualSnapshot before = new AssetPatch.VisualSnapshot(
|
||||
visual.getX(),
|
||||
visual.getY(),
|
||||
visual.getWidth(),
|
||||
visual.getHeight(),
|
||||
visual.getRotation(),
|
||||
visual.getSpeed(),
|
||||
visual.isMuted(),
|
||||
visual.getZIndex(),
|
||||
visual.getAudioVolume()
|
||||
);
|
||||
validateVisualTransform(req);
|
||||
|
||||
assetRepository.save(asset);
|
||||
if (req.getX() != null) visual.setX(req.getX());
|
||||
if (req.getY() != null) visual.setY(req.getY());
|
||||
if (req.getWidth() != null) visual.setWidth(req.getWidth());
|
||||
if (req.getHeight() != null) visual.setHeight(req.getHeight());
|
||||
if (req.getRotation() != null) visual.setRotation(req.getRotation());
|
||||
if (req.getZIndex() != null) visual.setZIndex(req.getZIndex());
|
||||
if (req.getSpeed() != null) visual.setSpeed(req.getSpeed());
|
||||
if (req.getMuted() != null) visual.setMuted(req.getMuted());
|
||||
if (req.getAudioVolume() != null) visual.setAudioVolume(req.getAudioVolume());
|
||||
|
||||
AssetView view = AssetView.from(normalized, asset);
|
||||
AssetPatch patch = AssetPatch.fromTransform(before, asset, req);
|
||||
visualAssetRepository.save(visual);
|
||||
|
||||
AssetView view = AssetView.fromVisual(normalized, asset, visual);
|
||||
AssetPatch patch = AssetPatch.fromVisualTransform(before, visual, req);
|
||||
if (hasPatchChanges(patch)) {
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.updated(broadcaster, patch));
|
||||
}
|
||||
@@ -295,21 +381,21 @@ public class ChannelDirectoryService {
|
||||
});
|
||||
}
|
||||
|
||||
private void validateTransform(TransformRequest req) {
|
||||
private void validateVisualTransform(TransformRequest req) {
|
||||
Settings settings = settingsService.get();
|
||||
double maxSpeed = settings.getMaxAssetPlaybackSpeedFraction();
|
||||
double minSpeed = settings.getMinAssetPlaybackSpeedFraction();
|
||||
double minPitch = settings.getMinAssetAudioPitchFraction();
|
||||
double maxPitch = settings.getMaxAssetAudioPitchFraction();
|
||||
double minVolume = settings.getMinAssetVolumeFraction();
|
||||
double maxVolume = settings.getMaxAssetVolumeFraction();
|
||||
int canvasMaxSizePixels = settings.getMaxCanvasSideLengthPixels();
|
||||
|
||||
if (req.getWidth() <= 0 || req.getWidth() > canvasMaxSizePixels) throw new ResponseStatusException(
|
||||
if (
|
||||
req.getWidth() == null || req.getWidth() <= 0 || req.getWidth() > canvasMaxSizePixels
|
||||
) throw new ResponseStatusException(
|
||||
BAD_REQUEST,
|
||||
"Canvas width out of range [0 to " + canvasMaxSizePixels + "]"
|
||||
);
|
||||
if (req.getHeight() <= 0) throw new ResponseStatusException(
|
||||
if (req.getHeight() == null || req.getHeight() <= 0) throw new ResponseStatusException(
|
||||
BAD_REQUEST,
|
||||
"Canvas height out of range [0 to " + canvasMaxSizePixels + "]"
|
||||
);
|
||||
@@ -320,6 +406,23 @@ public class ChannelDirectoryService {
|
||||
BAD_REQUEST,
|
||||
"zIndex must be >= 1"
|
||||
);
|
||||
if (
|
||||
req.getAudioVolume() != null && (req.getAudioVolume() < minVolume || req.getAudioVolume() > maxVolume)
|
||||
) throw new ResponseStatusException(
|
||||
BAD_REQUEST,
|
||||
"Audio volume out of range [" + minVolume + " to " + maxVolume + "]"
|
||||
);
|
||||
}
|
||||
|
||||
private void validateAudioTransform(TransformRequest req) {
|
||||
Settings settings = settingsService.get();
|
||||
double maxSpeed = settings.getMaxAssetPlaybackSpeedFraction();
|
||||
double minSpeed = settings.getMinAssetPlaybackSpeedFraction();
|
||||
double minPitch = settings.getMinAssetAudioPitchFraction();
|
||||
double maxPitch = settings.getMaxAssetAudioPitchFraction();
|
||||
double minVolume = settings.getMinAssetVolumeFraction();
|
||||
double maxVolume = settings.getMaxAssetVolumeFraction();
|
||||
|
||||
if (req.getAudioDelayMillis() != null && req.getAudioDelayMillis() < 0) throw new ResponseStatusException(
|
||||
BAD_REQUEST,
|
||||
"Audio delay >= 0"
|
||||
@@ -344,7 +447,10 @@ public class ChannelDirectoryService {
|
||||
.findById(assetId)
|
||||
.filter((a) -> normalized.equals(a.getBroadcaster()))
|
||||
.map((asset) -> {
|
||||
AssetView view = AssetView.from(normalized, asset);
|
||||
AssetView view = resolveAssetView(normalized, asset);
|
||||
if (view == null) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "Asset data missing");
|
||||
}
|
||||
boolean play = req == null || req.getPlay();
|
||||
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.play(broadcaster, view, play));
|
||||
return view;
|
||||
@@ -357,16 +463,43 @@ public class ChannelDirectoryService {
|
||||
.findById(assetId)
|
||||
.filter((a) -> normalized.equals(a.getBroadcaster()))
|
||||
.map((asset) -> {
|
||||
boolean wasHidden = asset.isHidden();
|
||||
boolean hidden = request.isHidden();
|
||||
if (wasHidden == hidden) {
|
||||
return AssetView.from(normalized, asset);
|
||||
if (asset.getAssetType() == AssetType.AUDIO) {
|
||||
AudioAsset audio = audioAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not audio"));
|
||||
if (audio.isHidden() == hidden) {
|
||||
return AssetView.fromAudio(normalized, asset, audio);
|
||||
}
|
||||
audio.setHidden(hidden);
|
||||
audioAssetRepository.save(audio);
|
||||
AssetView view = AssetView.fromAudio(normalized, asset, audio);
|
||||
AssetPatch patch = AssetPatch.fromVisibility(asset.getId(), hidden);
|
||||
AssetView payload = hidden ? null : view;
|
||||
messagingTemplate.convertAndSend(
|
||||
topicFor(broadcaster),
|
||||
AssetEvent.visibility(broadcaster, patch, payload)
|
||||
);
|
||||
return view;
|
||||
}
|
||||
|
||||
asset.setHidden(hidden);
|
||||
assetRepository.save(asset);
|
||||
AssetView view = AssetView.from(normalized, asset);
|
||||
AssetPatch patch = AssetPatch.fromVisibility(asset);
|
||||
if (asset.getAssetType() == AssetType.SCRIPT) {
|
||||
ScriptAsset script = scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not a script"));
|
||||
return AssetView.fromScript(normalized, asset, script);
|
||||
}
|
||||
|
||||
VisualAsset visual = visualAssetRepository
|
||||
.findById(asset.getId())
|
||||
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Asset is not visual"));
|
||||
if (visual.isHidden() == hidden) {
|
||||
return AssetView.fromVisual(normalized, asset, visual);
|
||||
}
|
||||
visual.setHidden(hidden);
|
||||
visualAssetRepository.save(visual);
|
||||
AssetView view = AssetView.fromVisual(normalized, asset, visual);
|
||||
AssetPatch patch = AssetPatch.fromVisibility(asset.getId(), hidden);
|
||||
AssetView payload = hidden ? null : view;
|
||||
messagingTemplate.convertAndSend(
|
||||
topicFor(broadcaster),
|
||||
@@ -380,8 +513,13 @@ public class ChannelDirectoryService {
|
||||
return assetRepository
|
||||
.findById(assetId)
|
||||
.map((asset) -> {
|
||||
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);
|
||||
assetStorageService.deleteAsset(asset);
|
||||
messagingTemplate.convertAndSend(
|
||||
topicFor(asset.getBroadcaster()),
|
||||
AssetEvent.deleted(asset.getBroadcaster(), assetId)
|
||||
@@ -392,14 +530,11 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
|
||||
public Optional<AssetContent> getAssetContent(String assetId) {
|
||||
return assetRepository.findById(assetId).flatMap(assetStorageService::loadAssetFileSafely);
|
||||
return assetRepository.findById(assetId).flatMap(this::loadAssetContent);
|
||||
}
|
||||
|
||||
public Optional<AssetContent> getAssetPreview(String assetId, boolean includeHidden) {
|
||||
return assetRepository
|
||||
.findById(assetId)
|
||||
.filter((a) -> includeHidden || !a.isHidden())
|
||||
.flatMap(assetStorageService::loadPreviewSafely);
|
||||
return assetRepository.findById(assetId).flatMap((asset) -> loadAssetPreview(asset, includeHidden));
|
||||
}
|
||||
|
||||
public boolean isAdmin(String broadcaster, String username) {
|
||||
@@ -457,30 +592,187 @@ public class ChannelDirectoryService {
|
||||
}
|
||||
|
||||
private List<AssetView> sortAndMapAssets(String broadcaster, Collection<Asset> assets) {
|
||||
List<String> audioIds = assets
|
||||
.stream()
|
||||
.filter((asset) -> asset.getAssetType() == AssetType.AUDIO)
|
||||
.map(Asset::getId)
|
||||
.toList();
|
||||
List<String> scriptIds = assets
|
||||
.stream()
|
||||
.filter((asset) -> asset.getAssetType() == AssetType.SCRIPT)
|
||||
.map(Asset::getId)
|
||||
.toList();
|
||||
List<String> visualIds = assets
|
||||
.stream()
|
||||
.filter(
|
||||
(asset) ->
|
||||
asset.getAssetType() == AssetType.IMAGE ||
|
||||
asset.getAssetType() == AssetType.VIDEO ||
|
||||
asset.getAssetType() == AssetType.OTHER
|
||||
)
|
||||
.map(Asset::getId)
|
||||
.toList();
|
||||
|
||||
Map<String, VisualAsset> visuals = visualAssetRepository
|
||||
.findByIdIn(visualIds)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(VisualAsset::getId, (asset) -> asset));
|
||||
Map<String, AudioAsset> audios = audioAssetRepository
|
||||
.findByIdIn(audioIds)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(AudioAsset::getId, (asset) -> asset));
|
||||
Map<String, ScriptAsset> scripts = scriptAssetRepository
|
||||
.findByIdIn(scriptIds)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(ScriptAsset::getId, (asset) -> asset));
|
||||
|
||||
return assets
|
||||
.stream()
|
||||
.map((asset) -> resolveAssetView(broadcaster, asset, visuals, audios, scripts))
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(
|
||||
Comparator.comparingInt(Asset::getZIndex).thenComparing(
|
||||
Asset::getCreatedAt,
|
||||
Comparator.nullsFirst(Comparator.naturalOrder())
|
||||
)
|
||||
Comparator.comparing((AssetView view) ->
|
||||
view.zIndex() == null ? Integer.MAX_VALUE : view.zIndex()
|
||||
).thenComparing(AssetView::createdAt, Comparator.nullsFirst(Comparator.naturalOrder()))
|
||||
)
|
||||
.map((a) -> AssetView.from(broadcaster, a))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private int nextZIndex(String broadcaster) {
|
||||
return (
|
||||
assetRepository
|
||||
.findByBroadcaster(normalize(broadcaster))
|
||||
visualAssetRepository
|
||||
.findByIdIn(assetsWithType(normalize(broadcaster), AssetType.IMAGE, AssetType.VIDEO, AssetType.OTHER))
|
||||
.stream()
|
||||
.mapToInt(Asset::getZIndex)
|
||||
.mapToInt(VisualAsset::getZIndex)
|
||||
.max()
|
||||
.orElse(0) +
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
private List<String> assetsWithType(String broadcaster, AssetType... types) {
|
||||
Set<AssetType> typeSet = EnumSet.noneOf(AssetType.class);
|
||||
typeSet.addAll(Arrays.asList(types));
|
||||
return assetRepository
|
||||
.findByBroadcaster(normalize(broadcaster))
|
||||
.stream()
|
||||
.filter((asset) -> typeSet.contains(asset.getAssetType()))
|
||||
.map(Asset::getId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private AssetView resolveAssetView(String broadcaster, Asset asset) {
|
||||
return resolveAssetView(broadcaster, asset, null, null, null);
|
||||
}
|
||||
|
||||
private AssetView resolveAssetView(
|
||||
String broadcaster,
|
||||
Asset asset,
|
||||
Map<String, VisualAsset> visuals,
|
||||
Map<String, AudioAsset> audios,
|
||||
Map<String, ScriptAsset> scripts
|
||||
) {
|
||||
if (asset.getAssetType() == AssetType.AUDIO) {
|
||||
AudioAsset audio = audios != null
|
||||
? audios.get(asset.getId())
|
||||
: audioAssetRepository.findById(asset.getId()).orElse(null);
|
||||
return audio == null ? null : AssetView.fromAudio(broadcaster, asset, audio);
|
||||
}
|
||||
if (asset.getAssetType() == AssetType.SCRIPT) {
|
||||
ScriptAsset script = scripts != null
|
||||
? scripts.get(asset.getId())
|
||||
: scriptAssetRepository.findById(asset.getId()).orElse(null);
|
||||
return script == null ? null : AssetView.fromScript(broadcaster, asset, script);
|
||||
}
|
||||
VisualAsset visual = visuals != null
|
||||
? visuals.get(asset.getId())
|
||||
: visualAssetRepository.findById(asset.getId()).orElse(null);
|
||||
return visual == null ? null : AssetView.fromVisual(broadcaster, asset, visual);
|
||||
}
|
||||
|
||||
private Optional<AssetContent> loadAssetContent(Asset asset) {
|
||||
switch (asset.getAssetType()) {
|
||||
case AUDIO -> {
|
||||
return audioAssetRepository
|
||||
.findById(asset.getId())
|
||||
.flatMap((audio) ->
|
||||
assetStorageService.loadAssetFileSafely(
|
||||
asset.getBroadcaster(),
|
||||
asset.getId(),
|
||||
audio.getMediaType()
|
||||
)
|
||||
);
|
||||
}
|
||||
case SCRIPT -> {
|
||||
return scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.flatMap((script) ->
|
||||
assetStorageService.loadAssetFileSafely(
|
||||
asset.getBroadcaster(),
|
||||
asset.getId(),
|
||||
script.getMediaType()
|
||||
)
|
||||
);
|
||||
}
|
||||
default -> {
|
||||
return visualAssetRepository
|
||||
.findById(asset.getId())
|
||||
.flatMap((visual) ->
|
||||
assetStorageService.loadAssetFileSafely(
|
||||
asset.getBroadcaster(),
|
||||
asset.getId(),
|
||||
visual.getMediaType()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<AssetContent> loadAssetPreview(Asset asset, boolean includeHidden) {
|
||||
if (
|
||||
asset.getAssetType() != AssetType.VIDEO &&
|
||||
asset.getAssetType() != AssetType.IMAGE &&
|
||||
asset.getAssetType() != AssetType.OTHER
|
||||
) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return visualAssetRepository
|
||||
.findById(asset.getId())
|
||||
.filter((visual) -> includeHidden || !visual.isHidden())
|
||||
.flatMap((visual) ->
|
||||
assetStorageService.loadPreviewSafely(
|
||||
asset.getBroadcaster(),
|
||||
asset.getId(),
|
||||
visual.getPreview() != null && !visual.getPreview().isBlank()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void deleteAssetStorage(Asset asset) {
|
||||
switch (asset.getAssetType()) {
|
||||
case AUDIO -> audioAssetRepository
|
||||
.findById(asset.getId())
|
||||
.ifPresent((audio) ->
|
||||
assetStorageService.deleteAsset(asset.getBroadcaster(), asset.getId(), audio.getMediaType(), false)
|
||||
);
|
||||
case SCRIPT -> scriptAssetRepository
|
||||
.findById(asset.getId())
|
||||
.ifPresent((script) ->
|
||||
assetStorageService.deleteAsset(asset.getBroadcaster(), asset.getId(), script.getMediaType(), false)
|
||||
);
|
||||
default -> visualAssetRepository
|
||||
.findById(asset.getId())
|
||||
.ifPresent((visual) ->
|
||||
assetStorageService.deleteAsset(
|
||||
asset.getBroadcaster(),
|
||||
asset.getId(),
|
||||
visual.getMediaType(),
|
||||
visual.getPreview() != null && !visual.getPreview().isBlank()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasPatchChanges(AssetPatch patch) {
|
||||
return (
|
||||
patch.x() != null ||
|
||||
|
||||
@@ -947,11 +947,17 @@ function updateHoverCursor(point) {
|
||||
}
|
||||
|
||||
function isVideoAsset(asset) {
|
||||
if (asset?.assetType) {
|
||||
return asset.assetType === "VIDEO";
|
||||
}
|
||||
const type = asset?.mediaType || asset?.originalMediaType || "";
|
||||
return type.startsWith("video/");
|
||||
}
|
||||
|
||||
function isCodeAsset(asset) {
|
||||
if (asset?.assetType) {
|
||||
return asset.assetType === "SCRIPT";
|
||||
}
|
||||
const type = (asset?.mediaType || asset?.originalMediaType || "").toLowerCase();
|
||||
return type.startsWith("application/javascript") || type.startsWith("text/javascript");
|
||||
}
|
||||
@@ -972,16 +978,31 @@ function isVideoElement(element) {
|
||||
return element && element.tagName === "VIDEO";
|
||||
}
|
||||
|
||||
function getDisplayMediaType(asset) {
|
||||
const raw = asset.originalMediaType || asset.mediaType || "";
|
||||
if (!raw) {
|
||||
return "Unknown";
|
||||
function getAssetTypeLabel(asset) {
|
||||
const type = asset?.assetType;
|
||||
if (type) {
|
||||
const lookup = {
|
||||
IMAGE: "Image",
|
||||
VIDEO: "Video",
|
||||
AUDIO: "Audio",
|
||||
SCRIPT: "Script",
|
||||
OTHER: "Other",
|
||||
};
|
||||
return lookup[type] || "Other";
|
||||
}
|
||||
if (isCodeAsset(asset)) {
|
||||
return "JavaScript";
|
||||
return "Script";
|
||||
}
|
||||
const raw = asset?.originalMediaType || asset?.mediaType || "";
|
||||
if (!raw) {
|
||||
return "Other";
|
||||
}
|
||||
const parts = raw.split("/");
|
||||
return parts.length > 1 ? parts[1].toUpperCase() : raw.toUpperCase();
|
||||
return parts.length > 1 ? parts[0].toUpperCase() : raw.toUpperCase();
|
||||
}
|
||||
|
||||
function getDisplayMediaType(asset) {
|
||||
return getAssetTypeLabel(asset);
|
||||
}
|
||||
|
||||
function isGifAsset(asset) {
|
||||
@@ -1327,9 +1348,7 @@ function renderAssetList() {
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = asset.name || `Asset ${asset.id.slice(0, 6)}`;
|
||||
const details = document.createElement("small");
|
||||
details.textContent = isCodeAsset(asset)
|
||||
? "JavaScript"
|
||||
: `${Math.round(asset.width)}x${Math.round(asset.height)}`;
|
||||
details.textContent = getAssetTypeLabel(asset);
|
||||
meta.appendChild(name);
|
||||
meta.appendChild(details);
|
||||
|
||||
@@ -1767,7 +1786,7 @@ function updateSelectedAssetSummary(asset) {
|
||||
}
|
||||
if (selectedAssetResolution) {
|
||||
if (asset) {
|
||||
if (isCodeAsset(asset)) {
|
||||
if (isCodeAsset(asset) || isAudioAsset(asset)) {
|
||||
selectedAssetResolution.textContent = "";
|
||||
selectedAssetResolution.classList.add("hidden");
|
||||
} else {
|
||||
@@ -2319,25 +2338,31 @@ function findAssetAtPoint(x, y) {
|
||||
|
||||
function persistTransform(asset, silent = false) {
|
||||
cancelPendingTransform(asset.id);
|
||||
const layer = getLayerValue(asset.id);
|
||||
const payload = {
|
||||
audioLoop: asset.audioLoop,
|
||||
audioDelayMillis: asset.audioDelayMillis,
|
||||
audioSpeed: asset.audioSpeed,
|
||||
audioPitch: asset.audioPitch,
|
||||
audioVolume: asset.audioVolume,
|
||||
};
|
||||
if (!isAudioAsset(asset) && !isCodeAsset(asset)) {
|
||||
const layer = getLayerValue(asset.id);
|
||||
payload.x = asset.x;
|
||||
payload.y = asset.y;
|
||||
payload.width = asset.width;
|
||||
payload.height = asset.height;
|
||||
payload.rotation = asset.rotation;
|
||||
payload.speed = asset.speed;
|
||||
payload.layer = layer;
|
||||
payload.zIndex = layer;
|
||||
if (isVideoAsset(asset)) {
|
||||
payload.muted = asset.muted;
|
||||
}
|
||||
}
|
||||
fetch(`/api/channels/${broadcaster}/assets/${asset.id}/transform`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
x: asset.x,
|
||||
y: asset.y,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
rotation: asset.rotation,
|
||||
speed: asset.speed,
|
||||
layer,
|
||||
zIndex: layer,
|
||||
audioLoop: asset.audioLoop,
|
||||
audioDelayMillis: asset.audioDelayMillis,
|
||||
audioSpeed: asset.audioSpeed,
|
||||
audioPitch: asset.audioPitch,
|
||||
audioVolume: asset.audioVolume,
|
||||
}),
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
|
||||
@@ -438,6 +438,9 @@ function recordDuration(assetId, seconds) {
|
||||
}
|
||||
|
||||
function isVideoAsset(asset) {
|
||||
if (asset?.assetType) {
|
||||
return asset.assetType === "VIDEO";
|
||||
}
|
||||
return asset?.mediaType?.startsWith("video/");
|
||||
}
|
||||
|
||||
@@ -454,6 +457,9 @@ function getVideoPlaybackState(element) {
|
||||
}
|
||||
|
||||
function isCodeAsset(asset) {
|
||||
if (asset?.assetType) {
|
||||
return asset.assetType === "SCRIPT";
|
||||
}
|
||||
const type = (asset?.mediaType || asset?.originalMediaType || "").toLowerCase();
|
||||
return type.startsWith("application/javascript") || type.startsWith("text/javascript");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ export function isAudioAsset(asset) {
|
||||
if (!asset) {
|
||||
console.warn("isAudioAsset called with null or undefined asset");
|
||||
}
|
||||
if (asset?.assetType) {
|
||||
return asset.assetType === "AUDIO";
|
||||
}
|
||||
const type = asset?.mediaType || asset?.originalMediaType || "";
|
||||
return type.startsWith("audio/");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user