feat: content download
This commit is contained in:
parent
5da02723cb
commit
f3def583ae
@ -3,21 +3,28 @@ package com.magamochi.content.controller;
|
|||||||
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
import com.magamochi.common.model.dto.DefaultResponseDTO;
|
||||||
import com.magamochi.content.model.dto.MangaContentDTO;
|
import com.magamochi.content.model.dto.MangaContentDTO;
|
||||||
import com.magamochi.content.model.dto.MangaContentImagesDTO;
|
import com.magamochi.content.model.dto.MangaContentImagesDTO;
|
||||||
|
import com.magamochi.content.model.enumeration.ContentArchiveFileType;
|
||||||
|
import com.magamochi.content.service.ContentDownloadService;
|
||||||
import com.magamochi.content.service.ContentService;
|
import com.magamochi.content.service.ContentService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/content")
|
@RequestMapping("/content")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ContentController {
|
public class ContentController {
|
||||||
private final ContentService contentService;
|
private final ContentService contentService;
|
||||||
|
private final ContentDownloadService contentDownloadService;
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Get the content for a specific manga/content provider combination",
|
summary = "Get the content for a specific manga/content provider combination",
|
||||||
@ -41,4 +48,32 @@ public class ContentController {
|
|||||||
@PathVariable Long mangaContentId) {
|
@PathVariable Long mangaContentId) {
|
||||||
return DefaultResponseDTO.ok(contentService.getContentImages(mangaContentId));
|
return DefaultResponseDTO.ok(contentService.getContentImages(mangaContentId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Download content archive",
|
||||||
|
description = "Download content as a compressed file by its ID.",
|
||||||
|
tags = {"Content"},
|
||||||
|
operationId = "downloadContentArchive")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "Successful download",
|
||||||
|
content =
|
||||||
|
@Content(
|
||||||
|
mediaType = "application/octet-stream",
|
||||||
|
schema = @Schema(type = "string", format = "binary"))),
|
||||||
|
})
|
||||||
|
@PostMapping(
|
||||||
|
value = "/{mangaContentId}/download",
|
||||||
|
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||||
|
public ResponseEntity<byte[]> downloadContentArchive(
|
||||||
|
@PathVariable Long mangaContentId,
|
||||||
|
@RequestParam ContentArchiveFileType contentArchiveFileType)
|
||||||
|
throws IOException {
|
||||||
|
var response = contentDownloadService.downloadContent(mangaContentId, contentArchiveFileType);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header("Content-Disposition", "attachment; filename=\"" + response.filename() + "\"")
|
||||||
|
.body(response.content());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
package com.magamochi.model.dto;
|
package com.magamochi.content.model.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
public record MangaChapterArchiveDTO(@NotBlank String filename, @NotNull byte[] content) {}
|
public record MangaContentArchiveDTO(@NotBlank String filename, @NotNull byte[] content) {}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.magamochi.content.model.enumeration;
|
||||||
|
|
||||||
|
public enum ContentArchiveFileType {
|
||||||
|
CBZ,
|
||||||
|
CBR
|
||||||
|
}
|
||||||
@ -1,12 +1,10 @@
|
|||||||
package com.magamochi.service;
|
package com.magamochi.content.service;
|
||||||
|
|
||||||
import com.magamochi.common.exception.UnprocessableException;
|
import com.magamochi.common.exception.UnprocessableException;
|
||||||
import com.magamochi.content.model.entity.MangaContent;
|
import com.magamochi.content.model.dto.MangaContentArchiveDTO;
|
||||||
import com.magamochi.content.model.entity.MangaContentImage;
|
import com.magamochi.content.model.entity.MangaContentImage;
|
||||||
import com.magamochi.content.model.repository.MangaContentImageRepository;
|
import com.magamochi.content.model.enumeration.ContentArchiveFileType;
|
||||||
import com.magamochi.content.model.repository.MangaContentRepository;
|
import com.magamochi.image.service.ImageService;
|
||||||
import com.magamochi.model.dto.MangaChapterArchiveDTO;
|
|
||||||
import com.magamochi.model.enumeration.ArchiveFileType;
|
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -21,27 +19,24 @@ import org.springframework.stereotype.Service;
|
|||||||
@Log4j2
|
@Log4j2
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class MangaChapterService {
|
public class ContentDownloadService {
|
||||||
private final MangaContentRepository mangaContentRepository;
|
private final ContentService contentService;
|
||||||
private final MangaContentImageRepository mangaContentImageRepository;
|
private final ImageService imageService;
|
||||||
|
|
||||||
private final OldImageService oldImageService;
|
public MangaContentArchiveDTO downloadContent(
|
||||||
|
Long mangaContentId, ContentArchiveFileType contentArchiveFileType) throws IOException {
|
||||||
public MangaChapterArchiveDTO downloadChapter(Long chapterId, ArchiveFileType archiveFileType)
|
var chapter = contentService.find(mangaContentId);
|
||||||
throws IOException {
|
var chapterImages = chapter.getMangaContentImages();
|
||||||
var chapter = getMangaChapterThrowIfNotFound(chapterId);
|
|
||||||
|
|
||||||
var chapterImages = mangaContentImageRepository.findAllByMangaContent(chapter);
|
|
||||||
|
|
||||||
var byteArrayOutputStream =
|
var byteArrayOutputStream =
|
||||||
switch (archiveFileType) {
|
switch (contentArchiveFileType) {
|
||||||
case CBZ -> getChapterCbzArchive(chapterImages);
|
case CBZ -> getChapterCbzArchive(chapterImages);
|
||||||
default ->
|
default ->
|
||||||
throw new UnprocessableException(
|
throw new UnprocessableException(
|
||||||
"Unsupported archive file type: " + archiveFileType.name());
|
"Unsupported archive file type: " + contentArchiveFileType.name());
|
||||||
};
|
};
|
||||||
|
|
||||||
return new MangaChapterArchiveDTO(
|
return new MangaContentArchiveDTO(
|
||||||
chapter.getTitle() + ".cbz", byteArrayOutputStream.toByteArray());
|
chapter.getTitle() + ".cbz", byteArrayOutputStream.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +55,7 @@ public class MangaChapterService {
|
|||||||
var paddedFileName = String.format("%0" + paddingLength + "d.jpg", imgSrc.getPosition());
|
var paddedFileName = String.format("%0" + paddingLength + "d.jpg", imgSrc.getPosition());
|
||||||
|
|
||||||
zipOutputStream.putNextEntry(new ZipEntry(paddedFileName));
|
zipOutputStream.putNextEntry(new ZipEntry(paddedFileName));
|
||||||
IOUtils.copy(oldImageService.getImageStream(imgSrc.getImage()), zipOutputStream);
|
IOUtils.copy(imageService.getStream(imgSrc.getImage()), zipOutputStream);
|
||||||
zipOutputStream.closeEntry();
|
zipOutputStream.closeEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,10 +64,4 @@ public class MangaChapterService {
|
|||||||
IOUtils.closeQuietly(zipOutputStream);
|
IOUtils.closeQuietly(zipOutputStream);
|
||||||
return byteArrayOutputStream;
|
return byteArrayOutputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MangaContent getMangaChapterThrowIfNotFound(Long chapterId) {
|
|
||||||
return mangaContentRepository
|
|
||||||
.findById(chapterId)
|
|
||||||
.orElseThrow(() -> new RuntimeException("Manga Chapter not found for ID: " + chapterId));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,46 +0,0 @@
|
|||||||
package com.magamochi.controller;
|
|
||||||
|
|
||||||
import com.magamochi.model.enumeration.ArchiveFileType;
|
|
||||||
import com.magamochi.service.MangaChapterService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
|
||||||
import java.io.IOException;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/mangas/chapters")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class MangaChapterController {
|
|
||||||
private final MangaChapterService mangaChapterService;
|
|
||||||
|
|
||||||
@Operation(
|
|
||||||
summary = "Download chapter archive",
|
|
||||||
description = "Download a chapter as a compressed file by its ID.",
|
|
||||||
tags = {"Manga Chapter"},
|
|
||||||
operationId = "downloadChapterArchive")
|
|
||||||
@ApiResponses({
|
|
||||||
@ApiResponse(
|
|
||||||
responseCode = "200",
|
|
||||||
description = "Successful download",
|
|
||||||
content =
|
|
||||||
@Content(
|
|
||||||
mediaType = "application/octet-stream",
|
|
||||||
schema = @Schema(type = "string", format = "binary"))),
|
|
||||||
})
|
|
||||||
@PostMapping(value = "/{chapterId}/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
|
||||||
public ResponseEntity<byte[]> downloadChapterArchive(
|
|
||||||
@PathVariable Long chapterId, @RequestParam ArchiveFileType archiveFileType)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
var response = mangaChapterService.downloadChapter(chapterId, archiveFileType);
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.header("Content-Disposition", "attachment; filename=\"" + response.filename() + "\"")
|
|
||||||
.body(response.content());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,6 +3,7 @@ package com.magamochi.image.service;
|
|||||||
import com.magamochi.common.exception.NotFoundException;
|
import com.magamochi.common.exception.NotFoundException;
|
||||||
import com.magamochi.image.model.entity.Image;
|
import com.magamochi.image.model.entity.Image;
|
||||||
import com.magamochi.image.model.repository.ImageRepository;
|
import com.magamochi.image.model.repository.ImageRepository;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -51,4 +52,8 @@ public class ImageService {
|
|||||||
public List<Image> findAll() {
|
public List<Image> findAll() {
|
||||||
return imageRepository.findAll();
|
return imageRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InputStream getStream(Image image) {
|
||||||
|
return s3Service.getFileStream(image.getObjectKey());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.magamochi.image.service;
|
|||||||
|
|
||||||
import static java.util.Objects.nonNull;
|
import static java.util.Objects.nonNull;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@ -103,4 +104,10 @@ public class S3Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public InputStream getFileStream(String key) {
|
||||||
|
var request = GetObjectRequest.builder().bucket(bucket).key(key).build();
|
||||||
|
|
||||||
|
return s3Client.getObject(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
package com.magamochi.model.enumeration;
|
|
||||||
|
|
||||||
public enum ArchiveFileType {
|
|
||||||
CBZ,
|
|
||||||
CBR
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package com.magamochi.service;
|
|
||||||
|
|
||||||
import com.magamochi.image.model.entity.Image;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Log4j2
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class OldImageService {
|
|
||||||
private final OldS3Service oldS3Service;
|
|
||||||
|
|
||||||
public InputStream getImageStream(Image image) {
|
|
||||||
return oldS3Service.getFile(image.getObjectKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
package com.magamochi.service;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import software.amazon.awssdk.services.s3.S3Client;
|
|
||||||
import software.amazon.awssdk.services.s3.model.*;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class OldS3Service {
|
|
||||||
@Value("${minio.bucket}")
|
|
||||||
private String bucket;
|
|
||||||
|
|
||||||
private final S3Client s3Client;
|
|
||||||
|
|
||||||
public InputStream getFile(String key) {
|
|
||||||
var request = GetObjectRequest.builder().bucket(bucket).key(key).build();
|
|
||||||
|
|
||||||
return s3Client.getObject(request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user