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