feat: initial commit
This commit is contained in:
commit
aa63fc66b8
9
.env
Normal file
9
.env
Normal file
@ -0,0 +1,9 @@
|
||||
DB_URL=jdbc:postgresql://localhost:5432/mangamochi?currentSchema=mangamochi
|
||||
DB_USER=mangamochi
|
||||
DB_PASS=mangamochi
|
||||
|
||||
MINIO_ENDPOINT=http://omv.badger-pirarucu.ts.net:9000
|
||||
MINIO_USER=admin
|
||||
MINIO_PASS=!E9v4i0v3
|
||||
|
||||
WEBSCRAPPER_ENDPOINT=http://localhost:8000/url
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
250
.gitignore
vendored
Normal file
250
.gitignore
vendored
Normal file
@ -0,0 +1,250 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/linux,intellij,jetbrains,java,react
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=linux,intellij,jetbrains,java,react
|
||||
|
||||
target/
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Intellij Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||
.idea/**/sonarlint/
|
||||
|
||||
# SonarQube Plugin
|
||||
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||
.idea/**/sonarIssues.xml
|
||||
|
||||
# Markdown Navigator plugin
|
||||
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||
.idea/**/markdown-navigator.xml
|
||||
.idea/**/markdown-navigator-enh.xml
|
||||
.idea/**/markdown-navigator/
|
||||
|
||||
# Cache file creation bug
|
||||
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||
.idea/$CACHE_FILE$
|
||||
|
||||
# CodeStream plugin
|
||||
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||
.idea/codestream.xml
|
||||
|
||||
# Azure Toolkit for IntelliJ plugin
|
||||
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
|
||||
.idea/**/azureSettings.xml
|
||||
|
||||
### Java ###
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
### JetBrains ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
|
||||
# AWS User-specific
|
||||
|
||||
# Generated files
|
||||
|
||||
# Sensitive or high-churn files
|
||||
|
||||
# Gradle
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
|
||||
# Mongo Explorer plugin
|
||||
|
||||
# File-based project format
|
||||
|
||||
# IntelliJ
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
|
||||
# JIRA plugin
|
||||
|
||||
# Cursive Clojure plugin
|
||||
|
||||
# SonarLint plugin
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
|
||||
# Editor-based Rest Client
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
|
||||
### JetBrains Patch ###
|
||||
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||
|
||||
# *.iml
|
||||
# modules.xml
|
||||
# .idea/misc.xml
|
||||
# *.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
# https://plugins.jetbrains.com/plugin/7973-sonarlint
|
||||
|
||||
# SonarQube Plugin
|
||||
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
|
||||
|
||||
# Markdown Navigator plugin
|
||||
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
|
||||
|
||||
# Cache file creation bug
|
||||
# See https://youtrack.jetbrains.com/issue/JBR-2257
|
||||
|
||||
# CodeStream plugin
|
||||
# https://plugins.jetbrains.com/plugin/12206-codestream
|
||||
|
||||
# Azure Toolkit for IntelliJ plugin
|
||||
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### react ###
|
||||
.DS_*
|
||||
logs
|
||||
**/*.backup.*
|
||||
**/*.back.*
|
||||
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
*.sublime*
|
||||
|
||||
psd
|
||||
thumb
|
||||
sketch
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/linux,intellij,jetbrains,java,react
|
||||
27
HELP.md
Normal file
27
HELP.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Getting Started
|
||||
|
||||
### Reference Documentation
|
||||
For further reference, please consider the following sections:
|
||||
|
||||
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
|
||||
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.6/maven-plugin)
|
||||
* [Create an OCI image](https://docs.spring.io/spring-boot/3.5.6/maven-plugin/build-image.html)
|
||||
* [Spring Data JPA](https://docs.spring.io/spring-boot/3.5.6/reference/data/sql.html#data.sql.jpa-and-spring-data)
|
||||
* [Spring Web](https://docs.spring.io/spring-boot/3.5.6/reference/web/servlet.html)
|
||||
* [Flyway Migration](https://docs.spring.io/spring-boot/3.5.6/how-to/data-initialization.html#howto.data-initialization.migration-tool.flyway)
|
||||
|
||||
### Guides
|
||||
The following guides illustrate how to use some features concretely:
|
||||
|
||||
* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/)
|
||||
* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
|
||||
* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
|
||||
* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/)
|
||||
|
||||
### Maven Parent overrides
|
||||
|
||||
Due to Maven's design, elements are inherited from the parent POM to the project POM.
|
||||
While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the parent.
|
||||
To prevent this, the project POM contains empty overrides for these elements.
|
||||
If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.
|
||||
|
||||
58
docker-compose.yml
Normal file
58
docker-compose.yml
Normal file
@ -0,0 +1,58 @@
|
||||
services:
|
||||
mangamochi:
|
||||
container_name: mangamochi
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: mangamochi:latest
|
||||
depends_on:
|
||||
- db
|
||||
profiles:
|
||||
- all
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- mangamochi-network
|
||||
environment:
|
||||
- DB_URL=jdbc:postgresql://db:5432/mangamochi?currentSchema=mangamochi
|
||||
- DB_USER=mangamochi
|
||||
- DB_PASS=mangamochi
|
||||
- MINIO_ENDPOINT=http://omv.badger-pirarucu.ts.net:9000
|
||||
- MINIO_USER=admin
|
||||
- MINIO_PASS=!E9v4i0v3
|
||||
|
||||
db:
|
||||
image: 'postgres:15'
|
||||
container_name: mangamochi_db
|
||||
environment:
|
||||
- POSTGRES_USER=mangamochi
|
||||
- POSTGRES_PASSWORD=mangamochi
|
||||
profiles:
|
||||
- all
|
||||
- minimal
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
- mangamochi-network
|
||||
|
||||
rabbit-mq:
|
||||
image: 'rabbitmq:4-management'
|
||||
container_name: mangamochi_rabbit-mq
|
||||
profiles:
|
||||
- all
|
||||
- minimal
|
||||
ports:
|
||||
- 5672:5672
|
||||
- 15672:15672
|
||||
- 15692:15692
|
||||
networks:
|
||||
- mangamochi-network
|
||||
|
||||
networks:
|
||||
mangamochi-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
295
mvnw
vendored
Executable file
295
mvnw
vendored
Executable file
@ -0,0 +1,295 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
189
mvnw.cmd
vendored
Normal file
189
mvnw.cmd
vendored
Normal file
@ -0,0 +1,189 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
165
pom.xml
Normal file
165
pom.xml
Normal file
@ -0,0 +1,165 @@
|
||||
<?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"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.6</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.magamochi</groupId>
|
||||
<artifactId>mangamochi</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>mangamochi</name>
|
||||
<description>Demo project for Spring Boot</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-postgresql</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
<version>2.34.5</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.8.13</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
<version>4.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jsoup</groupId>
|
||||
<artifactId>jsoup</artifactId>
|
||||
<version>1.21.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.hypersistence</groupId>
|
||||
<artifactId>hypersistence-utils-hibernate-63</artifactId>
|
||||
<version>3.11.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>33.5.0-jre</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.13.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.13.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.13.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.diffplug.spotless</groupId>
|
||||
<artifactId>spotless-maven-plugin</artifactId>
|
||||
<version>2.46.1</version>
|
||||
<configuration>
|
||||
<java>
|
||||
<googleJavaFormat/>
|
||||
</java>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@ -0,0 +1,16 @@
|
||||
package com.magamochi.mangamochi;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableFeignClients
|
||||
@EnableScheduling
|
||||
public class MangamochiApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MangamochiApplication.class, args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.magamochi.mangamochi.client;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.JikanMangaResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.JikanMangaSearchResponseDTO;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@FeignClient(name = "jikan", url = "https://api.jikan.moe/v4/manga")
|
||||
public interface JikanClient {
|
||||
@GetMapping
|
||||
JikanMangaSearchResponseDTO mangaSearch(@RequestParam String q);
|
||||
|
||||
@GetMapping("/{id}")
|
||||
JikanMangaResponseDTO getMangaById(@PathVariable Long id);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.magamochi.mangamochi.client;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.RapidFuzzRequestDTO;
|
||||
import com.magamochi.mangamochi.model.dto.RapidFuzzResponseDTO;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@FeignClient(name = "rapidFuzz", url = "http://127.0.0.1:9000/match-title")
|
||||
public interface RapidFuzzClient {
|
||||
@PostMapping
|
||||
RapidFuzzResponseDTO mangaSearch(@RequestBody RapidFuzzRequestDTO dto);
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.magamochi.mangamochi.client;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.WebScrapperClientRequestDTO;
|
||||
import com.magamochi.mangamochi.model.dto.WebScrapperClientResponseDTO;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@FeignClient(name = "web-scrapper", url = "${web-scrapper.endpoint}")
|
||||
public interface WebScrapperClient {
|
||||
@PostMapping(
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
WebScrapperClientResponseDTO scrape(@RequestBody WebScrapperClientRequestDTO dto);
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.magamochi.mangamochi.config;
|
||||
|
||||
import java.net.URI;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.S3Configuration;
|
||||
|
||||
@Configuration
|
||||
public class S3ClientConfig {
|
||||
@Value("${minio.accessKey}")
|
||||
private String accessKey;
|
||||
|
||||
@Value("${minio.secretKey}")
|
||||
private String secretKey;
|
||||
|
||||
@Value("${minio.endpoint}")
|
||||
private String endpoint;
|
||||
|
||||
@Bean
|
||||
public S3Client s3Client() {
|
||||
var credentials = AwsBasicCredentials.create(accessKey, secretKey);
|
||||
|
||||
var configuration = S3Configuration.builder().pathStyleAccessEnabled(true).build();
|
||||
|
||||
return S3Client.builder()
|
||||
.endpointOverride(URI.create(endpoint))
|
||||
.credentialsProvider(StaticCredentialsProvider.create(credentials))
|
||||
.region(Region.US_EAST_1)
|
||||
.serviceConfiguration(configuration)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package com.magamochi.mangamochi.config;
|
||||
|
||||
import com.magamochi.mangamochi.security.JwtRequestFilter;
|
||||
import com.magamochi.mangamochi.service.CustomUserDetailsService;
|
||||
import com.magamochi.mangamochi.util.JwtUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final CustomUserDetailsService userDetailsService;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@Bean
|
||||
public JwtRequestFilter jwtRequestFilter() {
|
||||
return new JwtRequestFilter(jwtUtil, userDetailsService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DaoAuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
|
||||
return authConfig.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/auth/**").permitAll()
|
||||
.requestMatchers("/swagger-ui/**").permitAll()
|
||||
.requestMatchers("/api-docs/**").permitAll()
|
||||
.requestMatchers("/h2-console/**").permitAll()
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
.headers(headers -> headers
|
||||
.frameOptions(frame -> frame.disable())
|
||||
)
|
||||
.authenticationProvider(authenticationProvider())
|
||||
.addFilterBefore(jwtRequestFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOriginPatterns(List.of("http://localhost:3000"));
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
14
src/main/java/com/magamochi/mangamochi/config/WebConfig.java
Normal file
14
src/main/java/com/magamochi/mangamochi/config/WebConfig.java
Normal file
@ -0,0 +1,14 @@
|
||||
package com.magamochi.mangamochi.config;
|
||||
|
||||
import lombok.NonNull;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addCorsMappings(@NonNull CorsRegistry registry) {
|
||||
registry.addMapping("/**").allowedOrigins("*").allowedMethods("*").allowedHeaders("*");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package com.magamochi.mangamochi.controller;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.AuthenticationRequestDTO;
|
||||
import com.magamochi.mangamochi.model.dto.AuthenticationResponseDTO;
|
||||
import com.magamochi.mangamochi.model.entity.User;
|
||||
import com.magamochi.mangamochi.model.repository.UserRepository;
|
||||
import com.magamochi.mangamochi.util.JwtUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
@CrossOrigin(origins = "*")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthenticationController {
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final UserDetailsService userDetailsService;
|
||||
private final JwtUtil jwtUtil;
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> createAuthenticationToken(
|
||||
@RequestBody AuthenticationRequestDTO authenticationRequestDTO) {
|
||||
try {
|
||||
authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
authenticationRequestDTO.username(), authenticationRequestDTO.password()));
|
||||
} catch (BadCredentialsException e) {
|
||||
return ResponseEntity.badRequest().body("Incorrect username or password");
|
||||
}
|
||||
|
||||
final var userDetails =
|
||||
userDetailsService.loadUserByUsername(authenticationRequestDTO.username());
|
||||
final var jwt = jwtUtil.generateToken(userDetails);
|
||||
|
||||
var user = userRepository.findByUsername(userDetails.getUsername()).orElseThrow();
|
||||
var role = user.getRole();
|
||||
|
||||
return ResponseEntity.ok(new AuthenticationResponseDTO(jwt, userDetails.getUsername(), role));
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<?> registerUser(
|
||||
@RequestBody AuthenticationRequestDTO registrationRequestDTO) {
|
||||
if (userRepository.existsByUsername(registrationRequestDTO.username())) {
|
||||
return ResponseEntity.badRequest().body("Username is already taken");
|
||||
}
|
||||
|
||||
userRepository.save(
|
||||
User.builder()
|
||||
.username(registrationRequestDTO.username())
|
||||
.password(passwordEncoder.encode(registrationRequestDTO.password()))
|
||||
.build());
|
||||
|
||||
return ResponseEntity.ok("User registered successfully");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.magamochi.mangamochi.controller;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.GenreDTO;
|
||||
import com.magamochi.mangamochi.service.GenreService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/genres")
|
||||
@RequiredArgsConstructor
|
||||
public class GenreController {
|
||||
private final GenreService genreService;
|
||||
|
||||
@Operation(
|
||||
summary = "Get a list of genres",
|
||||
description = "Retrieve a list of genres.",
|
||||
tags = {"Genre"},
|
||||
operationId = "getGenres")
|
||||
@GetMapping
|
||||
public List<GenreDTO> getGenres() {
|
||||
return genreService.getGenres();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,140 @@
|
||||
package com.magamochi.mangamochi.controller;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.MangaChapterDTO;
|
||||
import com.magamochi.mangamochi.model.dto.MangaChapterImagesDTO;
|
||||
import com.magamochi.mangamochi.model.dto.MangaDTO;
|
||||
import com.magamochi.mangamochi.model.dto.MangaListDTO;
|
||||
import com.magamochi.mangamochi.model.enumeration.ArchiveFileType;
|
||||
import com.magamochi.mangamochi.model.specification.MangaSpecification;
|
||||
import com.magamochi.mangamochi.service.MangaService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
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 java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/mangas")
|
||||
@RequiredArgsConstructor
|
||||
public class MangaController {
|
||||
private final MangaService mangaService;
|
||||
|
||||
@Operation(
|
||||
summary = "Get a list of mangas",
|
||||
description = "Retrieve a list of mangas with their details.",
|
||||
tags = {"Manga"},
|
||||
operationId = "getMangas")
|
||||
@GetMapping
|
||||
public Page<MangaListDTO> getMangas(
|
||||
@ParameterObject MangaSpecification specification,
|
||||
@Parameter(hidden = true) @ParameterObject @PageableDefault Pageable pageable) {
|
||||
return mangaService.getMangas(specification, pageable);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Get the details of a manga",
|
||||
tags = {"Manga"},
|
||||
operationId = "getManga")
|
||||
@GetMapping("/{mangaId}")
|
||||
public MangaDTO getManga(@PathVariable Long mangaId) {
|
||||
return mangaService.getManga(mangaId);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Get the available chapters for a specific manga/provider combination",
|
||||
description = "Retrieve a list of manga chapters for a specific manga/provider combination.",
|
||||
tags = {"Manga"},
|
||||
operationId = "getMangaChapters")
|
||||
@GetMapping("/{mangaProviderId}/chapters")
|
||||
public List<MangaChapterDTO> getMangaChapters(@PathVariable Long mangaProviderId) {
|
||||
return mangaService.getMangaChapters(mangaProviderId);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Fetch the available chapters for a specific manga/provider combination",
|
||||
description = "Fetch a list of manga chapters for a specific manga/provider combination.",
|
||||
tags = {"Manga"},
|
||||
operationId = "fetchMangaChapters")
|
||||
@PostMapping("/{mangaProviderId}/fetch-chapters")
|
||||
public void fetchMangaChapters(@PathVariable Long mangaProviderId) {
|
||||
mangaService.fetchMangaChapters(mangaProviderId);
|
||||
}
|
||||
|
||||
@Operation(summary = "Fetch chapter", operationId = "fetchChapter")
|
||||
@PostMapping(value = "/chapter/{chapterId}/fetch")
|
||||
public ResponseEntity<Void> fetchChapter(@PathVariable Long chapterId) {
|
||||
mangaService.fetchChapter(chapterId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Get the available chapters for a specific manga/provider combination",
|
||||
description = "Retrieve a list of manga chapters for a specific manga/provider combination.",
|
||||
tags = {"Manga"},
|
||||
operationId = "getMangaChapterImages")
|
||||
@GetMapping("/{chapterId}/images")
|
||||
public MangaChapterImagesDTO getMangaChapterImages(@PathVariable Long chapterId) {
|
||||
return mangaService.getMangaChapterImages(chapterId);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Mark a chapter as read",
|
||||
description = "Mark a chapter as read by its ID.",
|
||||
tags = {"Manga"},
|
||||
operationId = "markAsRead")
|
||||
@PostMapping("/{chapterId}/mark-as-read")
|
||||
public void markAsRead(@PathVariable Long chapterId) {
|
||||
mangaService.markAsRead(chapterId);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Download all chapters for a manga provider",
|
||||
operationId = "downloadAllChapters")
|
||||
@PostMapping(value = "/chapter/{mangaProviderId}/download-all")
|
||||
public ResponseEntity<Void> downloadAllChapters(@PathVariable Long mangaProviderId) {
|
||||
mangaService.downloadAllChapters(mangaProviderId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Operation(summary = "Download chapter archive", operationId = "downloadChapterArchive")
|
||||
@ApiResponses({
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Successful download",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/octet-stream",
|
||||
schema = @Schema(type = "string", format = "binary"))),
|
||||
})
|
||||
@PostMapping(
|
||||
value = "/chapter/{chapterId}/download-archive",
|
||||
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
public ResponseEntity<byte[]> downloadChapterArchive(
|
||||
@PathVariable Long chapterId, @RequestParam ArchiveFileType archiveFileType)
|
||||
throws IOException {
|
||||
|
||||
var response = mangaService.downloadChapter(chapterId, archiveFileType);
|
||||
return ResponseEntity.ok()
|
||||
.header("Content-Disposition", "attachment; filename=\"" + response.filename() + "\"")
|
||||
.body(response.content());
|
||||
}
|
||||
|
||||
@Operation(summary = "Update manga info", operationId = "updateMangaInfo")
|
||||
@PostMapping(value = "/manga/{mangaId}/info")
|
||||
public ResponseEntity<Void> updateMangaInfo(@PathVariable Long mangaId) {
|
||||
|
||||
mangaService.updateInfo(mangaId);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
public record AuthenticationRequestDTO(String username, String password) {}
|
||||
@ -0,0 +1,3 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
public record AuthenticationResponseDTO(String token, String username, String role) {}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record ContentProviderMangaChapterResponseDTO(
|
||||
@NotBlank String chapterTitle, @NotBlank String chapterUrl) {}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import com.magamochi.mangamochi.model.enumeration.MangaStatus;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record ContentProviderMangaInfoResponseDTO(
|
||||
@NotBlank String title, @NotBlank String url, String imgUrl, MangaStatus status) {}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Genre;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record GenreDTO(@NotNull Long id, @NotBlank String name) {
|
||||
public static GenreDTO from(Genre genre) {
|
||||
return new GenreDTO(genre.getId(), genre.getName());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record JikanMangaResponseDTO(MangaData data) {
|
||||
public record MangaData(
|
||||
Long mal_id,
|
||||
ImageData images,
|
||||
List<String> title_synonyms,
|
||||
String status,
|
||||
boolean publishing,
|
||||
String synopsis,
|
||||
Double score,
|
||||
PublishData published,
|
||||
List<AuthorData> authors,
|
||||
List<GenreData> genres) {
|
||||
public record ImageData(ImageUrls jpg) {
|
||||
public record ImageUrls(String large_image_url) {}
|
||||
}
|
||||
|
||||
public record PublishData(OffsetDateTime from, OffsetDateTime to) {}
|
||||
|
||||
public record AuthorData(Long mal_id, String name) {}
|
||||
|
||||
public record GenreData(Long mal_id, String name) {}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record JikanMangaSearchResponseDTO(List<MangaData> data) {
|
||||
public record MangaData(Long mal_id, String title, List<TitleData> titles) {
|
||||
public record TitleData(String type, String title) {}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record MangaChapterArchiveDTO(@NotBlank String filename, @NotNull byte[] content) {}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.MangaChapter;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record MangaChapterDTO(
|
||||
@NotNull Long id,
|
||||
@NotBlank String title,
|
||||
@NotNull Boolean downloaded,
|
||||
@NotNull Boolean isRead) {
|
||||
public static MangaChapterDTO from(MangaChapter mangaChapter) {
|
||||
return new MangaChapterDTO(
|
||||
mangaChapter.getId(),
|
||||
mangaChapter.getTitle(),
|
||||
mangaChapter.getDownloaded(),
|
||||
mangaChapter.getRead());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.MangaChapter;
|
||||
import com.magamochi.mangamochi.model.entity.MangaChapterImage;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public record MangaChapterImagesDTO(
|
||||
@NotNull Long id,
|
||||
@NotBlank String mangaTitle,
|
||||
@NotNull List<@NotBlank String> chapterImageKeys) {
|
||||
public static MangaChapterImagesDTO from(MangaChapter mangaChapter) {
|
||||
return new MangaChapterImagesDTO(
|
||||
mangaChapter.getId(),
|
||||
mangaChapter.getTitle(),
|
||||
mangaChapter.getMangaChapterImages().stream()
|
||||
.sorted(Comparator.comparing(MangaChapterImage::getPosition))
|
||||
.map(mangaChapterImage -> mangaChapterImage.getImage().getFileKey())
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.UUID;
|
||||
|
||||
public record MangaChapterResponseDTO(
|
||||
@NotNull UUID id,
|
||||
// @NotNull Long mangaProviderId,
|
||||
@NotNull String chapterTitle,
|
||||
@NotNull String chapterUrl) {}
|
||||
@ -0,0 +1,63 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Manga;
|
||||
import com.magamochi.mangamochi.model.entity.MangaChapter;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record MangaDTO(
|
||||
@NotNull Long id,
|
||||
@NotBlank String title,
|
||||
String coverImageKey,
|
||||
String status,
|
||||
OffsetDateTime publishedFrom,
|
||||
OffsetDateTime publishedTo,
|
||||
String synopsis,
|
||||
Integer providerCount,
|
||||
@NotNull List<String> alternativeTitles,
|
||||
@NotNull List<String> genres,
|
||||
@NotNull List<String> authors,
|
||||
@NotNull Double score,
|
||||
@NotNull List<MangaProviderDTO> providers) {
|
||||
public static MangaDTO from(Manga manga) {
|
||||
return new MangaDTO(
|
||||
manga.getId(),
|
||||
manga.getTitle(),
|
||||
isNull(manga.getCoverImage()) ? null : manga.getCoverImage().getFileKey(),
|
||||
manga.getStatus(),
|
||||
manga.getPublishedFrom(),
|
||||
manga.getPublishedTo(),
|
||||
manga.getSynopsis(),
|
||||
manga.getMangaProviders().size(),
|
||||
manga.getAlternativeTitles(),
|
||||
manga.getMangaGenres().stream().map(mangaGenre -> mangaGenre.getGenre().getName()).toList(),
|
||||
manga.getMangaAuthors().stream()
|
||||
.map(mangaAuthor -> mangaAuthor.getAuthor().getName())
|
||||
.toList(),
|
||||
0.0,
|
||||
manga.getMangaProviders().stream().map(MangaProviderDTO::from).toList());
|
||||
}
|
||||
|
||||
public record MangaProviderDTO(
|
||||
@NotNull long id,
|
||||
@NotBlank String providerName,
|
||||
@NotNull Integer chaptersAvailable,
|
||||
@NotNull Integer chaptersDownloaded) {
|
||||
public static MangaProviderDTO from(MangaProvider mangaProvider) {
|
||||
var chapters = mangaProvider.getMangaChapters();
|
||||
var chaptersAvailable = chapters.size();
|
||||
var chaptersDownloaded = (int) chapters.stream().filter(MangaChapter::getDownloaded).count();
|
||||
|
||||
return new MangaProviderDTO(
|
||||
mangaProvider.getId(),
|
||||
mangaProvider.getProvider().getName(),
|
||||
chaptersAvailable,
|
||||
chaptersDownloaded);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Manga;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record MangaListDTO(
|
||||
@NotNull Long id,
|
||||
@NotBlank String title,
|
||||
String coverImageKey,
|
||||
String status,
|
||||
OffsetDateTime publishedFrom,
|
||||
OffsetDateTime publishedTo,
|
||||
Integer providerCount,
|
||||
@NotNull List<String> genres,
|
||||
@NotNull List<String> authors,
|
||||
@NotNull Double score) {
|
||||
public static MangaListDTO from(Manga manga) {
|
||||
return new MangaListDTO(
|
||||
manga.getId(),
|
||||
manga.getTitle(),
|
||||
nonNull(manga.getCoverImage()) ? manga.getCoverImage().getFileKey() : null,
|
||||
manga.getStatus(),
|
||||
manga.getPublishedFrom(),
|
||||
manga.getPublishedTo(),
|
||||
manga.getMangaProviders().size(),
|
||||
manga.getMangaGenres().stream().map(mangaGenre -> mangaGenre.getGenre().getName()).toList(),
|
||||
manga.getMangaAuthors().stream()
|
||||
.map(mangaAuthor -> mangaAuthor.getAuthor().getName())
|
||||
.toList(),
|
||||
0.0);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record MangaMessageDTO(
|
||||
String contentProviderName, List<ContentProviderMangaInfoResponseDTO> mangaInfoResponseDTOs) {}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record RapidFuzzRequestDTO(String title, List<String> options) {}
|
||||
@ -0,0 +1,3 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
public record RapidFuzzResponseDTO(boolean match_found, String best_match, double similarity) {}
|
||||
@ -0,0 +1,3 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
public record WebScrapperClientRequestDTO(String url) {}
|
||||
@ -0,0 +1,3 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
public record WebScrapperClientResponseDTO(String page_source) {}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "authors")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class Author {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private Long malId;
|
||||
|
||||
private String name;
|
||||
|
||||
@CreationTimestamp private Instant createdAt;
|
||||
|
||||
@UpdateTimestamp private Instant updatedAt;
|
||||
|
||||
@OneToMany(mappedBy = "author")
|
||||
private List<MangaAuthor> mangaAuthors;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.List;
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "genres")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class Genre {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private Long malId;
|
||||
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "genre")
|
||||
private List<MangaGenre> mangaGenres;
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "images")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class Image {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
private String fileKey;
|
||||
|
||||
@CreationTimestamp private Instant createdAt;
|
||||
|
||||
@UpdateTimestamp private Instant updatedAt;
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import io.hypersistence.utils.hibernate.type.array.ListArrayType;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.Type;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "mangas")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class Manga {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private Long malId;
|
||||
|
||||
private String title;
|
||||
|
||||
@Type(ListArrayType.class)
|
||||
@Column(name = "alternative_titles", columnDefinition = "text[]")
|
||||
private List<String> alternativeTitles;
|
||||
|
||||
// @Enumerated(EnumType.STRING)
|
||||
private String status;
|
||||
|
||||
private String synopsis;
|
||||
|
||||
@CreationTimestamp private Instant createdAt;
|
||||
|
||||
@UpdateTimestamp private Instant updatedAt;
|
||||
|
||||
@OneToMany(mappedBy = "manga")
|
||||
private List<MangaProvider> mangaProviders;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "cover_image_id")
|
||||
private Image coverImage;
|
||||
|
||||
private Double score;
|
||||
|
||||
private OffsetDateTime publishedFrom;
|
||||
|
||||
private OffsetDateTime publishedTo;
|
||||
|
||||
@OneToMany(mappedBy = "manga")
|
||||
private List<MangaAuthor> mangaAuthors;
|
||||
|
||||
@OneToMany(mappedBy = "manga")
|
||||
private List<MangaGenre> mangaGenres;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "manga_author")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class MangaAuthor {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "manga_id")
|
||||
private Manga manga;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "author_id")
|
||||
private Author author;
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "manga_chapters")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class MangaChapter {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "manga_provider_id")
|
||||
private MangaProvider mangaProvider;
|
||||
|
||||
private String title;
|
||||
|
||||
private String url;
|
||||
|
||||
@Builder.Default private Boolean downloaded = false;
|
||||
|
||||
@Builder.Default private Boolean read = false;
|
||||
|
||||
@CreationTimestamp private Instant createdAt;
|
||||
|
||||
@UpdateTimestamp private Instant updatedAt;
|
||||
|
||||
@OneToMany(mappedBy = "mangaChapter")
|
||||
private List<MangaChapterImage> mangaChapterImages;
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "manga_chapter_images")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class MangaChapterImage {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "manga_chapter_id")
|
||||
private MangaChapter mangaChapter;
|
||||
|
||||
@OneToOne
|
||||
@JoinColumn(name = "image_id")
|
||||
private Image image;
|
||||
|
||||
private int position;
|
||||
|
||||
@CreationTimestamp private Instant createdAt;
|
||||
|
||||
@UpdateTimestamp private Instant updatedAt;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "manga_genre")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class MangaGenre {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "manga_id")
|
||||
private Manga manga;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "genre_id")
|
||||
private Genre genre;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "manga_import_reviews")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class MangaImportReview {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String title;
|
||||
|
||||
private String url;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "provider_id")
|
||||
private Provider provider;
|
||||
|
||||
@CreationTimestamp private Instant createdAt;
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "manga_provider")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class MangaProvider {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "manga_id", nullable = false)
|
||||
private Manga manga;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "provider_id", nullable = false)
|
||||
private Provider provider;
|
||||
|
||||
private String mangaTitle;
|
||||
|
||||
private String url;
|
||||
|
||||
@OneToMany(mappedBy = "mangaProvider")
|
||||
List<MangaChapter> mangaChapters;
|
||||
|
||||
@CreationTimestamp private Instant createdAt;
|
||||
|
||||
@UpdateTimestamp private Instant updatedAt;
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import com.magamochi.mangamochi.model.enumeration.ProviderStatus;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "providers")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class Provider {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private ProviderStatus status;
|
||||
|
||||
@CreationTimestamp private Instant createdAt;
|
||||
|
||||
@UpdateTimestamp private Instant updatedAt;
|
||||
|
||||
@OneToMany(mappedBy = "provider")
|
||||
List<MangaProvider> mangaProviders;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.magamochi.mangamochi.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true, nullable = false)
|
||||
private String username;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String password;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String role;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.magamochi.mangamochi.model.enumeration;
|
||||
|
||||
public enum ArchiveFileType {
|
||||
CBZ,
|
||||
CBR
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.magamochi.mangamochi.model.enumeration;
|
||||
|
||||
public enum MangaStatus {
|
||||
ONGOING,
|
||||
COMPLETED,
|
||||
HIATUS,
|
||||
CANCELLED,
|
||||
UNKNOWN
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.magamochi.mangamochi.model.enumeration;
|
||||
|
||||
public enum ProviderStatus {
|
||||
ACTIVE,
|
||||
INACTIVE
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Author;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface AuthorRepository extends JpaRepository<Author, Long> {
|
||||
Optional<Author> findByMalId(Long aLong);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Genre;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface GenreRepository extends JpaRepository<Genre, Long> {
|
||||
Optional<Genre> findByMalId(Long malId);
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Image;
|
||||
import java.util.UUID;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ImageRepository extends JpaRepository<Image, UUID> {}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Author;
|
||||
import com.magamochi.mangamochi.model.entity.Manga;
|
||||
import com.magamochi.mangamochi.model.entity.MangaAuthor;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MangaAuthorRepository extends JpaRepository<MangaAuthor, Long> {
|
||||
Optional<MangaAuthor> findByMangaAndAuthor(Manga manga, Author author);
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.MangaChapter;
|
||||
import com.magamochi.mangamochi.model.entity.MangaChapterImage;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MangaChapterImageRepository extends JpaRepository<MangaChapterImage, Long> {
|
||||
List<MangaChapterImage> findAllByMangaChapter(MangaChapter mangaChapter);
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.MangaChapter;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MangaChapterRepository extends JpaRepository<MangaChapter, Long> {
|
||||
Optional<MangaChapter> findByMangaProviderAndUrlIgnoreCase(
|
||||
MangaProvider mangaProvider, @NotBlank String url);
|
||||
|
||||
List<MangaChapter> findByMangaProviderId(Long mangaProvider_id);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.*;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MangaGenreRepository extends JpaRepository<MangaGenre, Long> {
|
||||
Optional<MangaGenre> findByMangaAndGenre(Manga manga, Genre genre);
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.MangaImportReview;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MangaImportReviewRepository extends JpaRepository<MangaImportReview, Long> {}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Manga;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import com.magamochi.mangamochi.model.entity.Provider;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface MangaProviderRepository extends JpaRepository<MangaProvider, Long> {
|
||||
Optional<MangaProvider> findByMangaAndProvider(Manga manga, Provider provider);
|
||||
|
||||
Optional<MangaProvider> findByMangaTitleIgnoreCaseAndProvider(
|
||||
String mangaTitle, Provider provider);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Manga;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
public interface MangaRepository
|
||||
extends JpaRepository<Manga, Long>, JpaSpecificationExecutor<Manga> {
|
||||
Optional<Manga> findByTitleIgnoreCase(String title);
|
||||
|
||||
Optional<Manga> findByMalId(Long malId);
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Provider;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ProviderRepository extends JpaRepository<Provider, Long> {
|
||||
Optional<Provider> findByNameIgnoreCase(String name);
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.magamochi.mangamochi.model.repository;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.User;
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
boolean existsByUsername(String username);
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package com.magamochi.mangamochi.model.specification;
|
||||
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Author;
|
||||
import com.magamochi.mangamochi.model.entity.Manga;
|
||||
import jakarta.persistence.criteria.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.NonNull;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
public record MangaSpecification(String searchQuery, List<Long> genreIds, List<String> statuses)
|
||||
implements Specification<Manga> {
|
||||
@Override
|
||||
public Predicate toPredicate(
|
||||
@NonNull Root<Manga> root, CriteriaQuery<?> query, @NonNull CriteriaBuilder criteriaBuilder) {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
if (StringUtils.isNotBlank(searchQuery)) {
|
||||
var searchPattern = "%" + searchQuery.toLowerCase() + "%";
|
||||
|
||||
var titlePredicate =
|
||||
criteriaBuilder.like(criteriaBuilder.lower(root.get("title")), searchPattern);
|
||||
|
||||
var authorsPredicate = searchInAuthors(root, query, criteriaBuilder, searchPattern);
|
||||
|
||||
var searchPredicate = criteriaBuilder.or(titlePredicate, authorsPredicate);
|
||||
|
||||
predicates.add(searchPredicate);
|
||||
}
|
||||
|
||||
if (nonNull(genreIds) && !genreIds.isEmpty()) {
|
||||
var genreJoin = root.join("mangaGenres", JoinType.LEFT);
|
||||
predicates.add(genreJoin.get("genre").get("id").in(genreIds));
|
||||
}
|
||||
|
||||
if (nonNull(statuses) && !statuses.isEmpty()) {
|
||||
predicates.add(
|
||||
criteriaBuilder
|
||||
.lower(root.get("status"))
|
||||
.in(statuses.stream().map(String::toLowerCase).toList()));
|
||||
}
|
||||
|
||||
query.distinct(true);
|
||||
|
||||
return criteriaBuilder.and(predicates.toArray(Predicate[]::new));
|
||||
}
|
||||
|
||||
private Predicate searchInAuthors(
|
||||
Root<Manga> root,
|
||||
CriteriaQuery<?> query,
|
||||
CriteriaBuilder criteriaBuilder,
|
||||
String searchPattern) {
|
||||
Join<Manga, ?> mangaAuthorsJoin = root.join("mangaAuthors", JoinType.LEFT);
|
||||
Join<Object, Author> authorJoin = mangaAuthorsJoin.join("author", JoinType.LEFT);
|
||||
|
||||
return criteriaBuilder.like(criteriaBuilder.lower(authorJoin.get("name")), searchPattern);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package com.magamochi.mangamochi.security;
|
||||
|
||||
import com.magamochi.mangamochi.service.CustomUserDetailsService;
|
||||
import com.magamochi.mangamochi.util.JwtUtil;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtRequestFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
private final CustomUserDetailsService userDetailsService;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain chain) throws ServletException, IOException {
|
||||
|
||||
final String authorizationHeader = request.getHeader("Authorization");
|
||||
|
||||
String username = null;
|
||||
String jwt = null;
|
||||
|
||||
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
|
||||
jwt = authorizationHeader.substring(7);
|
||||
try {
|
||||
username = jwtUtil.extractUsername(jwt);
|
||||
} catch (Exception e) {
|
||||
logger.warn("JWT token validation failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
|
||||
|
||||
if (jwtUtil.validateToken(jwt, userDetails)) {
|
||||
UsernamePasswordAuthenticationToken authToken =
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.magamochi.mangamochi.service;
|
||||
|
||||
import com.magamochi.mangamochi.model.repository.UserRepository;
|
||||
import java.util.Collections;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
var user =
|
||||
userRepository
|
||||
.findByUsername(username)
|
||||
.orElseThrow(
|
||||
() -> new UsernameNotFoundException("User not found with username: " + username));
|
||||
|
||||
return new User(
|
||||
user.getUsername(),
|
||||
user.getPassword(),
|
||||
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole())));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.magamochi.mangamochi.service;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.GenreDTO;
|
||||
import com.magamochi.mangamochi.model.repository.GenreRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GenreService {
|
||||
private final GenreRepository genreRepository;
|
||||
|
||||
public List<GenreDTO> getGenres() {
|
||||
var genres = genreRepository.findAll();
|
||||
|
||||
return genres.stream().map(GenreDTO::from).toList();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.magamochi.mangamochi.service;
|
||||
|
||||
import com.magamochi.mangamochi.model.entity.Image;
|
||||
import com.magamochi.mangamochi.model.repository.ImageRepository;
|
||||
import java.io.InputStream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ImageService {
|
||||
private final S3Service s3Service;
|
||||
private final ImageRepository imageRepository;
|
||||
|
||||
public Image uploadImage(byte[] data, String contentType, String path) {
|
||||
var fileKey = s3Service.uploadFile(data, contentType, path);
|
||||
|
||||
return imageRepository.save(Image.builder().fileKey(fileKey).build());
|
||||
}
|
||||
|
||||
public InputStream getImageStream(Image image) {
|
||||
return s3Service.getFile(image.getFileKey());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
package com.magamochi.mangamochi.service;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.magamochi.mangamochi.client.JikanClient;
|
||||
import com.magamochi.mangamochi.client.RapidFuzzClient;
|
||||
import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.JikanMangaSearchResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.RapidFuzzRequestDTO;
|
||||
import com.magamochi.mangamochi.model.entity.Manga;
|
||||
import com.magamochi.mangamochi.model.entity.MangaImportReview;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import com.magamochi.mangamochi.model.entity.Provider;
|
||||
import com.magamochi.mangamochi.model.enumeration.ProviderStatus;
|
||||
import com.magamochi.mangamochi.model.repository.MangaImportReviewRepository;
|
||||
import com.magamochi.mangamochi.model.repository.MangaProviderRepository;
|
||||
import com.magamochi.mangamochi.model.repository.MangaRepository;
|
||||
import com.magamochi.mangamochi.model.repository.ProviderRepository;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Log4j2
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MangaListService {
|
||||
private final ProviderRepository providerRepository;
|
||||
private final MangaRepository mangaRepository;
|
||||
private final MangaProviderRepository mangaProviderRepository;
|
||||
private final MangaImportReviewRepository mangaImportReviewRepository;
|
||||
|
||||
private final JikanClient jikanClient;
|
||||
private final RapidFuzzClient rapidFuzzClient;
|
||||
|
||||
public void updateMangaList(
|
||||
String contentProviderName, List<ContentProviderMangaInfoResponseDTO> mangaInfoResponseDTOs) {
|
||||
var rateLimiter = RateLimiter.create(1);
|
||||
|
||||
var provider =
|
||||
providerRepository
|
||||
.findByNameIgnoreCase(contentProviderName)
|
||||
.orElseGet(
|
||||
() ->
|
||||
providerRepository.save(
|
||||
Provider.builder()
|
||||
.name(contentProviderName)
|
||||
.status(ProviderStatus.ACTIVE)
|
||||
.build()));
|
||||
|
||||
mangaInfoResponseDTOs.forEach(
|
||||
mangaResponse -> {
|
||||
var mangaProvider =
|
||||
mangaProviderRepository.findByMangaTitleIgnoreCaseAndProvider(
|
||||
mangaResponse.title(), provider);
|
||||
|
||||
if (mangaProvider.isPresent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
rateLimiter.acquire();
|
||||
var manga = getOrCreateManga(mangaResponse.title(), mangaResponse.url(), provider);
|
||||
|
||||
if (isNull(manga)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
mangaProviderRepository.save(
|
||||
MangaProvider.builder()
|
||||
.manga(manga)
|
||||
.mangaTitle(mangaResponse.title())
|
||||
.provider(provider)
|
||||
.url(mangaResponse.url())
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Manga getOrCreateManga(String title, String url, Provider provider) {
|
||||
var existingManga = mangaRepository.findByTitleIgnoreCase(title);
|
||||
if (existingManga.isPresent()) {
|
||||
return existingManga.get();
|
||||
}
|
||||
|
||||
var jikanResults = jikanClient.mangaSearch(title).data();
|
||||
if (jikanResults.isEmpty()) {
|
||||
createMangaImportReview(title, url, provider);
|
||||
log.warn("No manga found with title {}", title);
|
||||
return null;
|
||||
}
|
||||
|
||||
var request =
|
||||
new RapidFuzzRequestDTO(
|
||||
title,
|
||||
jikanResults.stream()
|
||||
.flatMap(
|
||||
results ->
|
||||
results.titles().stream()
|
||||
.map(JikanMangaSearchResponseDTO.MangaData.TitleData::title))
|
||||
.toList());
|
||||
|
||||
var fuzzResults = rapidFuzzClient.mangaSearch(request);
|
||||
if (!fuzzResults.match_found()) {
|
||||
createMangaImportReview(title, url, provider);
|
||||
log.warn("No match found for manga with title {}", title);
|
||||
return null;
|
||||
}
|
||||
|
||||
var resultOptional =
|
||||
jikanResults.stream()
|
||||
.filter(
|
||||
results ->
|
||||
results.titles().stream()
|
||||
.map(JikanMangaSearchResponseDTO.MangaData.TitleData::title)
|
||||
.toList()
|
||||
.contains(fuzzResults.best_match()))
|
||||
.findFirst();
|
||||
if (resultOptional.isEmpty()) {
|
||||
createMangaImportReview(title, url, provider);
|
||||
log.warn("No match found for manga with title {}", title);
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = resultOptional.get();
|
||||
|
||||
existingManga = mangaRepository.findByTitleIgnoreCase(result.title());
|
||||
return existingManga.orElseGet(
|
||||
() ->
|
||||
mangaRepository.save(
|
||||
Manga.builder().title(result.title()).malId(result.mal_id()).build()));
|
||||
}
|
||||
|
||||
private void createMangaImportReview(String title, String url, Provider provider) {
|
||||
mangaImportReviewRepository.save(
|
||||
MangaImportReview.builder().title(title).url(url).provider(provider).build());
|
||||
}
|
||||
}
|
||||
239
src/main/java/com/magamochi/mangamochi/service/MangaService.java
Normal file
239
src/main/java/com/magamochi/mangamochi/service/MangaService.java
Normal file
@ -0,0 +1,239 @@
|
||||
package com.magamochi.mangamochi.service;
|
||||
|
||||
import com.magamochi.mangamochi.client.JikanClient;
|
||||
import com.magamochi.mangamochi.model.dto.*;
|
||||
import com.magamochi.mangamochi.model.entity.MangaChapter;
|
||||
import com.magamochi.mangamochi.model.entity.MangaChapterImage;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import com.magamochi.mangamochi.model.enumeration.ArchiveFileType;
|
||||
import com.magamochi.mangamochi.model.repository.MangaChapterImageRepository;
|
||||
import com.magamochi.mangamochi.model.repository.MangaChapterRepository;
|
||||
import com.magamochi.mangamochi.model.repository.MangaProviderRepository;
|
||||
import com.magamochi.mangamochi.model.repository.MangaRepository;
|
||||
import com.magamochi.mangamochi.model.specification.MangaSpecification;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProviderFactory;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.net.URL;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.tomcat.util.http.fileupload.IOUtils;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MangaService {
|
||||
private final MangaChapterRepository mangaChapterRepository;
|
||||
private final MangaRepository mangaRepository;
|
||||
private final MangaProviderRepository mangaProviderRepository;
|
||||
private final MangaChapterImageRepository mangaChapterImageRepository;
|
||||
private final ImageService imageService;
|
||||
|
||||
private final JikanClient jikanClient;
|
||||
|
||||
private final ContentProviderFactory contentProviderFactory;
|
||||
|
||||
public Page<MangaListDTO> getMangas(MangaSpecification specification, Pageable pageable) {
|
||||
return mangaRepository.findAll(specification, pageable).map(MangaListDTO::from);
|
||||
}
|
||||
|
||||
public List<MangaChapterDTO> getMangaChapters(Long mangaProviderId) {
|
||||
var mangaProvider =
|
||||
mangaProviderRepository
|
||||
.findById(mangaProviderId)
|
||||
.orElseThrow(() -> new RuntimeException("manga provider not found"));
|
||||
|
||||
return mangaProvider.getMangaChapters().stream()
|
||||
.sorted(Comparator.comparing(MangaChapter::getId))
|
||||
.map(MangaChapterDTO::from)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public void fetchChapter(Long chapterId) {
|
||||
try {
|
||||
var chapter =
|
||||
mangaChapterRepository
|
||||
.findById(chapterId)
|
||||
.orElseThrow(() -> new RuntimeException("Chapter not found"));
|
||||
|
||||
var mangaProvider = chapter.getMangaProvider();
|
||||
var provider =
|
||||
contentProviderFactory.getContentProvider(mangaProvider.getProvider().getName());
|
||||
|
||||
var chapterImagesUrls = provider.getChapterImagesUrls(chapter.getUrl());
|
||||
if (chapterImagesUrls.isEmpty()) {
|
||||
throw new RuntimeException("Chapter image not found");
|
||||
}
|
||||
|
||||
var chapterImages =
|
||||
chapterImagesUrls.entrySet().stream()
|
||||
.map(
|
||||
entry -> {
|
||||
try {
|
||||
var inputStream =
|
||||
new BufferedInputStream(
|
||||
new URL(new URI(entry.getValue()).toASCIIString()).openStream());
|
||||
|
||||
var bytes = inputStream.readAllBytes();
|
||||
|
||||
inputStream.close();
|
||||
var image =
|
||||
imageService.uploadImage(bytes, "image/jpeg", "chapter/" + chapterId);
|
||||
|
||||
return MangaChapterImage.builder()
|
||||
.mangaChapter(chapter)
|
||||
.position(entry.getKey())
|
||||
.image(image)
|
||||
.build();
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.toList();
|
||||
|
||||
mangaChapterImageRepository.saveAll(chapterImages);
|
||||
|
||||
chapter.setDownloaded(true);
|
||||
mangaChapterRepository.save(chapter);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public MangaChapterArchiveDTO downloadChapter(Long chapterId, ArchiveFileType archiveFileType)
|
||||
throws IOException {
|
||||
var chapter =
|
||||
mangaChapterRepository
|
||||
.findById(chapterId)
|
||||
.orElseThrow(() -> new RuntimeException("Chapter not found"));
|
||||
|
||||
var chapterImages = mangaChapterImageRepository.findAllByMangaChapter(chapter);
|
||||
|
||||
var byteArrayOutputStream =
|
||||
switch (archiveFileType) {
|
||||
case CBZ -> getChapterCbzArchive(chapterImages);
|
||||
default -> throw new RuntimeException("Unsupported archive file type");
|
||||
};
|
||||
|
||||
return new MangaChapterArchiveDTO(
|
||||
chapter.getTitle() + ".cbz", byteArrayOutputStream.toByteArray());
|
||||
}
|
||||
|
||||
private ByteArrayOutputStream getChapterCbzArchive(List<MangaChapterImage> chapterImages)
|
||||
throws IOException {
|
||||
var byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
var bufferedOutputStream = new BufferedOutputStream(byteArrayOutputStream);
|
||||
var zipOutputStream = new ZipOutputStream(bufferedOutputStream);
|
||||
|
||||
var totalPages = chapterImages.size();
|
||||
var paddingLength = String.valueOf(totalPages).length();
|
||||
|
||||
for (var pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
|
||||
var imgSrc = chapterImages.get(pageNumber - 1);
|
||||
|
||||
var paddedFileName = String.format("%0" + paddingLength + "d.jpg", imgSrc.getPosition());
|
||||
|
||||
zipOutputStream.putNextEntry(new ZipEntry(paddedFileName));
|
||||
IOUtils.copy(imageService.getImageStream(imgSrc.getImage()), zipOutputStream);
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
|
||||
zipOutputStream.finish();
|
||||
zipOutputStream.flush();
|
||||
IOUtils.closeQuietly(zipOutputStream);
|
||||
return byteArrayOutputStream;
|
||||
}
|
||||
|
||||
private void persistMangaChapters(
|
||||
MangaProvider mangaProvider, ContentProviderMangaChapterResponseDTO chapter) {
|
||||
var mangaChapter =
|
||||
mangaChapterRepository
|
||||
.findByMangaProviderAndUrlIgnoreCase(mangaProvider, chapter.chapterUrl())
|
||||
.orElseGet(MangaChapter::new);
|
||||
|
||||
mangaChapter.setMangaProvider(mangaProvider);
|
||||
mangaChapter.setTitle(chapter.chapterTitle());
|
||||
mangaChapter.setUrl(chapter.chapterUrl());
|
||||
|
||||
mangaChapterRepository.save(mangaChapter);
|
||||
}
|
||||
|
||||
public void downloadAllChapters(Long mangaProviderId) {
|
||||
var mangaProvider =
|
||||
mangaProviderRepository
|
||||
.findById(mangaProviderId)
|
||||
.orElseThrow(() -> new RuntimeException("Manga provider not found"));
|
||||
|
||||
var mangaChapters = mangaChapterRepository.findByMangaProviderId(mangaProviderId);
|
||||
|
||||
mangaChapters.forEach(mangaChapter -> fetchChapter(mangaChapter.getId()));
|
||||
}
|
||||
|
||||
public void updateInfo(Long mangaId) {
|
||||
var manga =
|
||||
mangaRepository
|
||||
.findById(mangaId)
|
||||
.orElseThrow(() -> new RuntimeException("Manga not found"));
|
||||
|
||||
var mangaSearchResponse = jikanClient.mangaSearch(manga.getTitle());
|
||||
if (mangaSearchResponse.data().isEmpty()) {
|
||||
throw new RuntimeException("Manga not found");
|
||||
}
|
||||
|
||||
// TODO: create logic to select appropriate manga
|
||||
var mangaResponse = mangaSearchResponse.data().getFirst();
|
||||
manga.setTitle(mangaResponse.title());
|
||||
manga.setMalId(mangaResponse.mal_id());
|
||||
|
||||
mangaRepository.save(manga);
|
||||
}
|
||||
|
||||
public MangaDTO getManga(Long mangaId) {
|
||||
var manga =
|
||||
mangaRepository
|
||||
.findById(mangaId)
|
||||
.orElseThrow(() -> new RuntimeException("Manga not found"));
|
||||
|
||||
return MangaDTO.from(manga);
|
||||
}
|
||||
|
||||
public void fetchMangaChapters(Long mangaProviderId) {
|
||||
var mangaProvider =
|
||||
mangaProviderRepository
|
||||
.findById(mangaProviderId)
|
||||
.orElseThrow(() -> new RuntimeException("manga provider not found"));
|
||||
|
||||
var contentProvider =
|
||||
contentProviderFactory.getContentProvider(mangaProvider.getProvider().getName());
|
||||
var availableChapters = contentProvider.getAvailableChapters(mangaProvider);
|
||||
|
||||
availableChapters.forEach(chapter -> persistMangaChapters(mangaProvider, chapter));
|
||||
}
|
||||
|
||||
public MangaChapterImagesDTO getMangaChapterImages(Long chapterId) {
|
||||
var chapter =
|
||||
mangaChapterRepository
|
||||
.findById(chapterId)
|
||||
.orElseThrow(() -> new RuntimeException("Chapter not found"));
|
||||
|
||||
return MangaChapterImagesDTO.from(chapter);
|
||||
}
|
||||
|
||||
public void markAsRead(Long chapterId) {
|
||||
var chapter =
|
||||
mangaChapterRepository
|
||||
.findById(chapterId)
|
||||
.orElseThrow(() -> new RuntimeException("Chapter not found"));
|
||||
chapter.setRead(true);
|
||||
|
||||
mangaChapterRepository.save(chapter);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package com.magamochi.mangamochi.service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class S3Service {
|
||||
@Value("${minio.bucket}")
|
||||
private String bucket;
|
||||
|
||||
private final S3Client s3Client;
|
||||
|
||||
public String uploadFile(byte[] data, String contentType, String path) {
|
||||
var filename = "manga/" + path + "/" + UUID.randomUUID();
|
||||
|
||||
var request =
|
||||
PutObjectRequest.builder().bucket(bucket).key(filename).contentType(contentType).build();
|
||||
|
||||
s3Client.putObject(request, RequestBody.fromBytes(data));
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
public InputStream getFile(String key) {
|
||||
var request = GetObjectRequest.builder().bucket(bucket).key(key).build();
|
||||
|
||||
return s3Client.getObject(request);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.magamochi.mangamochi.service;
|
||||
|
||||
import com.magamochi.mangamochi.client.WebScrapperClient;
|
||||
import com.magamochi.mangamochi.model.dto.WebScrapperClientRequestDTO;
|
||||
import java.io.IOException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WebScrapperClientProxyService {
|
||||
private final WebScrapperClient webScrapperClient;
|
||||
|
||||
public Document scrapeToJsoupDocument(String url) throws IOException {
|
||||
var htmlContent = scrape(url);
|
||||
return Jsoup.parse(htmlContent);
|
||||
}
|
||||
|
||||
private String scrape(String url) {
|
||||
return webScrapperClient.scrape(new WebScrapperClientRequestDTO(url)).page_source();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package com.magamochi.mangamochi.service.providers;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface ContentProvider {
|
||||
List<ContentProviderMangaInfoResponseDTO> getAvailableMangas();
|
||||
|
||||
List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(MangaProvider provider);
|
||||
|
||||
Map<Integer, String> getChapterImagesUrls(String chapterUrl);
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.magamochi.mangamochi.service.providers;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Getter
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ContentProviderFactory {
|
||||
private final Map<String, ContentProvider> contentProviders;
|
||||
|
||||
public ContentProvider getContentProvider(String providerName) {
|
||||
var provider = contentProviders.get(providerName);
|
||||
|
||||
if (Objects.isNull(provider)) {
|
||||
throw new IllegalArgumentException("No such provider " + providerName);
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.magamochi.mangamochi.service.providers;
|
||||
|
||||
public class ContentProviders {
|
||||
public static final String MANGA_LIVRE = "Manga Livre";
|
||||
public static final String MANGA_LIVRE_BLOG = "Manga Livre Blog";
|
||||
}
|
||||
@ -0,0 +1,170 @@
|
||||
package com.magamochi.mangamochi.service.providers.impl;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import com.magamochi.mangamochi.model.enumeration.MangaStatus;
|
||||
import com.magamochi.mangamochi.service.WebScrapperClientProxyService;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProvider;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProviders;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Log4j2
|
||||
@Service(ContentProviders.MANGA_LIVRE_BLOG)
|
||||
@RequiredArgsConstructor
|
||||
public class MangaLivreBlogProvider implements ContentProvider {
|
||||
private static final Pattern NUMERIC_PATTERN = Pattern.compile("-?\\d+");
|
||||
|
||||
private final String url = "https://mangalivre.blog/manga/";
|
||||
|
||||
private final WebScrapperClientProxyService webScrapperClientProxyService;
|
||||
|
||||
@Override
|
||||
public List<ContentProviderMangaInfoResponseDTO> getAvailableMangas() {
|
||||
var totalPages = getTotalPages();
|
||||
|
||||
if (Objects.isNull(totalPages) || totalPages < 1) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return IntStream.rangeClosed(1, totalPages)
|
||||
.mapToObj(this::getMangasFromPage)
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(List::stream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(
|
||||
MangaProvider mangaProvider) {
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(mangaProvider.getUrl());
|
||||
|
||||
var chapterList = document.getElementsByClass("chapters-list").getFirst();
|
||||
var chapterItems = chapterList.getElementsByClass("chapter-item");
|
||||
|
||||
return chapterItems.stream()
|
||||
.map(
|
||||
chapterItemElement -> {
|
||||
var chapterDetailsContainer =
|
||||
chapterItemElement.getElementsByClass("chapter-details").getFirst();
|
||||
var linkElement = chapterDetailsContainer.getElementsByTag("a").getFirst();
|
||||
var chapterNumberElement =
|
||||
linkElement.getElementsByClass("chapter-number").getFirst();
|
||||
|
||||
return new ContentProviderMangaChapterResponseDTO(
|
||||
chapterNumberElement.text(), linkElement.attr("href"));
|
||||
})
|
||||
.toList();
|
||||
} catch (IOException | NoSuchElementException e) {
|
||||
log.error("Error fetching mangas from MangaLivre", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Integer, String> getChapterImagesUrls(String chapterUrl) {
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(chapterUrl);
|
||||
|
||||
var chapterImageContainers = document.getElementsByClass("chapter-image-container");
|
||||
var imageUrls =
|
||||
chapterImageContainers.stream()
|
||||
.map(
|
||||
chapterImageContainerElement -> {
|
||||
var pageNumber = chapterImageContainerElement.id();
|
||||
var imageElement =
|
||||
chapterImageContainerElement.getElementsByTag("img").getFirst();
|
||||
return imageElement.attr("data-lazy-src");
|
||||
})
|
||||
.toList();
|
||||
|
||||
return IntStream.range(0, imageUrls.size())
|
||||
.boxed()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
|
||||
} catch (IOException | NoSuchElementException e) {
|
||||
log.error("Error fetching mangas from MangaLivre", e);
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
private List<ContentProviderMangaInfoResponseDTO> getMangasFromPage(int page) {
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(url + "page/" + page);
|
||||
|
||||
var mangaGrid = document.getElementsByClass("manga-grid").getFirst();
|
||||
var mangaElements = mangaGrid.getElementsByTag("article");
|
||||
|
||||
return mangaElements.stream()
|
||||
.map(
|
||||
element -> {
|
||||
try {
|
||||
var linkElement = element.getElementsByTag("a").getFirst();
|
||||
|
||||
var imageContainer =
|
||||
linkElement.getElementsByClass("manga-card-image").getFirst();
|
||||
var contentContainer =
|
||||
linkElement.getElementsByClass("manga-card-content").getFirst();
|
||||
|
||||
var imageUrl =
|
||||
imageContainer
|
||||
.getElementsByClass("attachment-manga-cover")
|
||||
.getFirst()
|
||||
.attr("data-lazy-src");
|
||||
|
||||
var title = contentContainer.getElementsByTag("h3").text();
|
||||
var url = linkElement.attr("href");
|
||||
var status =
|
||||
switch (imageContainer
|
||||
.getElementsByClass("manga-status")
|
||||
.text()
|
||||
.toLowerCase()) {
|
||||
case "em andamento" -> MangaStatus.ONGOING;
|
||||
case "completo" -> MangaStatus.COMPLETED;
|
||||
case "hiato" -> MangaStatus.HIATUS;
|
||||
default -> MangaStatus.UNKNOWN;
|
||||
};
|
||||
|
||||
return new ContentProviderMangaInfoResponseDTO(title, url, imageUrl, status);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
} catch (IOException | NoSuchElementException e) {
|
||||
log.error("Error fetching mangas from MangaLivre", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private Integer getTotalPages() {
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(url);
|
||||
|
||||
var navLinks = document.getElementsByClass("nav-links").getFirst();
|
||||
var links = navLinks.getElementsByTag("a");
|
||||
|
||||
var pageNumbers =
|
||||
links.stream()
|
||||
.map(Element::text)
|
||||
.filter(NUMERIC_PATTERN.asMatchPredicate())
|
||||
.map(Integer::parseInt)
|
||||
.toList();
|
||||
return pageNumbers.stream().max(Integer::compareTo).orElse(null);
|
||||
} catch (IOException | NoSuchElementException e) {
|
||||
log.error("Error fetching total pages from MangaLivre", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,152 @@
|
||||
package com.magamochi.mangamochi.service.providers.impl;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.ContentProviderMangaChapterResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.ContentProviderMangaInfoResponseDTO;
|
||||
import com.magamochi.mangamochi.model.entity.MangaProvider;
|
||||
import com.magamochi.mangamochi.model.enumeration.MangaStatus;
|
||||
import com.magamochi.mangamochi.service.WebScrapperClientProxyService;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProvider;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProviders;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Log4j2
|
||||
@Service(ContentProviders.MANGA_LIVRE)
|
||||
@RequiredArgsConstructor
|
||||
public class MangaLivreProvider implements ContentProvider {
|
||||
private final String url = "https://mangalivre.tv/manga/";
|
||||
|
||||
private final WebScrapperClientProxyService webScrapperClientProxyService;
|
||||
|
||||
@Override
|
||||
public List<ContentProviderMangaInfoResponseDTO> getAvailableMangas() {
|
||||
var totalPages = getTotalPages();
|
||||
|
||||
if (Objects.isNull(totalPages) || totalPages < 1) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return IntStream.rangeClosed(1, totalPages)
|
||||
.mapToObj(this::getMangasFromPage)
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(List::stream)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ContentProviderMangaChapterResponseDTO> getAvailableChapters(MangaProvider provider) {
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(provider.getUrl());
|
||||
|
||||
var chapterItems = document.getElementsByClass("wp-manga-chapter");
|
||||
|
||||
return chapterItems.stream()
|
||||
.map(
|
||||
chapterItemElement -> {
|
||||
var linkElement = chapterItemElement.getElementsByTag("a").getFirst();
|
||||
|
||||
return new ContentProviderMangaChapterResponseDTO(
|
||||
linkElement.text(), linkElement.attr("href"));
|
||||
})
|
||||
.toList();
|
||||
} catch (NoSuchElementException | IOException e) {
|
||||
log.error("Error parsing mangas from MangaLivre", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Integer, String> getChapterImagesUrls(String chapterUrl) {
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(chapterUrl);
|
||||
|
||||
var chapterImagesContainer = document.getElementsByClass("chapter-images").getFirst();
|
||||
var chapterImagesElements = chapterImagesContainer.getElementsByClass("page-break");
|
||||
|
||||
var imageUrls =
|
||||
chapterImagesElements.stream()
|
||||
.map(
|
||||
chapterImagesElement -> {
|
||||
var imageElement = chapterImagesElement.getElementsByTag("img").getFirst();
|
||||
return imageElement.attr("src");
|
||||
})
|
||||
.toList();
|
||||
|
||||
return IntStream.range(0, imageUrls.size())
|
||||
.boxed()
|
||||
.collect(
|
||||
Collectors.toMap(
|
||||
i -> i, imageUrls::get, (existing, replacement) -> existing, LinkedHashMap::new));
|
||||
} catch (NoSuchElementException | IOException e) {
|
||||
log.error("Error parsing mangas from MangaLivre", e);
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
private List<ContentProviderMangaInfoResponseDTO> getMangasFromPage(int page) {
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(url + "page/" + page);
|
||||
|
||||
var mangaElements = document.getElementsByClass("manga__item");
|
||||
|
||||
return mangaElements.stream()
|
||||
.map(
|
||||
element -> {
|
||||
var mangaTitleElement =
|
||||
element
|
||||
.getElementsByClass("manga__content")
|
||||
.getFirst()
|
||||
.getElementsByClass("manga__content_item")
|
||||
.getFirst()
|
||||
.getElementsByClass("post-title font-title")
|
||||
.getFirst()
|
||||
.getElementsByTag("h2")
|
||||
.getFirst();
|
||||
|
||||
var linkElement = mangaTitleElement.getElementsByTag("a").getFirst();
|
||||
var url = linkElement.attr("href");
|
||||
var title = linkElement.text().trim();
|
||||
|
||||
var imageElement =
|
||||
element
|
||||
.getElementsByClass("manga__thumb")
|
||||
.getFirst()
|
||||
.getElementsByClass("manga__thumb_item")
|
||||
.getFirst()
|
||||
.getElementsByTag("a")
|
||||
.getFirst()
|
||||
.getElementsByTag("img")
|
||||
.getFirst();
|
||||
var imgUrl = imageElement.attr("src");
|
||||
|
||||
return new ContentProviderMangaInfoResponseDTO(
|
||||
title, url, imgUrl, MangaStatus.UNKNOWN);
|
||||
})
|
||||
.toList();
|
||||
} catch (NoSuchElementException | IOException e) {
|
||||
log.error("Error parsing mangas from MangaLivre", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private Integer getTotalPages() {
|
||||
try {
|
||||
var document = webScrapperClientProxyService.scrapeToJsoupDocument(url);
|
||||
|
||||
var navLinks = document.getElementsByClass("wp-pagenavi").getFirst();
|
||||
var lastPageElement = navLinks.getElementsByClass("last").getFirst();
|
||||
var links = lastPageElement.attr("href");
|
||||
|
||||
var totalPages = links.replaceAll("\\D+", "");
|
||||
return Integer.parseInt(totalPages);
|
||||
} catch (NoSuchElementException | IOException e) {
|
||||
log.error("Error parsing total pages from MangaLivre", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
package com.magamochi.mangamochi.task;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.magamochi.mangamochi.client.JikanClient;
|
||||
import com.magamochi.mangamochi.model.entity.Author;
|
||||
import com.magamochi.mangamochi.model.entity.Genre;
|
||||
import com.magamochi.mangamochi.model.entity.MangaAuthor;
|
||||
import com.magamochi.mangamochi.model.entity.MangaGenre;
|
||||
import com.magamochi.mangamochi.model.repository.*;
|
||||
import com.magamochi.mangamochi.service.ImageService;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Log4j2
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UpdateMangaDataTask {
|
||||
private final AuthorRepository authorRepository;
|
||||
private final MangaAuthorRepository mangaAuthorRepository;
|
||||
private final MangaRepository mangaRepository;
|
||||
|
||||
private final JikanClient jikanClient;
|
||||
|
||||
private final ImageService imageService;
|
||||
private final GenreRepository genreRepository;
|
||||
private final MangaGenreRepository mangaGenreRepository;
|
||||
|
||||
// @Scheduled(fixedDelayString = "1d")
|
||||
public void updateMangaData() {
|
||||
var rateLimiter = RateLimiter.create(1);
|
||||
|
||||
var mangas =
|
||||
mangaRepository.findAll().stream().filter(manga -> isNull(manga.getCoverImage())).toList();
|
||||
|
||||
mangas.forEach(
|
||||
manga -> {
|
||||
log.info("Updating manga {}", manga.getTitle());
|
||||
|
||||
try {
|
||||
rateLimiter.acquire();
|
||||
var mangaData = jikanClient.getMangaById(manga.getMalId());
|
||||
|
||||
manga.setAlternativeTitles(mangaData.data().title_synonyms());
|
||||
manga.setSynopsis(mangaData.data().synopsis());
|
||||
manga.setStatus(mangaData.data().status());
|
||||
// manga.setScore(mangaData.data().score());
|
||||
manga.setPublishedFrom(mangaData.data().published().from());
|
||||
manga.setPublishedTo(mangaData.data().published().to());
|
||||
|
||||
var authors =
|
||||
mangaData.data().authors().stream()
|
||||
.map(
|
||||
authorData -> {
|
||||
return authorRepository
|
||||
.findByMalId(authorData.mal_id())
|
||||
.orElseGet(
|
||||
() ->
|
||||
authorRepository.save(
|
||||
Author.builder()
|
||||
.malId(authorData.mal_id())
|
||||
.name(authorData.name())
|
||||
.build()));
|
||||
})
|
||||
.toList();
|
||||
|
||||
var mangaAuthors =
|
||||
authors.stream()
|
||||
.map(
|
||||
author -> {
|
||||
return mangaAuthorRepository
|
||||
.findByMangaAndAuthor(manga, author)
|
||||
.orElseGet(
|
||||
() ->
|
||||
mangaAuthorRepository.save(
|
||||
MangaAuthor.builder()
|
||||
.manga(manga)
|
||||
.author(author)
|
||||
.build()));
|
||||
})
|
||||
.toList();
|
||||
|
||||
manga.setMangaAuthors(mangaAuthors);
|
||||
|
||||
var genres =
|
||||
mangaData.data().genres().stream()
|
||||
.map(
|
||||
genreData -> {
|
||||
return genreRepository
|
||||
.findByMalId(genreData.mal_id())
|
||||
.orElseGet(
|
||||
() ->
|
||||
genreRepository.save(
|
||||
Genre.builder()
|
||||
.malId(genreData.mal_id())
|
||||
.name(genreData.name())
|
||||
.build()));
|
||||
})
|
||||
.toList();
|
||||
|
||||
var mangaGenres =
|
||||
genres.stream()
|
||||
.map(
|
||||
genre -> {
|
||||
return mangaGenreRepository
|
||||
.findByMangaAndGenre(manga, genre)
|
||||
.orElseGet(
|
||||
() ->
|
||||
mangaGenreRepository.save(
|
||||
MangaGenre.builder().manga(manga).genre(genre).build()));
|
||||
})
|
||||
.toList();
|
||||
|
||||
manga.setMangaGenres(mangaGenres);
|
||||
|
||||
var inputStream =
|
||||
new BufferedInputStream(
|
||||
new URL(
|
||||
new URI(mangaData.data().images().jpg().large_image_url())
|
||||
.toASCIIString())
|
||||
.openStream());
|
||||
|
||||
var bytes = inputStream.readAllBytes();
|
||||
|
||||
inputStream.close();
|
||||
var image = imageService.uploadImage(bytes, "image/jpeg", "cover");
|
||||
|
||||
manga.setCoverImage(image);
|
||||
|
||||
mangaRepository.save(manga);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Error updating manga data for manga {}. {}", manga.getTitle(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.magamochi.mangamochi.task;
|
||||
|
||||
import com.magamochi.mangamochi.service.MangaListService;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProvider;
|
||||
import com.magamochi.mangamochi.service.providers.ContentProviderFactory;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Log4j2
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UpdateMangaListTask {
|
||||
private final ContentProviderFactory contentProviderFactory;
|
||||
private final MangaListService mangaListService;
|
||||
|
||||
// @Scheduled(fixedDelayString = "1d")
|
||||
public void updateMangaList() {
|
||||
log.info("Updating manga list...");
|
||||
|
||||
var contentProviders = contentProviderFactory.getContentProviders();
|
||||
contentProviders.forEach(this::updateProviderMangaList);
|
||||
|
||||
log.info("Manga list updated.");
|
||||
}
|
||||
|
||||
private void updateProviderMangaList(
|
||||
String contentProviderName, ContentProvider contentProvider) {
|
||||
log.info("Updating manga list for content provider {}", contentProviderName);
|
||||
mangaListService.updateMangaList(contentProviderName, contentProvider.getAvailableMangas());
|
||||
log.info("Manga list for content provider {} updated.", contentProviderName);
|
||||
}
|
||||
}
|
||||
68
src/main/java/com/magamochi/mangamochi/util/JwtUtil.java
Normal file
68
src/main/java/com/magamochi/mangamochi/util/JwtUtil.java
Normal file
@ -0,0 +1,68 @@
|
||||
package com.magamochi.mangamochi.util;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import java.security.Key;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private Long expiration;
|
||||
|
||||
private Key getSigningKey() {
|
||||
return Keys.hmacShaKeyFor(secret.getBytes());
|
||||
}
|
||||
|
||||
public String extractUsername(String token) {
|
||||
return extractClaim(token, Claims::getSubject);
|
||||
}
|
||||
|
||||
public Date extractExpiration(String token) {
|
||||
return extractClaim(token, Claims::getExpiration);
|
||||
}
|
||||
|
||||
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
||||
final Claims claims = extractAllClaims(token);
|
||||
return claimsResolver.apply(claims);
|
||||
}
|
||||
|
||||
private Claims extractAllClaims(String token) {
|
||||
return Jwts.parser().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();
|
||||
}
|
||||
|
||||
private Boolean isTokenExpired(String token) {
|
||||
return extractExpiration(token).before(new Date());
|
||||
}
|
||||
|
||||
public String generateToken(UserDetails userDetails) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
return createToken(claims, userDetails.getUsername());
|
||||
}
|
||||
|
||||
private String createToken(Map<String, Object> claims, String subject) {
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(new Date(System.currentTimeMillis()))
|
||||
.setExpiration(new Date(System.currentTimeMillis() + expiration))
|
||||
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Boolean validateToken(String token, UserDetails userDetails) {
|
||||
final String username = extractUsername(token);
|
||||
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
|
||||
}
|
||||
}
|
||||
3
src/main/resources/application-local.yml
Normal file
3
src/main/resources/application-local.yml
Normal file
@ -0,0 +1,3 @@
|
||||
spring:
|
||||
config:
|
||||
import: optional:file:.env[.properties]
|
||||
34
src/main/resources/application.yml
Normal file
34
src/main/resources/application.yml
Normal file
@ -0,0 +1,34 @@
|
||||
spring:
|
||||
application:
|
||||
name: mangamochi
|
||||
datasource:
|
||||
url: ${DB_URL}
|
||||
username: ${DB_USER}
|
||||
password: ${DB_PASS}
|
||||
jpa:
|
||||
properties:
|
||||
hibernate:
|
||||
default_schema: mangamochi
|
||||
flyway:
|
||||
enabled: true
|
||||
schemas:
|
||||
- mangamochi
|
||||
default-schema: mangamochi
|
||||
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
|
||||
web-scrapper:
|
||||
endpoint: ${WEBSCRAPPER_ENDPOINT}
|
||||
|
||||
minio:
|
||||
endpoint: ${MINIO_ENDPOINT}
|
||||
accessKey: ${MINIO_USER}
|
||||
secretKey: ${MINIO_PASS}
|
||||
bucket: mangamochi
|
||||
|
||||
# JWT Configuration
|
||||
jwt:
|
||||
secret: mySecretKeymySecretKeymySecretKeymySecretKeymySecretKeymySecretKey
|
||||
expiration: 86400000 # 24 hours in milliseconds
|
||||
7
src/main/resources/db/migration/V0001__IMAGES_TABLE.sql
Normal file
7
src/main/resources/db/migration/V0001__IMAGES_TABLE.sql
Normal file
@ -0,0 +1,7 @@
|
||||
CREATE TABLE images
|
||||
(
|
||||
id UUID NOT NULL PRIMARY KEY,
|
||||
file_key VARCHAR NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
11
src/main/resources/db/migration/V0002__MANGA.sql
Normal file
11
src/main/resources/db/migration/V0002__MANGA.sql
Normal file
@ -0,0 +1,11 @@
|
||||
CREATE TABLE mangas
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
mal_id BIGINT UNIQUE,
|
||||
title VARCHAR,
|
||||
alternative_titles TEXT[],
|
||||
status VARCHAR,
|
||||
synopsis VARCHAR,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
21
src/main/resources/db/migration/V0003__PROVIDER.sql
Normal file
21
src/main/resources/db/migration/V0003__PROVIDER.sql
Normal file
@ -0,0 +1,21 @@
|
||||
CREATE TABLE providers
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
name VARCHAR NOT NULL,
|
||||
status VARCHAR NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE manga_provider
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
manga_id BIGINT NOT NULL REFERENCES mangas (id) ON DELETE CASCADE,
|
||||
provider_id BIGINT NOT NULL REFERENCES providers (id) ON DELETE CASCADE,
|
||||
manga_title VARCHAR NOT NULL,
|
||||
url VARCHAR NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (manga_id, provider_id)
|
||||
)
|
||||
|
||||
20
src/main/resources/db/migration/V0004__CHAPTER.sql
Normal file
20
src/main/resources/db/migration/V0004__CHAPTER.sql
Normal file
@ -0,0 +1,20 @@
|
||||
CREATE TABLE manga_chapters
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
manga_provider_id BIGINT NOT NULL REFERENCES manga_provider (id) ON DELETE CASCADE,
|
||||
title VARCHAR NOT NULL,
|
||||
url VARCHAR NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE manga_chapter_images
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
manga_chapter_id BIGINT NOT NULL REFERENCES manga_chapters (id) ON DELETE CASCADE,
|
||||
image_id UUID REFERENCES images (id) ON DELETE CASCADE,
|
||||
position INT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
CREATE TABLE manga_import_reviews
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
provider_id BIGINT NOT NULL REFERENCES providers (id) ON DELETE CASCADE,
|
||||
title VARCHAR NOT NULL,
|
||||
url VARCHAR NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
5
src/main/resources/db/migration/V0006__MANGA_DATA.sql
Normal file
5
src/main/resources/db/migration/V0006__MANGA_DATA.sql
Normal file
@ -0,0 +1,5 @@
|
||||
ALTER TABLE mangas
|
||||
ADD COLUMN cover_image_id UUID REFERENCES images (id),
|
||||
ADD COLUMN score DOUBLE PRECISION,
|
||||
ADD COLUMN published_from TIMESTAMPTZ,
|
||||
ADD COLUMN published_to TIMESTAMPTZ;
|
||||
31
src/main/resources/db/migration/V0007__MANGA_DATA.sql
Normal file
31
src/main/resources/db/migration/V0007__MANGA_DATA.sql
Normal file
@ -0,0 +1,31 @@
|
||||
CREATE TABLE authors
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
mal_id BIGINT UNIQUE,
|
||||
name VARCHAR,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE manga_author
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
manga_id BIGINT REFERENCES mangas (id),
|
||||
author_id BIGINT REFERENCES authors (id),
|
||||
UNIQUE (manga_id, author_id)
|
||||
);
|
||||
|
||||
CREATE TABLE genres
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
mal_id BIGINT UNIQUE,
|
||||
name VARCHAR
|
||||
);
|
||||
|
||||
CREATE TABLE manga_genre
|
||||
(
|
||||
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
manga_id BIGINT REFERENCES mangas (id),
|
||||
genre_id BIGINT REFERENCES genres (id),
|
||||
UNIQUE (manga_id, genre_id)
|
||||
);
|
||||
@ -0,0 +1 @@
|
||||
ALTER TABLE manga_chapters ADD COLUMN downloaded BOOLEAN DEFAULT FALSE;
|
||||
@ -0,0 +1 @@
|
||||
ALTER TABLE manga_chapters ADD COLUMN read BOOLEAN DEFAULT FALSE;
|
||||
6
src/main/resources/db/migration/V0010__USERS.sql
Normal file
6
src/main/resources/db/migration/V0010__USERS.sql
Normal file
@ -0,0 +1,6 @@
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR NOT NULL UNIQUE,
|
||||
password VARCHAR NOT NULL,
|
||||
role VARCHAR
|
||||
);
|
||||
@ -0,0 +1,22 @@
|
||||
package com.magamochi.mangamochi;
|
||||
|
||||
import com.magamochi.mangamochi.client.JikanClient;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
@Log4j2
|
||||
@SpringBootTest
|
||||
@RequiredArgsConstructor
|
||||
@EnableFeignClients
|
||||
class MangamochiApplicationTests {
|
||||
private final JikanClient jikanClient;
|
||||
|
||||
@Test
|
||||
void testJikan() {
|
||||
var response = jikanClient.mangaSearch("Saint Seiya");
|
||||
log.info(response.toString());
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user