Compare commits

..

18 Commits

Author SHA1 Message Date
1e42f24e3a Use logstash as logging destination 2025-09-14 01:58:32 +05:00
740e1a0279 Use buildx caching in docker mvn package 2025-09-07 01:06:08 +05:00
13be17fa8f Use docker shared network instead of localhost 2025-09-01 19:21:31 +05:00
21199307c7 Use external docker network 2025-09-01 19:19:47 +05:00
e07f48f39d Change compose service name 2025-08-28 17:23:56 +05:00
53dcde6bd9 Exclude error message and change application name to anyame-kodik-search-backend 2025-08-28 17:22:27 +05:00
eca6f50730 Bump spring-boot 3.5.0 -> 3.5.5 2025-08-28 12:36:59 +05:00
4d99a59947 Add .env.example and add eureka client 2025-08-28 11:47:39 +05:00
f2eee1752d Include "season" in anime identifier 2025-08-10 16:15:23 +05:00
9cc7cede69 [Fix] Allow only specific types only in search results (#2)
Reviewed-on: #2
Co-authored-by: bivashy <botyrbojey@gmail.com>
Co-committed-by: bivashy <botyrbojey@gmail.com>
2025-07-19 18:50:29 +00:00
9540a71307 [Fix] Return unique search results (#1)
Reviewed-on: #1
Co-authored-by: bivashy <botyrbojey@gmail.com>
Co-committed-by: bivashy <botyrbojey@gmail.com>
2025-07-19 18:41:34 +00:00
55bbb79510 Fix compose image name 2025-07-14 00:45:08 +05:00
2b0f816633 Optimize Dockerfile and add docker compose 2025-07-12 18:15:55 +05:00
3fc06eabff Add kodik token to application.properties 2025-07-12 18:14:32 +05:00
95113ce2fb Change logger style in controller 2025-07-12 18:14:19 +05:00
99c0f723aa Move package to com.backend.search.kodik.service... and add Dockerfile 2025-07-07 21:36:31 +05:00
c97c1a9c5a Configure CORS, ignore unkown properties of MaterialData 2025-06-20 00:40:56 +05:00
f145b17527 Generate OpenAPI v3 2025-06-20 00:39:11 +05:00
15 changed files with 287 additions and 103 deletions

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
KODIK_TOKEN=YOUR_KODIK_TOKEN
EUREKA_SCHEMA=http
EUREKA_HOST=anyame-vue-bff:8080

49
Dockerfile.prod Normal file
View File

@ -0,0 +1,49 @@
# Build
FROM maven:3.9.6-eclipse-temurin-21 AS builder
WORKDIR /workspace
COPY pom.xml .
RUN --mount=type=cache,target=/root/.m2 mvn dependency:go-offline -B
COPY src ./src
RUN --mount=type=cache,target=/root/.m2 mvn clean package -DskipTests
# Create optimized runtime
FROM eclipse-temurin:21 AS app-build
ENV RELEASE=21
WORKDIR /opt/build
COPY --from=builder /workspace/target/*.jar ./application.jar
RUN java -Djarmode=layertools -jar application.jar extract
RUN $JAVA_HOME/bin/jlink \
--add-modules $(jdeps --ignore-missing-deps -q -recursive --multi-release ${RELEASE} --print-module-deps -cp 'dependencies/BOOT-INF/lib/*' application.jar),jdk.crypto.ec,jdk.security.auth,jdk.crypto.cryptoki \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output jdk
# Run
FROM debian:bookworm-slim
ARG BUILD_PATH=/opt/build
ENV JAVA_HOME=/opt/jdk
ENV PATH="${JAVA_HOME}/bin:${PATH}"
RUN groupadd --gid 1000 spring-app \
&& useradd --uid 1000 --gid spring-app --shell /bin/bash --create-home spring-app
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
USER spring-app:spring-app
WORKDIR /opt/workspace
COPY --from=app-build $BUILD_PATH/jdk $JAVA_HOME
COPY --from=app-build $BUILD_PATH/spring-boot-loader/ ./
COPY --from=app-build $BUILD_PATH/dependencies/ ./
COPY --from=app-build $BUILD_PATH/snapshot-dependencies/ ./
COPY --from=app-build $BUILD_PATH/application/ ./
EXPOSE 8080/tcp
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

15
compose.yml Normal file
View File

@ -0,0 +1,15 @@
services:
kodik-search:
image: anyame-kodik-search:latest
ports:
- 8080:8080
env_file: .env
networks:
- anyame-shared
- elk-network
networks:
anyame-shared:
external: true
elk-network:
external: true

151
pom.xml
View File

@ -1,73 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<modelVersion>4.0.0</modelVersion> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot</groupId> <parent>
<artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId>
<version>3.5.0</version> <artifactId>spring-boot-starter-parent</artifactId>
<relativePath/> <!-- lookup parent from repository --> <version>3.5.5</version>
</parent> <relativePath /> <!-- lookup parent from repository -->
<groupId>com.backend.search.kodik.service</groupId> </parent>
<artifactId>anyame-backend</artifactId> <groupId>com.backend.search.kodik.service</groupId>
<version>0.0.1-SNAPSHOT</version> <artifactId>anyame-kodik-search-backend</artifactId>
<name>anyame-backend</name> <version>0.0.1-SNAPSHOT</version>
<description>Kodik search service for anyame</description> <name>anyame-kodik-search-backend</name>
<url/> <description>Kodik search service for anyame</description>
<licenses> <url />
<license/> <licenses>
</licenses> <license />
<developers> </licenses>
<developer/> <developers>
</developers> <developer />
<scm> </developers>
<connection/> <scm>
<developerConnection/> <connection />
<tag/> <developerConnection />
<url/> <tag />
</scm> <url />
<properties> </scm>
<java.version>21</java.version> <properties>
<java.version>21</java.version>
<retrofit.version>3.0.0</retrofit.version> <retrofit.version>3.0.0</retrofit.version>
<spring-dotenv.version>4.0.0</spring-dotenv.version> <spring-dotenv.version>4.0.0</spring-dotenv.version>
</properties> <springdoc-openapi-starter.version>2.8.9</springdoc-openapi-starter.version>
<dependencies> <spring-eureka-client.version>4.3.0</spring-eureka-client.version>
<dependency> <logstash-logback-encoder.version>8.1</logstash-logback-encoder.version>
<groupId>org.springframework.boot</groupId> </properties>
<artifactId>spring-boot-starter-web</artifactId> <dependencies>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.squareup.retrofit2</groupId> <groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId> <artifactId>retrofit</artifactId>
<version>${retrofit.version}</version> <version>${retrofit.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.squareup.retrofit2</groupId> <groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId> <artifactId>converter-jackson</artifactId>
<version>${retrofit.version}</version> <version>${retrofit.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>me.paulschwarz</groupId> <groupId>me.paulschwarz</groupId>
<artifactId>spring-dotenv</artifactId> <artifactId>spring-dotenv</artifactId>
<version>${spring-dotenv.version}</version> <version>${spring-dotenv.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc-openapi-starter.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>${spring-eureka-client.version}</version>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>${logstash-logback-encoder.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

View File

@ -0,0 +1,27 @@
package com.backend.search.kodik.service.anyame_backend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootApplication
public class AnyameBackendApplication {
public static void main(String[] args) {
SpringApplication.run(AnyameBackendApplication.class, args);
}
// TODO: Research if this is good approach or not?
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("http://localhost:3000", "http://localhost:3001");
}
};
}
}

View File

@ -1,6 +1,7 @@
package com.backend.search.service.anyame_backend.api; package com.backend.search.kodik.service.anyame_backend.api;
import com.backend.search.kodik.service.anyame_backend.api.model.KodikResponse;
import com.backend.search.service.anyame_backend.api.model.KodikResponse;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.http.Field; import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded; import retrofit2.http.FormUrlEncoded;
@ -14,7 +15,6 @@ public interface KodikAPI {
@Field("token") String token, @Field("token") String token,
@Field("title") String title, @Field("title") String title,
@Field("limit") int limit, @Field("limit") int limit,
@Field("with_material_data") int withMaterialData @Field("with_material_data") int withMaterialData);
);
} }

View File

@ -1,4 +1,4 @@
package com.backend.search.service.anyame_backend.api.model; package com.backend.search.kodik.service.anyame_backend.api.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
@ -45,6 +45,18 @@ public class KodikResponse {
public String createdAt; public String createdAt;
@JsonProperty("updated_at") @JsonProperty("updated_at")
public String updatedAt; public String updatedAt;
@Override
public String toString() {
return "Result [id=" + id + ", type=" + type + ", link=" + link + ", title=" + title + ", titleOrig="
+ titleOrig + ", otherTitle=" + otherTitle + ", translation=" + translation + ", year=" + year
+ ", lastSeason=" + lastSeason + ", lastEpisode=" + lastEpisode + ", episodesCount=" + episodesCount
+ ", kinopoiskId=" + kinopoiskId + ", imdbId=" + imdbId + ", worldartLink=" + worldartLink
+ ", shikimoriId=" + shikimoriId + ", quality=" + quality + ", camrip=" + camrip + ", lgbt=" + lgbt
+ ", blockedCountries=" + blockedCountries + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt
+ ", materialData=" + materialData + ", screenshots=" + screenshots + "]";
}
@JsonProperty("material_data") @JsonProperty("material_data")
public MaterialData materialData; public MaterialData materialData;
public List<String> screenshots; public List<String> screenshots;
@ -57,4 +69,9 @@ public class KodikResponse {
public String type; public String type;
} }
@Override
public String toString() {
return "KodikResponse [total=" + total + ", results=" + results + ", toString()=" + super.toString() + "]";
}
} }

View File

@ -1,9 +1,11 @@
package com.backend.search.service.anyame_backend.api.model; package com.backend.search.kodik.service.anyame_backend.api.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List; import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class MaterialData { public class MaterialData {
public String title; public String title;
@ -80,4 +82,4 @@ public class MaterialData {
public List<String> designers; public List<String> designers;
public List<String> operators; public List<String> operators;
} }

View File

@ -1,11 +1,11 @@
package com.backend.search.service.anyame_backend.component; package com.backend.search.kodik.service.anyame_backend.component;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class KodikAPITokenProvider { public class KodikAPITokenProvider {
@Value("${KODIK_TOKEN}") @Value("${kodik.token}")
private String kodikToken; private String kodikToken;
public String getKodikToken() { public String getKodikToken() {

View File

@ -1,8 +1,10 @@
package com.backend.search.service.anyame_backend.config; package com.backend.search.kodik.service.anyame_backend.config;
import com.backend.search.service.anyame_backend.api.KodikAPI;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import com.backend.search.kodik.service.anyame_backend.api.KodikAPI;
import retrofit2.Retrofit; import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory; import retrofit2.converter.jackson.JacksonConverterFactory;

View File

@ -1,8 +1,7 @@
package com.backend.search.service.anyame_backend.controller; package com.backend.search.kodik.service.anyame_backend.controller;
import java.io.IOException;
import com.backend.search.service.anyame_backend.api.KodikAPI;
import com.backend.search.service.anyame_backend.api.model.KodikResponse;
import com.backend.search.service.anyame_backend.component.KodikAPITokenProvider;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -10,31 +9,38 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import retrofit2.Response;
import java.io.IOException; import com.backend.search.kodik.service.anyame_backend.api.KodikAPI;
import com.backend.search.kodik.service.anyame_backend.api.model.KodikResponse;
import com.backend.search.kodik.service.anyame_backend.component.KodikAPITokenProvider;
import com.backend.search.kodik.service.anyame_backend.service.KodikSearchFilterService;
import retrofit2.Response;
@RestController @RestController
public class SearchController { public class SearchController {
Logger log = LoggerFactory.getLogger(SearchController.class);
private static final Logger log = LoggerFactory.getLogger(SearchController.class);
private final KodikAPI kodikAPI; private final KodikAPI kodikAPI;
private final KodikAPITokenProvider tokenProvider; private final KodikAPITokenProvider tokenProvider;
private final KodikSearchFilterService searchFilterService;
public SearchController(KodikAPI kodikAPI, KodikAPITokenProvider tokenProvider) { public SearchController(KodikAPI kodikAPI, KodikAPITokenProvider tokenProvider,
KodikSearchFilterService searchFilterService) {
this.kodikAPI = kodikAPI; this.kodikAPI = kodikAPI;
this.tokenProvider = tokenProvider; this.tokenProvider = tokenProvider;
this.searchFilterService = searchFilterService;
} }
@GetMapping("/search") @GetMapping("/search")
public KodikResponse search(@RequestParam("title") String title) { public KodikResponse search(@RequestParam("title") String title) {
try { try {
Response<KodikResponse> response = kodikAPI.search(tokenProvider.getKodikToken(), title, 5, 1).execute(); Response<KodikResponse> response = kodikAPI.search(tokenProvider.getKodikToken(), title, 100, 1).execute();
if (!response.isSuccessful()) { if (!response.isSuccessful()) {
log.info("failed search request with title {}, response code {}, message {}", title, response.code(), response.message()); log.info("failed search request with title {}, response code {}, message {}", title, response.code(),
response.message());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "bad response, code: " + response.code()); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "bad response, code: " + response.code());
} }
return response.body(); return searchFilterService.filter(response.body());
} catch (IOException e) { } catch (IOException e) {
log.warn("i/o error", e); log.warn("i/o error", e);
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "i/o error"); throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "i/o error");

View File

@ -0,0 +1,44 @@
package com.backend.search.kodik.service.anyame_backend.service;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import com.backend.search.kodik.service.anyame_backend.api.model.KodikResponse;
import com.backend.search.kodik.service.anyame_backend.api.model.KodikResponse.Result;
@Service
public class KodikSearchFilterService {
Logger log = LoggerFactory.getLogger(KodikSearchFilterService.class);
private static final List<String> ALLOWED_TYPES = Arrays.asList("anime-serial", "anime");
public KodikResponse filter(KodikResponse body) {
Set<String> seenIds = new HashSet<>();
List<Result> filteredResults = body.results.stream()
.filter(result -> ALLOWED_TYPES.contains(result.type))
.filter(result -> {
String identifier = identifier(result);
boolean updated = seenIds.add(identifier);
return updated;
})
.toList();
body.results = filteredResults;
return body;
}
public String identifier(Result result) {
List<String> identifiers = Arrays.asList(result.kinopoiskId,
result.imdbId,
result.shikimoriId,
result.worldartLink);
return identifiers.stream().filter(identifier -> identifier != null && !identifier.isBlank()).findFirst()
.orElse(result.id) + Integer.toString(result.lastSeason);
}
}

View File

@ -1,13 +0,0 @@
package com.backend.search.service.anyame_backend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AnyameBackendApplication {
public static void main(String[] args) {
SpringApplication.run(AnyameBackendApplication.class, args);
}
}

View File

@ -1 +1,3 @@
spring.application.name=anyame-backend spring.application.name=anyame-kodik-search-backend
kodik.token=${KODIK_TOKEN}
eureka.client.serviceUrl.defaultZone: ${EUREKA_SCHEMA}://${EUREKA_HOST}/eureka/

View File

@ -0,0 +1,11 @@
<configuration>
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>logstash:5044</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>
<root level="INFO">
<appender-ref ref="LOGSTASH" />
</root>
</configuration>