mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-02-18 03:07:40 +01:00
Enhance Comicvine metadata parsing and error handling (#835)
This commit is contained in:
@@ -6,7 +6,10 @@ import com.adityachandel.booklore.model.entity.AuthorEntity;
|
||||
import com.adityachandel.booklore.model.entity.BookEntity;
|
||||
import com.adityachandel.booklore.model.entity.CategoryEntity;
|
||||
import com.adityachandel.booklore.model.entity.LibraryPathEntity;
|
||||
import org.mapstruct.*;
|
||||
import org.mapstruct.Context;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Named;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -5,7 +5,10 @@ import com.adityachandel.booklore.model.dto.FetchedProposal;
|
||||
import com.adityachandel.booklore.model.entity.MetadataFetchProposalEntity;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.mapstruct.*;
|
||||
import org.mapstruct.AfterMapping;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.MappingTarget;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
|
||||
@@ -5,8 +5,6 @@ import com.adityachandel.booklore.model.entity.LibraryEntity;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface LibraryMapper {
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package com.adityachandel.booklore.mapper;
|
||||
|
||||
import com.adityachandel.booklore.model.dto.LibraryPath;
|
||||
import com.adityachandel.booklore.model.dto.Shelf;
|
||||
import com.adityachandel.booklore.model.entity.LibraryPathEntity;
|
||||
import com.adityachandel.booklore.model.entity.ShelfEntity;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface LibraryPathMapper {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package com.adityachandel.booklore.mapper;
|
||||
|
||||
import com.adityachandel.booklore.model.dto.OpdsUser;
|
||||
import com.adityachandel.booklore.model.dto.Shelf;
|
||||
import com.adityachandel.booklore.model.entity.OpdsUserEntity;
|
||||
import com.adityachandel.booklore.model.entity.ShelfEntity;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
|
||||
@@ -13,7 +13,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
|
||||
@@ -3,12 +3,10 @@ package com.adityachandel.booklore.mapper.v2;
|
||||
import com.adityachandel.booklore.model.dto.Book;
|
||||
import com.adityachandel.booklore.model.dto.BookMetadata;
|
||||
import com.adityachandel.booklore.model.dto.LibraryPath;
|
||||
import com.adityachandel.booklore.model.entity.AuthorEntity;
|
||||
import com.adityachandel.booklore.model.entity.BookEntity;
|
||||
import com.adityachandel.booklore.model.entity.BookMetadataEntity;
|
||||
import com.adityachandel.booklore.model.entity.CategoryEntity;
|
||||
import com.adityachandel.booklore.model.entity.LibraryPathEntity;
|
||||
import org.mapstruct.*;
|
||||
import com.adityachandel.booklore.model.entity.*;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Named;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -1,127 +1,83 @@
|
||||
package com.adityachandel.booklore.model.dto.response.comicvineapi;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonTypeInfo(
|
||||
use = JsonTypeInfo.Id.NAME,
|
||||
include = JsonTypeInfo.As.EXISTING_PROPERTY,
|
||||
|
||||
property = "resource_type",
|
||||
defaultImpl = Volume.class
|
||||
)
|
||||
public class Comic {
|
||||
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = Volume.class, name = "volume"),
|
||||
@JsonSubTypes.Type(value = Issue.class, name = "issue")
|
||||
})
|
||||
private int id;
|
||||
|
||||
public abstract class Comic {
|
||||
private int id;
|
||||
protected String name;
|
||||
protected ComicVineImage image;
|
||||
private String description;
|
||||
@JsonProperty("api_detail_url")
|
||||
private String apiDetailUrl;
|
||||
|
||||
@JsonProperty("concept_credits")
|
||||
private List<ComicvineItem> conceptCredits;
|
||||
@JsonProperty("cover_date")
|
||||
private String coverDate;
|
||||
|
||||
private String description;
|
||||
|
||||
@JsonProperty("person_credits")
|
||||
private List<ComicvineItem> personCredits;
|
||||
private String name;
|
||||
|
||||
@JsonProperty("issue_number")
|
||||
private String issueNumber;
|
||||
|
||||
private Image image;
|
||||
|
||||
private Volume volume;
|
||||
|
||||
|
||||
@JsonProperty("resource_type")
|
||||
private String resourceType;
|
||||
|
||||
|
||||
public abstract String getDisplayName();
|
||||
|
||||
public abstract String getComicId();
|
||||
|
||||
public abstract LocalDate getDate();
|
||||
|
||||
public Set<String> getAuthors() {
|
||||
Set<String> authors = new HashSet<>();
|
||||
if (personCredits != null) {
|
||||
for (ComicvineItem person : personCredits) {
|
||||
authors.add(person.name);
|
||||
}
|
||||
}
|
||||
return authors;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
if (image == null) {
|
||||
return null;
|
||||
}
|
||||
return image.getOriginalUrl() != null ? image.getOriginalUrl() : image.getThumbUrl();
|
||||
}
|
||||
|
||||
@JsonProperty("resource_type")
|
||||
private String resourceType;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ComicVineImage {
|
||||
public static class Image {
|
||||
@JsonProperty("icon_url")
|
||||
private String iconUrl;
|
||||
|
||||
|
||||
@JsonProperty("medium_url")
|
||||
private String mediumUrl;
|
||||
|
||||
|
||||
@JsonProperty("screen_url")
|
||||
private String screenUrl;
|
||||
|
||||
|
||||
@JsonProperty("screen_large_url")
|
||||
private String screenLargeUrl;
|
||||
|
||||
|
||||
@JsonProperty("small_url")
|
||||
private String smallUrl;
|
||||
|
||||
|
||||
@JsonProperty("super_url")
|
||||
private String superUrl;
|
||||
|
||||
|
||||
@JsonProperty("thumb_url")
|
||||
private String thumbUrl;
|
||||
|
||||
|
||||
@JsonProperty("tiny_url")
|
||||
private String tinyUrl;
|
||||
|
||||
|
||||
@JsonProperty("original_url")
|
||||
private String originalUrl;
|
||||
|
||||
|
||||
@JsonProperty("image_tags")
|
||||
private String imageTags;
|
||||
}
|
||||
|
||||
|
||||
@Data
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ComicvineItem {
|
||||
@JsonProperty("api_detail_url")
|
||||
private String apiDetailUrl;
|
||||
|
||||
public static class Volume {
|
||||
private int id;
|
||||
private String name;
|
||||
|
||||
@JsonProperty("api_detail_url")
|
||||
private String apiDetailUrl;
|
||||
|
||||
@JsonProperty("site_detail_url")
|
||||
private String siteDetailUrl;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -2,16 +2,11 @@ package com.adityachandel.booklore.model.dto.response.comicvineapi;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -30,5 +25,4 @@ public class ComicvineApiResponse {
|
||||
private int statusCode;
|
||||
private List<Comic> results;
|
||||
private String version;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.adityachandel.booklore.model.dto.response.comicvineapi;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class ComicvineIssueResponse {
|
||||
private String error;
|
||||
private int limit;
|
||||
private int offset;
|
||||
private int number_of_page_results;
|
||||
private int number_of_total_results;
|
||||
private int status_code;
|
||||
private IssueResults results;
|
||||
private String version;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class IssueResults {
|
||||
@JsonProperty("person_credits")
|
||||
private List<PersonCredit> personCredits;
|
||||
}
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class PersonCredit {
|
||||
private long id;
|
||||
private String name;
|
||||
private String role;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.adityachandel.booklore.model.dto.response.comicvineapi;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class Issue extends Comic {
|
||||
@JsonProperty("cover_date")
|
||||
private String coverDate;
|
||||
|
||||
@JsonProperty("volume")
|
||||
private ComicvineItem volume;
|
||||
|
||||
@JsonProperty("issue_number")
|
||||
private int issueNumber;
|
||||
|
||||
|
||||
@Override
|
||||
public String getComicId() {
|
||||
return "4000-" + String.valueOf(getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
if(name ==null){
|
||||
if(volume != null){
|
||||
return volume.getName() + " " + "Issue #" + String.valueOf(issueNumber);
|
||||
|
||||
}
|
||||
else{
|
||||
return "Unknown Comic";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate getDate() {
|
||||
if (coverDate == null || coverDate.isEmpty()) return null;
|
||||
try {
|
||||
return LocalDate.parse(coverDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.adityachandel.booklore.model.dto.response.comicvineapi;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class Volume extends Comic{
|
||||
@JsonProperty("api_detail_url")
|
||||
private String apiDetailUrl;
|
||||
|
||||
private String description;
|
||||
|
||||
@JsonProperty("publisher")
|
||||
private ComicvineItem publisher;
|
||||
|
||||
@JsonProperty("site_detail_url")
|
||||
private String siteDetailUrl;
|
||||
|
||||
@JsonProperty("start_year")
|
||||
private String startYear;
|
||||
|
||||
public String getPublisherName() {
|
||||
return publisher != null ? publisher.getName() : null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getDisplayName(){
|
||||
if(name==null){
|
||||
return "Unknown Comic";
|
||||
}
|
||||
else{
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getComicId() {
|
||||
return "4500-" + String.valueOf(getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate getDate() {
|
||||
if (startYear == null || startYear.isEmpty()) return null;
|
||||
try {
|
||||
return LocalDate.parse(startYear, DateTimeFormatter.ofPattern("yyyy"));
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@ package com.adityachandel.booklore.service.metadata.parser;
|
||||
import com.adityachandel.booklore.model.dto.Book;
|
||||
import com.adityachandel.booklore.model.dto.BookMetadata;
|
||||
import com.adityachandel.booklore.model.dto.request.FetchMetadataRequest;
|
||||
import com.adityachandel.booklore.model.dto.response.comicvineapi.ComicvineApiResponse;
|
||||
import com.adityachandel.booklore.model.dto.response.comicvineapi.Issue;
|
||||
import com.adityachandel.booklore.model.dto.response.comicvineapi.Volume;
|
||||
import com.adityachandel.booklore.model.dto.response.comicvineapi.Comic;
|
||||
import com.adityachandel.booklore.model.dto.response.comicvineapi.ComicvineApiResponse;
|
||||
import com.adityachandel.booklore.model.enums.MetadataProvider;
|
||||
import com.adityachandel.booklore.service.appsettings.AppSettingService;
|
||||
import com.adityachandel.booklore.util.BookUtils;
|
||||
@@ -23,134 +21,181 @@ import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ComicvineBookParser implements BookParser {
|
||||
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private static final String COMICVINE_URL = "https://comicvine.gamespot.com/api/";
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final AppSettingService appSettingService;
|
||||
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||
|
||||
@Override
|
||||
public List<BookMetadata> fetchMetadata(Book book, FetchMetadataRequest fetchMetadataRequest) {
|
||||
String searchTerm = getSearchTerm(book, fetchMetadataRequest);
|
||||
return searchTerm != null ? getMetadataListByTerm(searchTerm) : List.of();
|
||||
if (searchTerm == null) {
|
||||
log.warn("No valid search term provided for metadata fetch.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return getMetadataListByTerm(searchTerm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BookMetadata fetchTopMetadata(Book book, FetchMetadataRequest fetchMetadataRequest) {
|
||||
List<BookMetadata> metadataList = fetchMetadata(book, fetchMetadataRequest);
|
||||
return metadataList.isEmpty() ? null : metadataList.getFirst();
|
||||
}
|
||||
|
||||
public List<BookMetadata> getMetadataListByTerm(String term) {
|
||||
String apiToken = appSettingService.getAppSettings().getMetadataProviderSettings().getComicvine().getApiKey();
|
||||
if (apiToken == null || apiToken.isEmpty()) {
|
||||
log.warn("Comicvine API token not set");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
log.info("Comicvine: Fetching metadata for: {}", term);
|
||||
String apiToken = getApiToken();
|
||||
if (apiToken == null) return Collections.emptyList();
|
||||
|
||||
log.info("Comicvine: Fetching metadata for term: '{}'", term);
|
||||
try {
|
||||
|
||||
String fieldsList = "cover_date,id,issue_number,name,person_credits,volume,api_detail_url,concept_credits,start_year,publisher,description,image";
|
||||
String fieldsList = String.join(",", "api_detail_url", "cover_date", "description", "id", "image", "issue_number", "name", "publisher", "volume");
|
||||
String resources = "volume,issue";
|
||||
|
||||
URI uri = UriComponentsBuilder.fromUriString(COMICVINE_URL) // Base URL
|
||||
|
||||
URI uri = UriComponentsBuilder.fromUriString(COMICVINE_URL)
|
||||
.path("/search/")
|
||||
.queryParam("api_key", apiToken)
|
||||
.queryParam("api_key", apiToken)
|
||||
.queryParam("format", "json")
|
||||
.queryParam("resources", resources)
|
||||
.queryParam("resource_type", resources)
|
||||
.queryParam("query", term)
|
||||
.queryParam("filter", "name:" + term)
|
||||
.queryParam("limit", "10") // Limit results to reduce response size
|
||||
.queryParam("limit", 10)
|
||||
.queryParam("field_list", fieldsList)
|
||||
.build()
|
||||
.toUri();
|
||||
|
||||
log.debug("Comicvine API request URI: {}", uri);
|
||||
HttpClient httpClient = HttpClient.newHttpClient();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(uri)
|
||||
.header("User-Agent", "MyComicApp/1.0")
|
||||
.header("User-Agent", "Booklore/1.0")
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
return parseComicvineApiResponse(response.body());
|
||||
} else {
|
||||
log.error("Failed to fetch data from Comicvine API. Status code: {}", response.statusCode());
|
||||
return List.of();
|
||||
log.error("Comicvine Search API returned status code {}", response.statusCode());
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
log.error("Error fetching metadata from Comicvine API", e);
|
||||
return List.of();
|
||||
log.error("Error fetching metadata from Comicvine Search API", e);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public BookMetadata fetchTopMetadata(Book book, FetchMetadataRequest fetchMetadataRequest) {
|
||||
List<BookMetadata> fetchedBookMetadata = fetchMetadata(book, fetchMetadataRequest);
|
||||
return fetchedBookMetadata.isEmpty() ? null : fetchedBookMetadata.get(0);
|
||||
}
|
||||
|
||||
private String getSearchTerm(Book book, FetchMetadataRequest request) {
|
||||
return (request.getTitle() != null && !request.getTitle().isEmpty())
|
||||
? request.getTitle()
|
||||
: (book.getFileName() != null && !book.getFileName().isEmpty()
|
||||
? BookUtils.cleanFileName(book.getFileName())
|
||||
: null);
|
||||
private String getSearchTerm(Book book, FetchMetadataRequest request) {
|
||||
if (request.getTitle() != null && !request.getTitle().isEmpty()) {
|
||||
return request.getTitle();
|
||||
} else if (book.getFileName() != null && !book.getFileName().isEmpty()) {
|
||||
return BookUtils.cleanFileName(book.getFileName());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<BookMetadata> parseComicvineApiResponse(String responseBody) throws IOException {
|
||||
ComicvineApiResponse apiResponse = objectMapper.readValue(responseBody, ComicvineApiResponse.class);
|
||||
if (apiResponse.getResults() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return apiResponse.getResults().stream()
|
||||
.map(this::convertToBookMetadata)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private BookMetadata convertToBookMetadata(Comic comic) {
|
||||
BookMetadata.BookMetadataBuilder builder = BookMetadata.builder()
|
||||
.title(comic.getDisplayName())
|
||||
.comicvineId(String.valueOf(comic.getId()))
|
||||
.authors(comic.getAuthors())
|
||||
.thumbnailUrl(comic.getImageUrl())
|
||||
.description(comic.getDescription())
|
||||
.provider(MetadataProvider.Comicvine);
|
||||
return BookMetadata.builder()
|
||||
.provider(MetadataProvider.Comicvine)
|
||||
.comicvineId(String.valueOf(comic.getId()))
|
||||
.title(comic.getName())
|
||||
.authors(new HashSet<>())
|
||||
.thumbnailUrl(comic.getImage() != null ? comic.getImage().getMediumUrl() : null)
|
||||
.description(comic.getDescription())
|
||||
.seriesName(comic.getVolume() != null ? comic.getVolume().getName() : null)
|
||||
.seriesNumber(safeParseFloat(comic.getIssueNumber()))
|
||||
.publishedDate(safeParseDate(comic.getCoverDate()))
|
||||
.build();
|
||||
}
|
||||
|
||||
// Handle publishedDate based on the comic type
|
||||
if (comic instanceof Volume) {
|
||||
Volume volume = (Volume) comic;
|
||||
builder.publisher(volume.getPublisherName());
|
||||
builder.publishedDate(volume.getDate());
|
||||
} else if (comic instanceof Issue) {
|
||||
Issue issue = (Issue) comic;
|
||||
builder.seriesName(issue.getVolume().getName());
|
||||
builder.seriesNumber((float) issue.getIssueNumber());
|
||||
builder.publishedDate(issue.getDate()); // Already parsed
|
||||
private static LocalDate safeParseDate(String dateStr) {
|
||||
if (dateStr == null || dateStr.isEmpty()) return null;
|
||||
try {
|
||||
return LocalDate.parse(dateStr, DATE_FORMATTER);
|
||||
} catch (DateTimeParseException e) {
|
||||
log.warn("Invalid date '{}'", dateStr);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the final built object
|
||||
return builder.build();
|
||||
private static Float safeParseFloat(String numStr) {
|
||||
if (numStr == null || numStr.isEmpty()) return null;
|
||||
try {
|
||||
return Float.valueOf(numStr);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String getApiToken() {
|
||||
String apiToken = appSettingService.getAppSettings().getMetadataProviderSettings().getComicvine().getApiKey();
|
||||
if (apiToken == null || apiToken.isEmpty()) {
|
||||
log.warn("Comicvine API token not set");
|
||||
return null;
|
||||
}
|
||||
return apiToken;
|
||||
}
|
||||
|
||||
/*public Set<String> fetchAuthors(int issueId) {
|
||||
String apiToken = getApiToken();
|
||||
if (apiToken == null) return Collections.emptySet();
|
||||
|
||||
try {
|
||||
String fieldsList = String.join(",", "person_credits");
|
||||
|
||||
}
|
||||
}
|
||||
URI uri = UriComponentsBuilder.fromUriString(COMICVINE_URL)
|
||||
.path("/issue/4000-" + issueId + "/")
|
||||
.queryParam("api_key", apiToken)
|
||||
.queryParam("format", "json")
|
||||
.queryParam("field_list", fieldsList)
|
||||
.build()
|
||||
.toUri();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(uri)
|
||||
.header("User-Agent", "Booklore/1.0")
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
ComicvineIssueResponse issueResponse = objectMapper.readValue(response.body(), ComicvineIssueResponse.class);
|
||||
if (issueResponse.getResults() == null || issueResponse.getResults().getPersonCredits() == null) {
|
||||
log.warn("No person credits found for issue ID {}", issueId);
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
return issueResponse.getResults().getPersonCredits().stream()
|
||||
.filter(pc -> pc.getRole() != null && pc.getRole().toLowerCase().contains("writer"))
|
||||
.map(ComicvineIssueResponse.PersonCredit::getName)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
} else {
|
||||
log.error("Comicvine Issue API returned status code {}", response.statusCode());
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
log.error("Error fetching issue metadata from Comicvine Issue API", e);
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}*/
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export class BookdropFileMetadataPickerComponent {
|
||||
{label: 'HC Reviews', controlName: 'hardcoverReviewCount', lockedKey: 'hardcoverReviewCountLocked', fetchedKey: 'hardcoverReviewCount'},
|
||||
{label: 'HC Rating', controlName: 'hardcoverRating', lockedKey: 'hardcoverRatingLocked', fetchedKey: 'hardcoverRating'},
|
||||
{label: 'Google ID', controlName: 'googleId', lockedKey: 'googleIdLocked', fetchedKey: 'googleIdRating'},
|
||||
{label: 'CV ID', controlName: 'comicvineId', lockedKey: 'comicvineIdLocked', fetchedKey: 'comicvineId'},
|
||||
{label: 'Comicvine ID', controlName: 'comicvineId', lockedKey: 'comicvineIdLocked', fetchedKey: 'comicvineId'},
|
||||
{label: 'Pages', controlName: 'pageCount', lockedKey: 'pageCountLocked', fetchedKey: 'pageCount'}
|
||||
];
|
||||
|
||||
|
||||
@@ -61,11 +61,11 @@ export class MetadataPickerComponent implements OnInit {
|
||||
{label: 'ASIN', controlName: 'asin', lockedKey: 'asinLocked', fetchedKey: 'asin'},
|
||||
{label: 'Amz Reviews', controlName: 'amazonReviewCount', lockedKey: 'amazonReviewCountLocked', fetchedKey: 'amazonReviewCount'},
|
||||
{label: 'Amz Rating', controlName: 'amazonRating', lockedKey: 'amazonRatingLocked', fetchedKey: 'amazonRating'},
|
||||
{label: 'GR ID', controlName: 'goodreadsId', lockedKey: 'goodreadsIdLocked', fetchedKey: 'goodreadsId'},
|
||||
{label: 'CV ID', controlName: 'comicvineId', lockedKey: 'comicvineIdLocked', fetchedKey: 'comicvineId'},
|
||||
{label: 'Comicvine ID', controlName: 'comicvineId', lockedKey: 'comicvineIdLocked', fetchedKey: 'comicvineId'},
|
||||
{label: 'Goodreads ID', controlName: 'goodreadsId', lockedKey: 'goodreadsIdLocked', fetchedKey: 'goodreadsId'},
|
||||
{label: 'GR Reviews', controlName: 'goodreadsReviewCount', lockedKey: 'goodreadsReviewCountLocked', fetchedKey: 'goodreadsReviewCount'},
|
||||
{label: 'GR Rating', controlName: 'goodreadsRating', lockedKey: 'goodreadsRatingLocked', fetchedKey: 'goodreadsRating'},
|
||||
{label: 'HC ID', controlName: 'hardcoverId', lockedKey: 'hardcoverIdLocked', fetchedKey: 'hardcoverId'},
|
||||
{label: 'Hardcover ID', controlName: 'hardcoverId', lockedKey: 'hardcoverIdLocked', fetchedKey: 'hardcoverId'},
|
||||
{label: 'HC Reviews', controlName: 'hardcoverReviewCount', lockedKey: 'hardcoverReviewCountLocked', fetchedKey: 'hardcoverReviewCount'},
|
||||
{label: 'HC Rating', controlName: 'hardcoverRating', lockedKey: 'hardcoverRatingLocked', fetchedKey: 'hardcoverRating'},
|
||||
{label: 'Google ID', controlName: 'googleId', lockedKey: 'googleIdLocked', fetchedKey: 'googleIdRating'},
|
||||
|
||||
@@ -80,14 +80,18 @@
|
||||
<div class="flex flex-col md:flex-row gap-2 md:gap-4 justify-between">
|
||||
<p class="font-bold">{{ truncateText(metadata['title']!, 90) }}</p>
|
||||
<div class="flex flex-wrap gap-2 text-sm">
|
||||
<p>ISBN: {{ metadata['isbn10'] || metadata['asin'] }}</p>
|
||||
<span class="hidden md:inline">|</span>
|
||||
@if (metadata['isbn10'] || metadata['asin']) {
|
||||
<p>ISBN: {{ metadata['isbn10'] || metadata['asin'] }}</p>
|
||||
<span class="hidden md:inline">|</span>
|
||||
}
|
||||
<p>Published: {{ metadata['publishedDate'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 text-sm">
|
||||
<p>by {{ truncateText(metadata['authors']!.join(', '), 70) }}</p>
|
||||
<span class="hidden md:inline">|</span>
|
||||
@if (metadata['authors'] && metadata['authors'].length > 0) {
|
||||
<p>by {{ truncateText(metadata['authors'].join(', '), 70) }}</p>
|
||||
<span class="hidden md:inline">|</span>
|
||||
}
|
||||
<p>Source: <span [innerHTML]="buildProviderLink(metadata)"></span></p>
|
||||
</div>
|
||||
<p class="mt-4 truncate-text p-secondary">
|
||||
|
||||
Reference in New Issue
Block a user