Add listing

This commit is contained in:
2025-12-04 16:36:02 +01:00
parent 9f9c14c1bb
commit cc5fa07054
10 changed files with 121 additions and 79 deletions

View File

@@ -2,12 +2,12 @@ package com.imgfloat.app.controller;
import com.imgfloat.app.model.AdminRequest;
import com.imgfloat.app.model.Asset;
import com.imgfloat.app.model.AssetRequest;
import com.imgfloat.app.model.TransformRequest;
import com.imgfloat.app.model.VisibilityRequest;
import com.imgfloat.app.service.ChannelDirectoryService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
@@ -18,11 +18,14 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collection;
import java.io.IOException;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
@RestController
@RequestMapping("/api/channels/{broadcaster}")
@@ -82,15 +85,22 @@ public class ChannelApiController {
return channelDirectoryService.getVisibleAssets(broadcaster);
}
@PostMapping("/assets")
@PostMapping(value = "/assets", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<Asset> createAsset(@PathVariable("broadcaster") String broadcaster,
@Valid @RequestBody AssetRequest request,
@org.springframework.web.bind.annotation.RequestPart("file") MultipartFile file,
OAuth2AuthenticationToken authentication) {
String login = TwitchUser.from(authentication).login();
ensureAuthorized(broadcaster, login);
return channelDirectoryService.createAsset(broadcaster, request)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Channel not found"));
if (file == null || file.isEmpty()) {
throw new ResponseStatusException(BAD_REQUEST, "Asset file is required");
}
try {
return channelDirectoryService.createAsset(broadcaster, file)
.map(ResponseEntity::ok)
.orElseThrow(() -> new ResponseStatusException(BAD_REQUEST, "Unable to read image"));
} catch (IOException e) {
throw new ResponseStatusException(BAD_REQUEST, "Failed to process image", e);
}
}
@PutMapping("/assets/{assetId}/transform")

View File

@@ -22,6 +22,7 @@ public class ViewController {
String login = TwitchUser.from(authentication).login();
model.addAttribute("username", login);
model.addAttribute("channel", login);
model.addAttribute("adminChannels", channelDirectoryService.adminChannelsFor(login));
return "dashboard";
}
return "index";

View File

@@ -1,39 +0,0 @@
package com.imgfloat.app.model;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public class AssetRequest {
@NotBlank
private String url;
@Min(1)
private double width;
@Min(1)
private double height;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
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;
}
}

View File

@@ -2,17 +2,23 @@ package com.imgfloat.app.service;
import com.imgfloat.app.model.Asset;
import com.imgfloat.app.model.AssetEvent;
import com.imgfloat.app.model.AssetRequest;
import com.imgfloat.app.model.Channel;
import com.imgfloat.app.model.TransformRequest;
import com.imgfloat.app.model.VisibilityRequest;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import javax.imageio.ImageIO;
@Service
public class ChannelDirectoryService {
@@ -55,9 +61,16 @@ public class ChannelDirectoryService {
.toList();
}
public Optional<Asset> createAsset(String broadcaster, AssetRequest request) {
public Optional<Asset> createAsset(String broadcaster, MultipartFile file) throws IOException {
Channel channel = getOrCreateChannel(broadcaster);
Asset asset = new Asset(request.getUrl(), request.getWidth(), request.getHeight());
byte[] bytes = file.getBytes();
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
if (image == null) {
return Optional.empty();
}
String contentType = Optional.ofNullable(file.getContentType()).orElse("application/octet-stream");
String dataUrl = "data:" + contentType + ";base64," + Base64.getEncoder().encodeToString(bytes);
Asset asset = new Asset(dataUrl, image.getWidth(), image.getHeight());
channel.getAssets().put(asset.getId(), asset);
messagingTemplate.convertAndSend(topicFor(broadcaster), AssetEvent.created(broadcaster, asset));
return Optional.of(asset);
@@ -108,6 +121,17 @@ public class ChannelDirectoryService {
return channel != null && channel.getAdmins().contains(username.toLowerCase());
}
public Collection<String> adminChannelsFor(String username) {
if (username == null) {
return List.of();
}
String login = username.toLowerCase();
return channels.values().stream()
.filter(channel -> channel.getAdmins().contains(login))
.map(Channel::getBroadcaster)
.toList();
}
private String topicFor(String broadcaster) {
return "/topic/channel/" + broadcaster.toLowerCase();
}