├── LICENSE
├── README.md
├── build.gradle
├── gradlew
├── gradlew.bat
├── libs
├── DjVu2Image-1.0.jar
└── fb2parser.jar
├── settings.gradle
└── src
└── main
├── java
└── xyz
│ └── atsumeru
│ └── web
│ ├── AtsumeruApplication.java
│ ├── Beans.java
│ ├── archive
│ ├── ArchiveReader.java
│ ├── CBZPacker.java
│ └── iterator
│ │ ├── IArchiveIterator.java
│ │ ├── SevenZipIterator.java
│ │ └── ZipIterator.java
│ ├── component
│ └── Localization.java
│ ├── configuration
│ ├── CustomLocaleResolver.java
│ ├── EnumConverterConfiguration.java
│ ├── FileWatcherConfiguration.java
│ ├── GsonConfiguration.java
│ ├── MultiPartConfiguration.java
│ ├── OpenApiConfiguration.java
│ ├── RestApiConfiguration.java
│ ├── ScheduleConfiguration.java
│ ├── ServerConfiguration.java
│ └── WebConfiguration.java
│ ├── controller
│ ├── mvc
│ │ └── MainController.java
│ └── rest
│ │ ├── ServerApiController.java
│ │ ├── book
│ │ ├── BooksApiController.java
│ │ └── FilteredByBoundServiceApiController.java
│ │ ├── category
│ │ ├── CategoriesApiController.java
│ │ └── MetacategoriesApiController.java
│ │ ├── file
│ │ └── FilesApiController.java
│ │ ├── filesystem
│ │ └── FileSystemApiController.java
│ │ ├── history
│ │ └── HistoryApiController.java
│ │ ├── hub
│ │ └── HubApiController.java
│ │ ├── importer
│ │ └── ImporterApiController.java
│ │ ├── metadata
│ │ └── MetadataApiController.java
│ │ ├── service
│ │ └── ServicesApiController.java
│ │ ├── settings
│ │ └── SettingsApiController.java
│ │ ├── sync
│ │ └── SyncApiController.java
│ │ ├── uploader
│ │ └── UploaderApiController.java
│ │ └── user
│ │ └── UsersApiController.java
│ ├── converter
│ ├── StringToNonNullEnumConverter.java
│ └── StringToNullableEnumConverter.java
│ ├── enums
│ ├── AgeRating.java
│ ├── BookType.java
│ ├── Censorship.java
│ ├── Color.java
│ ├── ContentType.java
│ ├── Genre.java
│ ├── LibraryPresentation.java
│ ├── LogicalMode.java
│ ├── PlotType.java
│ ├── ServiceType.java
│ ├── Sort.java
│ ├── Status.java
│ └── TranslationStatus.java
│ ├── exception
│ ├── ArchiveReadingException.java
│ ├── AtsumeruExceptionHandler.java
│ ├── ChapterNotFoundException.java
│ ├── DjVuReadingException.java
│ ├── DownloadsNotAllowedException.java
│ ├── ImportActiveException.java
│ ├── MediaUnsupportedException.java
│ ├── MetadataUpdateActiveException.java
│ ├── NoCoverFoundException.java
│ ├── NoReadableFoundException.java
│ ├── NotAcceptableForOnlineReadingException.java
│ ├── PDFReadingException.java
│ ├── PageNotFoundException.java
│ ├── RendererNotImplementedException.java
│ └── UserNotFoundException.java
│ ├── filter
│ └── StatsFilter.java
│ ├── helper
│ ├── ChapterRecognition.java
│ ├── Constants.java
│ ├── ExternalIpChecker.java
│ ├── FilesHelper.java
│ ├── HashHelper.java
│ ├── ImageHelper.java
│ ├── JSONHelper.java
│ ├── JSONLogHelper.java
│ ├── JavaHelper.java
│ ├── OrmLiteUpgradeTable.java
│ ├── PasswordGenerator.java
│ ├── RestHelper.java
│ ├── ServerHelper.java
│ └── ValuesMapper.java
│ ├── importer
│ ├── Importer.java
│ └── listener
│ │ └── OnImportCallback.java
│ ├── interceptor
│ ├── InterceptorRegistry.java
│ └── RequestLogInterceptor.java
│ ├── io
│ └── image
│ │ ├── ImageOutStreamWriter.java
│ │ ├── ImageOutStreamWriterFactory.java
│ │ └── impl
│ │ ├── ArchivedImageOutStreamWriter.java
│ │ └── RenderedImageOutStreamWriter.java
│ ├── json
│ ├── adapter
│ │ ├── AdminFieldAdapter.java
│ │ ├── CategoriesFieldAdapter.java
│ │ ├── LinksBidirectionalAdapter.java
│ │ ├── OmitEmptyStringsAdapter.java
│ │ └── StringListBidirectionalAdapter.java
│ └── annotation
│ │ └── Exclude.java
│ ├── logger
│ └── FileLogger.java
│ ├── manager
│ ├── ImageCache.java
│ ├── Settings.java
│ ├── Workspace.java
│ ├── cache
│ │ ├── AtsumeruCache.java
│ │ └── AtsumeruRenderersCache.java
│ └── fswatcher
│ │ ├── ChangedFile.java
│ │ ├── ChangedFiles.java
│ │ ├── FileChangeListener.java
│ │ ├── FileSnapshot.java
│ │ ├── FileSystemWatcher.java
│ │ └── FolderSnapshot.java
│ ├── metadata
│ ├── BookInfo.java
│ ├── ComicInfo.java
│ ├── DjVuInfo.java
│ ├── EpubOPF.java
│ ├── FictionBookInfo.java
│ └── PDFInfo.java
│ ├── model
│ ├── AtsumeruMessage.java
│ ├── GenreModel.java
│ ├── ServerInfo.java
│ ├── UserAccessConstants.java
│ ├── book
│ │ ├── BaseBook.java
│ │ ├── BookArchive.java
│ │ ├── BookSerie.java
│ │ ├── DownloadedLinks.java
│ │ ├── IBaseBookItem.java
│ │ ├── chapter
│ │ │ └── BookChapter.java
│ │ ├── franchise
│ │ │ └── Franchise.java
│ │ ├── image
│ │ │ └── Images.java
│ │ ├── service
│ │ │ └── BoundService.java
│ │ └── volume
│ │ │ └── VolumeItem.java
│ ├── category
│ │ └── Metacategory.java
│ ├── covers
│ │ └── CoversCachingStatus.java
│ ├── database
│ │ ├── AtsumeruUser.java
│ │ ├── Category.java
│ │ ├── DatabaseFields.java
│ │ ├── DatabaseVersion.java
│ │ └── History.java
│ ├── filter
│ │ └── Filters.java
│ ├── importer
│ │ ├── ImportFolder.java
│ │ ├── ImportStatus.java
│ │ └── ReadableContent.java
│ ├── metadata
│ │ └── MetadataUpdateStatus.java
│ ├── service
│ │ └── ServicesStatus.java
│ └── settings
│ │ └── ServerSettings.java
│ ├── properties
│ └── ImportFolders.java
│ ├── renderer
│ ├── AbstractRenderer.java
│ ├── RendererFactory.java
│ └── impl
│ │ ├── DjVuRenderer.java
│ │ └── PDFRenderer.java
│ ├── repository
│ ├── BooksRepository.java
│ ├── CategoryRepository.java
│ ├── FilteredBooksRepository.java
│ ├── HistoryRepository.java
│ ├── MetacategoryRepository.java
│ └── dao
│ │ ├── BaseDaoManager.java
│ │ ├── BooksDaoManager.java
│ │ └── UsersDaoManager.java
│ ├── security
│ ├── configuration
│ │ ├── NoSecurityConfiguration.java
│ │ └── WebSecurityConfiguration.java
│ ├── repository
│ │ └── UsersRepository.java
│ └── service
│ │ └── UsersDetailsService.java
│ ├── service
│ ├── CoversSaverService.java
│ ├── ImportService.java
│ └── MetadataUpdateService.java
│ └── util
│ ├── AppUtils.java
│ ├── ArrayUtils.java
│ ├── ContentDetector.java
│ ├── EnumUtils.java
│ ├── FileUtils.java
│ ├── LinkUtils.java
│ ├── StreamUtils.java
│ ├── StringUtils.java
│ ├── TypeUtils.java
│ └── comparator
│ ├── AlphanumComparator.java
│ └── NaturalStringComparator.java
└── resources
├── application.properties
├── banner.txt
├── changelog.txt
├── messages.properties
└── messages_ru.properties
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Atsumeru.xyz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [](https://github.com/AtsumeruDev/Atsumeru/releases) [](https://github.com/AtsumeruDev/Atsumeru/releases)
6 | [](https://hub.docker.com/r/atsumerudev/atsumeru)
7 |
8 | # Atsumeru
9 |
10 | Free self-hosted mangas/comics/light novels media server
11 |
12 | # Main features
13 |
14 | - Import and read your manga/comics/light novels with native clients for `Windows`/`Linux`/`Mac`/`Android`
15 | - Organize your library using `Autocategories`, `Custom categories` and `Metacategories`
16 | - Edit your metadata in easy way with ability to parse it from supported catalogs
17 | - Auto import metadata from `ComicInfo.xml` and `book_info.json` formats
18 | - Create multiple users with separate history and powerful access-controls
19 | - Download whole `Series` in supported apps
20 | - Easy and convenient `REST API`
21 |
22 | # Download and setup `.jar` file
23 |
24 | Download actual version from [Releases](https://github.com/AtsumeruDev/Atsumeru/releases) section. Setup it by using [official guide](https://atsumeru.xyz/installation/jar.html)
25 |
26 | # Docker setup
27 |
28 | Fast setup your own server with [Docker image](https://atsumeru.xyz/installation/docker.html)
29 |
30 | # Documentation
31 |
32 | Swagger-UI is available at [http://localhost:31337/swagger-ui/index.html](http://localhost:31337/swagger-ui/index.html)
33 |
34 | # Wiki
35 |
36 | Wiki is available on [Atsumeru](https://atsumeru.xyz/) website
37 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'java'
3 | id 'idea'
4 |
5 | // Spring
6 | id 'org.springframework.boot' version '3.3.5'
7 | id 'io.spring.dependency-management' version '1.1.7'
8 |
9 | // GraalVM
10 | id 'org.graalvm.buildtools.native' version '0.9.28'
11 | }
12 |
13 | group 'xyz.atsumeru.web'
14 | version '1.2'
15 |
16 | compileJava.options.encoding = 'UTF-8'
17 | System.setProperty('file.encoding', 'UTF-8')
18 |
19 | repositories {
20 | mavenCentral()
21 | maven { url 'https://jitpack.io' }
22 | }
23 |
24 | configurations {
25 | all*.exclude module : 'spring-boot-starter-json'
26 |
27 | developmentOnly
28 | runtimeClasspath {
29 | extendsFrom developmentOnly
30 | }
31 | }
32 |
33 | bootJar {
34 | manifest {
35 | attributes(
36 | 'Implementation-Title': 'Atsumeru',
37 | 'Implementation-Version': archiveVersion
38 | )
39 | }
40 | }
41 |
42 | dependencies {
43 | implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.50"
44 |
45 | // AnnotationsProcessors
46 | annotationProcessor 'org.projectlombok:lombok'
47 | compileOnly 'org.projectlombok:lombok'
48 |
49 | // Spring Boot
50 | implementation 'org.springframework.boot:spring-boot-starter-web'
51 |
52 | // Spring Security
53 | implementation 'org.springframework.boot:spring-boot-starter-security'
54 | implementation 'org.springframework.security:spring-security-web'
55 |
56 | // OpenAPI & SwaggerUI
57 | implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
58 |
59 | // ORM
60 | implementation "com.j256.ormlite:ormlite-core:5.1"
61 | implementation "com.j256.ormlite:ormlite-jdbc:5.1"
62 | implementation 'org.xerial:sqlite-jdbc:3.41.2.2'
63 |
64 | // CLI ProgressBar
65 | implementation 'me.tongfei:progressbar:0.10.1'
66 |
67 | // ImageIO extensions (Extended JPEG and WebP support)
68 | implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.9.3'
69 | implementation 'com.twelvemonkeys.imageio:imageio-webp:3.9.3'
70 |
71 | // Image thumbnails creation
72 | implementation 'net.coobird:thumbnailator:0.4.14'
73 |
74 | // Image palette generation
75 | implementation 'com.github.trickl:palette:0.1.1'
76 |
77 | // Apache Commons
78 | implementation 'commons-io:commons-io:2.14.0'
79 | implementation 'org.apache.commons:commons-compress:1.26.0'
80 |
81 | // Comparators
82 | implementation "net.grey-panther:natural-comparator:1.1"
83 |
84 | // Caching
85 | implementation "com.github.ben-manes.caffeine:caffeine:2.9.0"
86 |
87 | // JSON
88 | implementation 'com.google.code.gson:gson:2.8.9'
89 | implementation 'org.json:json:20231013'
90 | implementation "org.apache.clerezza.ext:org.json.simple:0.4"
91 |
92 | // File formats support
93 | // Content type detection (Apache Tika)
94 | implementation 'org.apache.tika:tika-core:2.4.1'
95 |
96 | // Extended archives support (7Zip Bindings)
97 | implementation "net.sf.sevenzipjbinding:sevenzipjbinding:16.02-2.01"
98 | implementation "net.sf.sevenzipjbinding:sevenzipjbinding-all-platforms:16.02-2.01"
99 |
100 | // ePub (JSOUP parser)
101 | implementation "org.jsoup:jsoup:1.13.1"
102 |
103 | // FB2 (FB2 Parser)
104 | implementation files("libs/fb2parser.jar")
105 |
106 | // DjVu (DjVu2Image converter)
107 | implementation files("libs/DjVu2Image-1.0.jar")
108 |
109 | // PDF (PDFBox)
110 | implementation 'org.apache.pdfbox:pdfbox:2.0.25'
111 | }
112 |
113 | configurations {
114 | compileOnly {
115 | extendsFrom annotationProcessor
116 | }
117 | all {
118 | exclude group: 'commons-logging', module: 'commons-logging'
119 | }
120 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
34 |
35 | @rem Find java.exe
36 | if defined JAVA_HOME goto findJavaFromJavaHome
37 |
38 | set JAVA_EXE=java.exe
39 | %JAVA_EXE% -version >NUL 2>&1
40 | if "%ERRORLEVEL%" == "0" goto init
41 |
42 | echo.
43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
44 | echo.
45 | echo Please set the JAVA_HOME variable in your environment to match the
46 | echo location of your Java installation.
47 |
48 | goto fail
49 |
50 | :findJavaFromJavaHome
51 | set JAVA_HOME=%JAVA_HOME:"=%
52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
53 |
54 | if exist "%JAVA_EXE%" goto init
55 |
56 | echo.
57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
58 | echo.
59 | echo Please set the JAVA_HOME variable in your environment to match the
60 | echo location of your Java installation.
61 |
62 | goto fail
63 |
64 | :init
65 | @rem Get command-line arguments, handling Windows variants
66 |
67 | if not "%OS%" == "Windows_NT" goto win9xME_args
68 |
69 | :win9xME_args
70 | @rem Slurp the command line arguments.
71 | set CMD_LINE_ARGS=
72 | set _SKIP=2
73 |
74 | :win9xME_args_slurp
75 | if "x%~1" == "x" goto execute
76 |
77 | set CMD_LINE_ARGS=%*
78 |
79 | :execute
80 | @rem Setup the command line
81 |
82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
83 |
84 | @rem Execute Gradle
85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
86 |
87 | :end
88 | @rem End local scope for the variables with windows NT shell
89 | if "%ERRORLEVEL%"=="0" goto mainEnd
90 |
91 | :fail
92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
93 | rem the _cmd.exe /c_ return code!
94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
95 | exit /b 1
96 |
97 | :mainEnd
98 | if "%OS%"=="Windows_NT" endlocal
99 |
100 | :omega
101 |
--------------------------------------------------------------------------------
/libs/DjVu2Image-1.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Atsumeru-xyz/Atsumeru/323c87d89c2f512acbdecb3d07c4b02e31bf3a04/libs/DjVu2Image-1.0.jar
--------------------------------------------------------------------------------
/libs/fb2parser.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Atsumeru-xyz/Atsumeru/323c87d89c2f512acbdecb3d07c4b02e31bf3a04/libs/fb2parser.jar
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'Atsumeru'
2 |
3 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/AtsumeruApplication.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web;
2 |
3 | import lombok.Getter;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.boot.ApplicationArguments;
7 | import org.springframework.boot.ApplicationRunner;
8 | import org.springframework.boot.SpringApplication;
9 | import org.springframework.boot.autoconfigure.SpringBootApplication;
10 | import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
11 | import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
12 | import org.springframework.boot.context.event.ApplicationReadyEvent;
13 | import org.springframework.context.ConfigurableApplicationContext;
14 | import org.springframework.context.event.EventListener;
15 | import org.springframework.core.env.Environment;
16 | import xyz.atsumeru.web.util.StringUtils;
17 |
18 | import java.util.Arrays;
19 |
20 | @SpringBootApplication(exclude = { SecurityAutoConfiguration.class, JacksonAutoConfiguration.class })
21 | public class AtsumeruApplication implements ApplicationRunner {
22 | private static final Logger logger = LoggerFactory.getLogger(AtsumeruApplication.class.getSimpleName());
23 |
24 | @Getter
25 | private static ConfigurableApplicationContext context;
26 |
27 | @Getter
28 | private static boolean isInDevMode;
29 |
30 | public static void main(String[] args) {
31 | context = SpringApplication.run(AtsumeruApplication.class, args);
32 | }
33 |
34 | public AtsumeruApplication(Environment environment) {
35 | isInDevMode = Arrays.stream(environment.getActiveProfiles())
36 | .anyMatch(profile -> StringUtils.equalsIgnoreCase(profile, "dev"));
37 | }
38 |
39 | @EventListener(ApplicationReadyEvent.class)
40 | public void doAfterStart() {
41 | // Do nothing
42 | }
43 |
44 | @Override
45 | public void run(ApplicationArguments args) {
46 | logger.info("Application started with command-line arguments: {}", Arrays.toString(args.getSourceArgs()));
47 | }
48 |
49 | public static void restart() {
50 | ApplicationArguments args = context.getBean(ApplicationArguments.class);
51 |
52 | Thread thread = new Thread(() -> {
53 | context.close();
54 | context = SpringApplication.run(AtsumeruApplication.class, args.getSourceArgs());
55 | });
56 |
57 | thread.setDaemon(false);
58 | thread.start();
59 | }
60 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/Beans.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web;
2 |
3 | import com.google.gson.ExclusionStrategy;
4 | import com.google.gson.FieldAttributes;
5 | import com.google.gson.Gson;
6 | import lombok.Getter;
7 | import org.springframework.context.annotation.Bean;
8 | import org.springframework.context.annotation.Profile;
9 | import org.springframework.stereotype.Component;
10 | import xyz.atsumeru.web.json.annotation.Exclude;
11 | import xyz.atsumeru.web.repository.dao.BooksDaoManager;
12 |
13 | @Component
14 | public class Beans {
15 | @Getter
16 | private static BooksDaoManager booksDaoManager;
17 |
18 | public Beans(BooksDaoManager booksDaoManager) {
19 | Beans.booksDaoManager = booksDaoManager;
20 | }
21 |
22 | // Custom beans
23 | @Profile("!dev")
24 | @Bean
25 | public Gson gson(ExclusionStrategy gsonExclusionStrategy) {
26 | return new Gson().newBuilder()
27 | .setExclusionStrategies(gsonExclusionStrategy)
28 | .create();
29 | }
30 |
31 | @Profile("dev")
32 | @Bean
33 | public Gson gsonPretty(ExclusionStrategy gsonExclusionStrategy) {
34 | return new Gson().newBuilder()
35 | .setExclusionStrategies(gsonExclusionStrategy)
36 | .setPrettyPrinting()
37 | .create();
38 | }
39 |
40 | @Bean
41 | public ExclusionStrategy gsonExclusionStrategy() {
42 | return new ExclusionStrategy() {
43 | @Override
44 | public boolean shouldSkipClass(Class> clazz) {
45 | return false;
46 | }
47 |
48 | @Override
49 | public boolean shouldSkipField(FieldAttributes field) {
50 | return field.getAnnotation(Exclude.class) != null;
51 | }
52 | };
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/archive/ArchiveReader.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.archive;
2 |
3 | import xyz.atsumeru.web.archive.iterator.IArchiveIterator;
4 | import xyz.atsumeru.web.archive.iterator.SevenZipIterator;
5 | import xyz.atsumeru.web.archive.iterator.ZipIterator;
6 | import xyz.atsumeru.web.exception.MediaUnsupportedException;
7 | import xyz.atsumeru.web.util.ContentDetector;
8 |
9 | import java.io.IOException;
10 | import java.nio.file.Paths;
11 | import java.util.HashMap;
12 | import java.util.Map;
13 |
14 | public class ArchiveReader {
15 | static Map archiveIteratorMap = new HashMap<>();
16 |
17 | static {
18 | addArchiveIterator(ZipIterator.create());
19 | addArchiveIterator(SevenZipIterator.create());
20 | }
21 |
22 | private static void addArchiveIterator(IArchiveIterator archiveIterator) {
23 | archiveIterator.getMediaTypes().forEach(it -> archiveIteratorMap.putIfAbsent(it, archiveIterator));
24 | }
25 |
26 | public static IArchiveIterator getArchiveIterator(String path) throws IOException {
27 | String mediaType = ContentDetector.detectMediaType(Paths.get(path));
28 | if (archiveIteratorMap.containsKey(mediaType)) {
29 | IArchiveIterator archiveIterator = archiveIteratorMap.get(mediaType).createInstance();
30 | archiveIterator.open(path);
31 | return archiveIterator;
32 | }
33 |
34 | throw new MediaUnsupportedException("Unsupported archive format: " + mediaType);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/archive/iterator/IArchiveIterator.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.archive.iterator;
2 |
3 | import java.io.Closeable;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.util.List;
7 | import java.util.Map;
8 |
9 | public interface IArchiveIterator extends Closeable {
10 | List getMediaTypes();
11 | IArchiveIterator createInstance();
12 | void open(String archivePath) throws IOException;
13 | void reset() throws IOException;
14 | boolean next();
15 | long getEntrySize() throws IOException;
16 | String getEntryName() throws IOException;
17 | InputStream getEntryInputStream() throws IOException;
18 | InputStream getEntryInputStreamByName(String entryName) throws IOException;
19 | boolean saveIntoArchive(String filePath, String fileName, String fileContent);
20 | boolean saveIntoArchive(String filePath, Map fileNameWithContentMap);
21 | String getArchivePath();
22 | void close();
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/component/Localization.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.component;
2 |
3 | import org.springframework.context.i18n.LocaleContextHolder;
4 | import org.springframework.context.support.ResourceBundleMessageSource;
5 | import org.springframework.stereotype.Component;
6 | import xyz.atsumeru.web.enums.ContentType;
7 |
8 | import java.util.Locale;
9 |
10 | @Component
11 | public class Localization {
12 | private static ResourceBundleMessageSource messageSource;
13 |
14 | public Localization(ResourceBundleMessageSource messageSource) {
15 | Localization.messageSource = messageSource;
16 | }
17 |
18 | public static String toLocale(String msgCode) {
19 | Locale locale = LocaleContextHolder.getLocale();
20 | try {
21 | return messageSource.getMessage(msgCode, null, locale);
22 | } catch (Exception ex) {
23 | return "Unlocalized";
24 | }
25 | }
26 |
27 | public static String toLocale(String msgCode, String... formatArgs) {
28 | Locale locale = LocaleContextHolder.getLocale();
29 | try {
30 | return String.format(messageSource.getMessage(msgCode, null, locale), (Object) formatArgs);
31 | } catch (Exception ex) {
32 | return "Unlocalized";
33 | }
34 | }
35 |
36 | public static String getFormatterForVolumeOrIssue(ContentType contentType, boolean archiveMode) {
37 | return contentType == ContentType.COMICS
38 | ? toLocale(archiveMode ? "web.issue" : "web.serie_issue")
39 | : toLocale(archiveMode ? "web.volume" : "web.serie_volume");
40 | }
41 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/configuration/CustomLocaleResolver.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.configuration;
2 |
3 | import jakarta.servlet.http.HttpServletRequest;
4 | import org.jetbrains.annotations.NotNull;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 | import org.springframework.context.support.ResourceBundleMessageSource;
8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
9 | import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
10 | import xyz.atsumeru.web.util.StringUtils;
11 |
12 | import java.util.Arrays;
13 | import java.util.List;
14 | import java.util.Locale;
15 |
16 | @Configuration
17 | public class CustomLocaleResolver extends AcceptHeaderLocaleResolver implements WebMvcConfigurer {
18 | List LOCALES = Arrays.asList(
19 | new Locale("en"),
20 | new Locale("ru"),
21 | new Locale("ua")
22 | );
23 |
24 | @NotNull
25 | @Override
26 | public Locale resolveLocale(HttpServletRequest request) {
27 | String headerLang = request.getHeader("Accept-Language");
28 | return StringUtils.isEmpty(headerLang)
29 | ? Locale.getDefault()
30 | : Locale.lookup(Locale.LanguageRange.parse(headerLang), LOCALES);
31 | }
32 |
33 | @Bean
34 | public ResourceBundleMessageSource messageSource() {
35 | ResourceBundleMessageSource resourceBundle = new ResourceBundleMessageSource();
36 | resourceBundle.setBasename("messages");
37 | resourceBundle.setDefaultEncoding("UTF-8");
38 | resourceBundle.setUseCodeAsDefaultMessage(true);
39 | return resourceBundle;
40 | }
41 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/configuration/EnumConverterConfiguration.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.configuration;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 | import org.springframework.format.FormatterRegistry;
5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
6 | import xyz.atsumeru.web.converter.StringToNonNullEnumConverter;
7 | import xyz.atsumeru.web.converter.StringToNullableEnumConverter;
8 | import xyz.atsumeru.web.enums.*;
9 | import xyz.atsumeru.web.manager.ImageCache;
10 |
11 | @Configuration
12 | public class EnumConverterConfiguration implements WebMvcConfigurer {
13 |
14 | @Override
15 | public void addFormatters(FormatterRegistry registry) {
16 | registry.addConverter(String.class, Sort.class, new StringToNullableEnumConverter<>(Sort.class));
17 | registry.addConverter(String.class, ContentType.class, new StringToNullableEnumConverter<>(ContentType.class));
18 | registry.addConverter(String.class, Status.class, new StringToNullableEnumConverter<>(Status.class));
19 | registry.addConverter(String.class, TranslationStatus.class, new StringToNullableEnumConverter<>(TranslationStatus.class));
20 | registry.addConverter(String.class, PlotType.class, new StringToNullableEnumConverter<>(PlotType.class));
21 | registry.addConverter(String.class, Censorship.class, new StringToNullableEnumConverter<>(Censorship.class));
22 | registry.addConverter(String.class, AgeRating.class, new StringToNullableEnumConverter<>(AgeRating.class));
23 | registry.addConverter(String.class, ServiceType.class, new StringToNullableEnumConverter<>(ServiceType.class));
24 |
25 | registry.addConverter(String.class, LibraryPresentation.class, new StringToNonNullEnumConverter<>(LibraryPresentation.class));
26 | registry.addConverter(String.class, LogicalMode.class, new StringToNonNullEnumConverter<>(LogicalMode.class));
27 | registry.addConverter(String.class, ImageCache.ImageCacheType.class, new StringToNonNullEnumConverter<>(ImageCache.ImageCacheType.class));
28 | }
29 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/configuration/GsonConfiguration.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.configuration;
2 |
3 | import org.springframework.boot.autoconfigure.gson.GsonBuilderCustomizer;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import xyz.atsumeru.web.json.adapter.OmitEmptyStringsAdapter;
7 |
8 | @Configuration
9 | public class GsonConfiguration {
10 |
11 | @Bean
12 | public GsonBuilderCustomizer typeAdapterRegistration() {
13 | return builder -> builder.registerTypeAdapter(String.class, new OmitEmptyStringsAdapter());
14 | }
15 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/configuration/MultiPartConfiguration.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.configuration;
2 |
3 | import jakarta.servlet.MultipartConfigElement;
4 | import org.springframework.boot.web.servlet.MultipartConfigFactory;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.ComponentScan;
7 | import org.springframework.context.annotation.Configuration;
8 | import org.springframework.util.unit.DataSize;
9 |
10 | @Configuration
11 | @ComponentScan
12 | public class MultiPartConfiguration {
13 | private static final DataSize DATA_SIZE_256_MB = DataSize.ofMegabytes(256);
14 |
15 | @Bean
16 | public MultipartConfigElement multipartConfigElement() {
17 | MultipartConfigFactory factory = new MultipartConfigFactory();
18 | factory.setMaxFileSize(DATA_SIZE_256_MB);
19 | factory.setMaxRequestSize(DATA_SIZE_256_MB);
20 | return factory.createMultipartConfig();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/configuration/OpenApiConfiguration.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.configuration;
2 |
3 | import io.swagger.v3.oas.annotations.ExternalDocumentation;
4 | import io.swagger.v3.oas.annotations.OpenAPIDefinition;
5 | import io.swagger.v3.oas.annotations.info.Contact;
6 | import io.swagger.v3.oas.annotations.info.Info;
7 | import io.swagger.v3.oas.annotations.info.License;
8 | import org.springframework.context.annotation.Configuration;
9 |
10 | @Configuration
11 | @OpenAPIDefinition(
12 | info = @Info(
13 | title = "Atsumeru API",
14 | version = "1.0",
15 | description = """
16 | API documentation for Atsumeru self-hosted mangas/comics/light novels media server
17 |
18 | Implementations:
19 | [Kotlin](https://github.com/Atsumeru-xyz/Atsumeru-API)
20 | """,
21 | contact = @Contact(
22 | name = "Wiki",
23 | url = "https://atsumeru.xyz"
24 | ),
25 | license = @License(
26 | name = "MIT",
27 | url = "https://mit-license.org/"
28 | )
29 | ),
30 | externalDocs = @ExternalDocumentation(
31 | description = "Source Code",
32 | url = "https://github.com/Atsumeru-xyz/Atsumeru"
33 | )
34 |
35 | )
36 | public class OpenApiConfiguration {
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/configuration/RestApiConfiguration.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.configuration;
2 |
3 | import org.springframework.boot.web.servlet.FilterRegistrationBean;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.core.Ordered;
7 | import org.springframework.web.bind.annotation.CrossOrigin;
8 | import org.springframework.web.cors.CorsConfiguration;
9 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
10 | import org.springframework.web.filter.CorsFilter;
11 |
12 | @CrossOrigin
13 | @Configuration
14 | public class RestApiConfiguration {
15 |
16 | @Bean
17 | public FilterRegistrationBean corsFilter() {
18 | return createCorsFilterRegistrationBean();
19 | }
20 |
21 | private FilterRegistrationBean createCorsFilterRegistrationBean() {
22 | FilterRegistrationBean bean = new FilterRegistrationBean<>(createCorsFilter());
23 | bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
24 | return bean;
25 | }
26 |
27 | private CorsFilter createCorsFilter() {
28 | CorsConfiguration corsConfig = createCorsConfiguration();
29 | UrlBasedCorsConfigurationSource source = createUrlBasedCorsConfigurationSource(corsConfig);
30 | return new CorsFilter(source);
31 | }
32 |
33 | private CorsConfiguration createCorsConfiguration() {
34 | CorsConfiguration config = new CorsConfiguration();
35 | config.setAllowCredentials(true);
36 | config.addAllowedOrigin("*");
37 | config.addAllowedHeader("*");
38 | config.addAllowedMethod("OPTIONS");
39 | config.addAllowedMethod("GET");
40 | config.addAllowedMethod("PUT");
41 | config.addAllowedMethod("POST");
42 | config.addAllowedMethod("DELETE");
43 | config.addAllowedMethod("PATCH");
44 |
45 | return config;
46 | }
47 |
48 | private UrlBasedCorsConfigurationSource createUrlBasedCorsConfigurationSource(CorsConfiguration corsConfig) {
49 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
50 | source.registerCorsConfiguration("/**", corsConfig);
51 | return source;
52 | }
53 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/configuration/ScheduleConfiguration.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.configuration;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.ComponentScan;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.scheduling.annotation.EnableScheduling;
7 | import org.springframework.scheduling.annotation.SchedulingConfigurer;
8 | import org.springframework.scheduling.config.ScheduledTaskRegistrar;
9 |
10 | import java.util.concurrent.Executor;
11 | import java.util.concurrent.Executors;
12 |
13 | @Configuration
14 | @EnableScheduling
15 | @ComponentScan
16 | public class ScheduleConfiguration implements SchedulingConfigurer {
17 |
18 | @Override
19 | public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
20 | taskRegistrar.setScheduler(taskExecutor());
21 | }
22 |
23 | @Bean(destroyMethod = "shutdownNow")
24 | public Executor taskExecutor() {
25 | return Executors.newScheduledThreadPool(100);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/configuration/ServerConfiguration.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.configuration;
2 |
3 | import org.springframework.boot.ApplicationArguments;
4 | import org.springframework.context.annotation.Configuration;
5 | import xyz.atsumeru.web.util.ArrayUtils;
6 | import xyz.atsumeru.web.util.TypeUtils;
7 |
8 | import java.util.Arrays;
9 | import java.util.List;
10 |
11 | @Configuration
12 | public class ServerConfiguration {
13 | public static final List ROLES = Arrays.asList("ADMIN", "USER");
14 | public static final List AUTHORITIES = Arrays.asList("IMPORTER", "UPLOADER", "METADATA_UPDATER", "DOWNLOAD_FILES");
15 |
16 | private final ApplicationArguments applicationArguments;
17 |
18 | public ServerConfiguration(ApplicationArguments applicationArguments) {
19 | this.applicationArguments = applicationArguments;
20 | }
21 |
22 | private boolean getArgsBooleanValue(String optionName, boolean def) {
23 | List args = applicationArguments.getOptionValues(optionName);
24 | return ArrayUtils.isEmpty(args) || TypeUtils.getBoolDef(args.get(0), def);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/configuration/WebConfiguration.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.configuration;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 | import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
6 |
7 | @Configuration
8 | public class WebConfiguration implements WebMvcConfigurer {
9 |
10 | @Override
11 | public void configurePathMatch(PathMatchConfigurer configurer) {
12 | configurer.setUseTrailingSlashMatch(true);
13 | }
14 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/controller/mvc/MainController.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.controller.mvc;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.http.ResponseEntity;
5 | import org.springframework.stereotype.Controller;
6 | import org.springframework.web.bind.annotation.RequestMapping;
7 |
8 | @Controller
9 | @RequestMapping("/")
10 | public class MainController {
11 |
12 | @RequestMapping(value = {"", "/"})
13 | public ResponseEntity fuckOffPage() {
14 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/controller/rest/ServerApiController.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.controller.rest;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import org.springframework.http.HttpStatus;
6 | import org.springframework.http.ResponseEntity;
7 | import org.springframework.security.access.prepost.PreAuthorize;
8 | import org.springframework.web.bind.annotation.GetMapping;
9 | import org.springframework.web.bind.annotation.RequestMapping;
10 | import org.springframework.web.bind.annotation.RestController;
11 | import xyz.atsumeru.web.AtsumeruApplication;
12 | import xyz.atsumeru.web.Beans;
13 | import xyz.atsumeru.web.helper.JavaHelper;
14 | import xyz.atsumeru.web.helper.RestHelper;
15 | import xyz.atsumeru.web.manager.Workspace;
16 | import xyz.atsumeru.web.model.AtsumeruMessage;
17 | import xyz.atsumeru.web.model.ServerInfo;
18 | import xyz.atsumeru.web.model.book.BookArchive;
19 | import xyz.atsumeru.web.model.book.BookSerie;
20 | import xyz.atsumeru.web.model.book.chapter.BookChapter;
21 | import xyz.atsumeru.web.model.database.Category;
22 | import xyz.atsumeru.web.service.CoversSaverService;
23 | import xyz.atsumeru.web.util.FileUtils;
24 |
25 | @RestController
26 | @RequestMapping(ServerApiController.ROOT_ENDPOINT)
27 | @Tag(name = "Server", description = "Server specific API: ping, get server info, clear covers cache")
28 | public class ServerApiController {
29 | protected static final String ROOT_ENDPOINT = "/api/server";
30 | private static final String PING_ENDPOINT = "/ping";
31 |
32 | public static String getPingEndpoint() {
33 | return ROOT_ENDPOINT + PING_ENDPOINT;
34 | }
35 |
36 | @Operation(summary = "Ping server", description = "May be used to check if server is online")
37 | @GetMapping(value = PING_ENDPOINT)
38 | public ResponseEntity ping() {
39 | return ResponseEntity.ok().build();
40 | }
41 |
42 | @Operation(summary = "Get server info", description = "Get server info: version, name and series, volumes, chapters, categories stats")
43 | @GetMapping("/info")
44 | public ServerInfo info() {
45 | return new ServerInfo()
46 | .setName("Atsumeru")
47 | .setVersion(JavaHelper.getAppVersion(AtsumeruApplication.class))
48 | .setVersionName("Bohrium")
49 | .setHasPassword(true)
50 | .setDebugMode(JavaHelper.isDebug())
51 | .setStats(new ServerInfo.Stats()
52 | .setTotalSeries(Beans.getBooksDaoManager().count(BookSerie.class))
53 | .setTotalArchives(Beans.getBooksDaoManager().count(BookArchive.class))
54 | .setTotalChapters(Beans.getBooksDaoManager().count(BookChapter.class))
55 | .setTotalCategories(Beans.getBooksDaoManager().count(Category.class)));
56 | }
57 |
58 | @Operation(summary = "Recreate covers cache", description = "Clear covers cache and start caching service to build new cache")
59 | @PreAuthorize("hasRole('ADMIN')")
60 | @GetMapping("/clear_cover_cache")
61 | public ResponseEntity clearCache() {
62 | new Thread(() -> {
63 | FileUtils.deleteDirectory(Workspace.CACHE_DIR);
64 | Workspace.checkWorkspace();
65 | CoversSaverService.saveNonExistentCoversIntoCache();
66 | }).start();
67 |
68 | return RestHelper.createResponseMessage("Cache cleared", HttpStatus.OK);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/controller/rest/book/FilteredByBoundServiceApiController.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.controller.rest.book;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import jakarta.servlet.http.HttpServletRequest;
6 | import org.springframework.cache.annotation.Cacheable;
7 | import org.springframework.security.access.prepost.PreAuthorize;
8 | import org.springframework.stereotype.Controller;
9 | import org.springframework.util.MultiValueMap;
10 | import org.springframework.web.bind.annotation.*;
11 | import xyz.atsumeru.web.Beans;
12 | import xyz.atsumeru.web.enums.LibraryPresentation;
13 | import xyz.atsumeru.web.enums.ServiceType;
14 | import xyz.atsumeru.web.model.book.BookSerie;
15 | import xyz.atsumeru.web.model.book.DownloadedLinks;
16 | import xyz.atsumeru.web.util.ArrayUtils;
17 |
18 | import java.util.List;
19 | import java.util.Map;
20 | import java.util.Optional;
21 | import java.util.Set;
22 | import java.util.stream.Collectors;
23 |
24 | @Controller
25 | @RestController
26 | @RequestMapping("/api/v1/books/")
27 | @Tag(name = "Filtering by Bound Service", description = "API for Books requesting by Bound Service connection")
28 | public class FilteredByBoundServiceApiController {
29 |
30 | @Operation(summary = "Books by Bound Service", description = "Get Books list by Bound Service Name and ID")
31 | @Cacheable(value = "books_by_bound_service", key = "#request.userPrincipal.name.concat('-')" +
32 | ".concat(\"\" + #boundServiceName).concat('-')" +
33 | ".concat(#boundServiceId).concat('-')")
34 | @GetMapping("{bound_service_name}/{bound_service_id}")
35 | public List getBooksByBoundService(HttpServletRequest request,
36 | @PathVariable("bound_service_name") String boundServiceName,
37 | @PathVariable("bound_service_id") String boundServiceId) {
38 | return Optional.ofNullable(ServiceType.getDbFieldNameForSimpleName(boundServiceName))
39 | .map(dbFieldName -> Beans.getBooksDaoManager().query(dbFieldName, boundServiceId, BookSerie.class)
40 | .stream()
41 | .map(BookSerie.class::cast)
42 | .peek(BookSerie::prepareBoundServices)
43 | .collect(Collectors.toList())
44 | ).orElse(null);
45 | }
46 |
47 | @Operation(summary = "Check Book present", description = "Check if Book is present in database by Download Link")
48 | @PreAuthorize("hasRole('ADMIN')")
49 | @PostMapping("/check_downloaded")
50 | public DownloadedLinks checkLinksDownloaded(@RequestBody MultiValueMap formData) {
51 | List links = ArrayUtils.splitString(formData.getFirst("links"), ",");
52 |
53 | Set downloadedLinks = Beans.getBooksDaoManager()
54 | .queryAll(BookSerie.class, LibraryPresentation.SERIES_AND_SINGLES)
55 | .stream()
56 | .map(BookSerie.class::cast)
57 | .flatMap(serie -> ArrayUtils.splitString(serie.getSerieLinks()).stream())
58 | .collect(Collectors.toSet());
59 |
60 | Map> collected = links.stream().collect(Collectors.groupingBy(downloadedLinks::contains));
61 | return new DownloadedLinks(collected.get(true), collected.get(false));
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/controller/rest/category/MetacategoriesApiController.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.controller.rest.category;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import jakarta.servlet.http.HttpServletRequest;
6 | import org.springframework.stereotype.Controller;
7 | import org.springframework.web.bind.annotation.*;
8 | import xyz.atsumeru.web.model.book.IBaseBookItem;
9 | import xyz.atsumeru.web.model.category.Metacategory;
10 | import xyz.atsumeru.web.repository.MetacategoryRepository;
11 | import xyz.atsumeru.web.security.repository.UsersRepository;
12 |
13 | import java.util.List;
14 | import java.util.Set;
15 |
16 | @Controller
17 | @RestController
18 | @RequestMapping("/api/v1/books/metacategories")
19 | @Tag(name = "Metacategories", description = "API for requesting Metacategories specific info")
20 | public class MetacategoriesApiController {
21 | private final UsersRepository userService;
22 |
23 | public MetacategoriesApiController(UsersRepository userService) {
24 | this.userService = userService;
25 | }
26 |
27 | @Operation(summary = "Metacategories list", description = "Get list of available auto-created Metacategories")
28 | @GetMapping("")
29 | public Set getMetacategoryList() {
30 | return MetacategoryRepository.getMetacategories();
31 | }
32 |
33 | @Operation(summary = "Metacategory entries", description = "Get list Metacategory entries by id")
34 | @GetMapping("/{metacategory_id}")
35 | public List getMetacategoryEntries(@PathVariable(value = "metacategory_id") String metacategoryId) {
36 | return MetacategoryRepository.getEntries(metacategoryId);
37 | }
38 |
39 | @Operation(summary = "Metacategory Book list", description = "Get list Books in corresponding Metacategory by id and filter")
40 | @GetMapping("/{metacategory_id}/{filter}")
41 | public List getMetacategoryEntries(HttpServletRequest request,
42 | @PathVariable(value = "metacategory_id") String metacategoryId,
43 | @PathVariable(value = "filter") String filter,
44 | @RequestParam(value = "page", defaultValue = "1") int page,
45 | @RequestParam(value = "limit", defaultValue = "30") int limit,
46 | @RequestParam(value = "with_volumes", defaultValue = "false") boolean withVolumesAndHistory,
47 | @RequestParam(value = "with_chapters", defaultValue = "false") boolean withChapters) {
48 | return MetacategoryRepository.getFilteredList(userService.getUserFromRequest(request), metacategoryId, filter, page, limit, withVolumesAndHistory, withChapters);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/controller/rest/file/FilesApiController.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.controller.rest.file;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import jakarta.servlet.http.HttpServletResponse;
6 | import org.springframework.http.MediaType;
7 | import org.springframework.security.core.context.SecurityContextHolder;
8 | import org.springframework.web.bind.annotation.*;
9 | import xyz.atsumeru.web.helper.FilesHelper;
10 | import xyz.atsumeru.web.manager.ImageCache;
11 |
12 | import java.io.IOException;
13 |
14 | @RestController
15 | @RequestMapping("/api/v1")
16 | @Tag(name = "Files", description = "API for requesting files from server")
17 | public class FilesApiController {
18 |
19 | @Operation(summary = "Download Volume", description = "Download Volume archive file by Volume Hash")
20 | @GetMapping("/download/{archive_hash}")
21 | public void downloadBook(HttpServletResponse response,
22 | @PathVariable(value = "archive_hash") String archiveHash) throws IOException {
23 | FilesHelper.downloadFile(response, SecurityContextHolder.getContext().getAuthentication(), archiveHash);
24 | }
25 |
26 | @Operation(summary = "Book Cover", description = "Get Book cover image by Cover Hash with optional converting to PNG")
27 | @GetMapping(value = "/cover/{image_hash}", produces = MediaType.IMAGE_PNG_VALUE)
28 | public @ResponseBody byte[] getBookCover(HttpServletResponse response,
29 | @PathVariable(value = "image_hash") String imageHash,
30 | @RequestParam(value = "type", defaultValue = "original") ImageCache.ImageCacheType imageCacheType,
31 | @RequestParam(value = "convert", defaultValue = "false") boolean convertImage) {
32 | return FilesHelper.getCover(response, imageHash, imageCacheType, convertImage);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/controller/rest/history/HistoryApiController.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.controller.rest.history;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import jakarta.servlet.http.HttpServletRequest;
6 | import org.springframework.cache.annotation.Cacheable;
7 | import org.springframework.stereotype.Controller;
8 | import org.springframework.web.bind.annotation.GetMapping;
9 | import org.springframework.web.bind.annotation.RequestMapping;
10 | import org.springframework.web.bind.annotation.RequestParam;
11 | import org.springframework.web.bind.annotation.RestController;
12 | import xyz.atsumeru.web.enums.LibraryPresentation;
13 | import xyz.atsumeru.web.model.book.IBaseBookItem;
14 | import xyz.atsumeru.web.repository.HistoryRepository;
15 | import xyz.atsumeru.web.security.repository.UsersRepository;
16 |
17 | import java.util.List;
18 |
19 | @Controller
20 | @RestController
21 | @RequestMapping("/api/v1/books")
22 | @Tag(name = "History", description = "API for requesting user History")
23 | public class HistoryApiController {
24 | private final UsersRepository userService;
25 |
26 | public HistoryApiController(UsersRepository userService) {
27 | this.userService = userService;
28 | }
29 |
30 | //*****************************//
31 | //* History *//
32 | //*****************************//
33 | @Operation(summary = "History list", description = "Get user History list by LibraryPresentation")
34 | @GetMapping("/history")
35 | @Cacheable(value = "history", key="#request.userPrincipal.name.concat('-')" +
36 | ".concat(#libraryPresentation.toString()).concat('-')" +
37 | ".concat(#page).concat('-')" +
38 | ".concat(#limit).concat('-')")
39 | public List getBooksHistory(HttpServletRequest request,
40 | @RequestParam(value = "presentation", defaultValue = "series") LibraryPresentation libraryPresentation,
41 | @RequestParam(value = "page", defaultValue = "1") int page,
42 | @RequestParam(value = "limit", defaultValue = "50") long limit) {
43 | return HistoryRepository.getBooksHistory(userService.getUserFromRequest(request), libraryPresentation, page, limit);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/controller/rest/service/ServicesApiController.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.controller.rest.service;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import org.springframework.security.access.prepost.PreAuthorize;
6 | import org.springframework.security.core.Authentication;
7 | import org.springframework.security.core.context.SecurityContextHolder;
8 | import org.springframework.web.bind.annotation.GetMapping;
9 | import org.springframework.web.bind.annotation.RequestMapping;
10 | import org.springframework.web.bind.annotation.RestController;
11 | import xyz.atsumeru.web.controller.rest.importer.ImporterApiController;
12 | import xyz.atsumeru.web.controller.rest.metadata.MetadataApiController;
13 | import xyz.atsumeru.web.exception.MetadataUpdateActiveException;
14 | import xyz.atsumeru.web.model.service.ServicesStatus;
15 | import xyz.atsumeru.web.security.service.UsersDetailsService;
16 | import xyz.atsumeru.web.service.CoversSaverService;
17 | import xyz.atsumeru.web.service.MetadataUpdateService;
18 |
19 | @RestController
20 | @RequestMapping(ServicesApiController.ROOT_ENDPOINT)
21 | @PreAuthorize("hasRole('ADMIN') or hasAnyAuthority('IMPORTER', 'METADATA_UPDATER')")
22 | @Tag(name = "Services", description = "API for requesting Services status")
23 | public class ServicesApiController {
24 | protected static final String ROOT_ENDPOINT = "/api/v1/services";
25 | private static final String STATUS_ENDPOINT = "/status";
26 |
27 | private final ImporterApiController importerController;
28 | private final MetadataApiController metadataController;
29 |
30 | public ServicesApiController(ImporterApiController importerController, MetadataApiController metadataController) {
31 | this.importerController = importerController;
32 | this.metadataController = metadataController;
33 | }
34 |
35 | public static void checkIsBlockingServicesRunning(boolean isServiceStatusRequest) {
36 | if (MetadataUpdateService.isUpdateActive() && !isServiceStatusRequest) {
37 | throw new MetadataUpdateActiveException();
38 | }
39 | }
40 |
41 | @Operation(summary = "Status", description = "Get all running services Status (Imported, Metadata, Covers Caching)")
42 | @GetMapping(STATUS_ENDPOINT)
43 | public ServicesStatus getStatus() {
44 | Authentication auth = SecurityContextHolder.getContext().getAuthentication();
45 | if (auth != null) {
46 | boolean isUserAdminOrImporter = UsersDetailsService.isUserInRole(auth, "ADMIN", "IMPORTER");
47 | return new ServicesStatus(
48 | isUserAdminOrImporter ? importerController.getStatus() : null,
49 | UsersDetailsService.isUserInRole(auth, "ADMIN", "METADATA_UPDATER") ? metadataController.getStatus() : null,
50 | isUserAdminOrImporter ? CoversSaverService.getStatus() : null
51 | );
52 | }
53 | return null;
54 | }
55 |
56 | public static String getStatusEndpoint() {
57 | return ROOT_ENDPOINT + STATUS_ENDPOINT;
58 | }
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/controller/rest/settings/SettingsApiController.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.controller.rest.settings;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import org.springframework.context.ApplicationContext;
6 | import org.springframework.http.HttpStatus;
7 | import org.springframework.http.ResponseEntity;
8 | import org.springframework.security.access.prepost.PreAuthorize;
9 | import org.springframework.web.bind.annotation.*;
10 | import xyz.atsumeru.web.AtsumeruApplication;
11 | import xyz.atsumeru.web.configuration.FileWatcherConfiguration;
12 | import xyz.atsumeru.web.helper.RestHelper;
13 | import xyz.atsumeru.web.interceptor.RequestLogInterceptor;
14 | import xyz.atsumeru.web.manager.Settings;
15 | import xyz.atsumeru.web.model.AtsumeruMessage;
16 | import xyz.atsumeru.web.model.settings.ServerSettings;
17 | import xyz.atsumeru.web.service.CoversSaverService;
18 | import xyz.atsumeru.web.service.ImportService;
19 | import xyz.atsumeru.web.service.MetadataUpdateService;
20 | import xyz.atsumeru.web.util.AppUtils;
21 |
22 | import java.util.function.Supplier;
23 |
24 | @RestController
25 | @RequestMapping("/api/v1/settings")
26 | @PreAuthorize("hasRole('ADMIN')")
27 | @Tag(name = "Settings", description = "API controlling server settings")
28 | public class SettingsApiController {
29 | private final Supplier serverLockedSupplier = () -> MetadataUpdateService.isUpdateActive() || ImportService.isImportActive() || CoversSaverService.isCachingActive();
30 |
31 | private final ApplicationContext context;
32 |
33 | public SettingsApiController(ApplicationContext context) {
34 | this.context = context;
35 | }
36 |
37 | @Operation(summary = "Get settings", description = "Get current server settings")
38 | @GetMapping("/get")
39 | public ResponseEntity> getSettings() {
40 | if (serverLockedSupplier.get()) {
41 | return RestHelper.createResponseMessage("Unable to edit server settings", HttpStatus.NOT_ACCEPTABLE);
42 | }
43 | return new ResponseEntity<>(
44 | new ServerSettings(
45 | Settings.isAllowListLoadingWithVolumes(),
46 | Settings.isAllowListLoadingWithChapters(),
47 | Settings.isDisableRequestLoggingIntoConsole(),
48 | Settings.isDisableFileWatcher(),
49 | Settings.isDisableWatchForModifiedFiles(),
50 | Settings.isDisableChapters()
51 | ),
52 | HttpStatus.OK
53 | );
54 | }
55 |
56 | @Operation(summary = "Update settings", description = "Update server settings")
57 | @PostMapping("/update")
58 | public ResponseEntity updateSettings(@RequestBody ServerSettings settings) {
59 | boolean currentDisableChapters = Settings.isDisableChapters();
60 |
61 | Settings.putAllowListLoadingWithVolumes(settings.isAllowLoadingListWithVolumes());
62 | Settings.putAllowListLoadingWithChapters(settings.isAllowLoadingListWithChapters());
63 | Settings.putDisableRequestLoggingIntoConsole(settings.isDisableRequestLoggingIntoConsole());
64 | Settings.putDisableFileWatcher(settings.isDisableFileWatcher());
65 | Settings.putDisableWatchForModifiedFiles(settings.isDisableWatchForModifiedFiles());
66 | Settings.putDisableChapters(settings.isDisableChapters());
67 |
68 | context.getBean(RequestLogInterceptor.class).onSettingsUpdate();
69 | FileWatcherConfiguration.start();
70 |
71 | if (currentDisableChapters != settings.isDisableChapters()) {
72 | restartServerDelayed();
73 | }
74 |
75 | return RestHelper.createResponseMessage("Settings updated successfully", HttpStatus.OK);
76 | }
77 |
78 | private void restartServerDelayed() {
79 | new Thread(() -> {
80 | AppUtils.sleepWhile(1000, serverLockedSupplier);
81 | AtsumeruApplication.restart();
82 | }).start();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/controller/rest/sync/SyncApiController.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.controller.rest.sync;
2 |
3 | import io.swagger.v3.oas.annotations.Operation;
4 | import io.swagger.v3.oas.annotations.tags.Tag;
5 | import jakarta.servlet.http.HttpServletRequest;
6 | import org.springframework.cache.annotation.CacheEvict;
7 | import org.springframework.http.HttpStatus;
8 | import org.springframework.http.ResponseEntity;
9 | import org.springframework.stereotype.Controller;
10 | import org.springframework.util.MultiValueMap;
11 | import org.springframework.web.bind.annotation.*;
12 | import xyz.atsumeru.web.helper.RestHelper;
13 | import xyz.atsumeru.web.model.AtsumeruMessage;
14 | import xyz.atsumeru.web.model.database.History;
15 | import xyz.atsumeru.web.repository.BooksRepository;
16 | import xyz.atsumeru.web.repository.HistoryRepository;
17 | import xyz.atsumeru.web.security.repository.UsersRepository;
18 | import xyz.atsumeru.web.util.ArrayUtils;
19 | import xyz.atsumeru.web.util.StringUtils;
20 | import xyz.atsumeru.web.util.TypeUtils;
21 |
22 | import java.util.List;
23 | import java.util.Map;
24 |
25 | @Controller
26 | @RestController
27 | @RequestMapping("/api/v1/books/sync")
28 | @Tag(name = "Sync", description = "API for synchronizing read history")
29 | public class SyncApiController {
30 | private final UsersRepository userService;
31 |
32 | public SyncApiController(UsersRepository userService) {
33 | this.userService = userService;
34 | }
35 |
36 | @Operation(summary = "Push", description = "Push read history (current page) for Book and Volume/Chapter by Hashes")
37 | @GetMapping(value = "/push")
38 | @CacheEvict(cacheNames = {"history", "books", "books_by_bound_service"}, allEntries = true)
39 | public ResponseEntity getPushReadHistory(HttpServletRequest request,
40 | @RequestParam(value = "hash", required = false) String hash,
41 | @RequestParam(value = "archive_hash", required = false) String archiveHash,
42 | @RequestParam(value = "chapter_hash", required = false) String chapterHash,
43 | @RequestParam(value = "page") int page) {
44 | HistoryRepository.saveReadedPage(userService.getUserFromRequest(request), StringUtils.getFirstNotEmptyValue(hash, archiveHash), chapterHash, page);
45 | return RestHelper.createResponseMessage("Synced successfully", HttpStatus.OK);
46 | }
47 |
48 | @Operation(summary = "Batch push", description = "Batch push read history (current page) for Book and Volume/Chapter by Hashes")
49 | @PostMapping(value = "/push")
50 | @CacheEvict(cacheNames = {"history", "books", "books_by_bound_service"}, allEntries = true)
51 | public ResponseEntity postPushReadHistory(HttpServletRequest request, @RequestBody MultiValueMap formData) {
52 | if (ArrayUtils.isNotEmpty(formData)) {
53 | for (Map.Entry> entry : formData.entrySet()) {
54 | String hash = entry.getKey();
55 | HistoryRepository.saveReadedPage(
56 | userService.getUserFromRequest(request),
57 | BooksRepository.isArchiveHash(hash) ? hash : null,
58 | BooksRepository.isChapterHash(hash) ? hash : null,
59 | TypeUtils.getIntDef(entry.getValue().get(0), 0)
60 | );
61 | }
62 | return RestHelper.createResponseMessage("Synced successfully", HttpStatus.OK);
63 | }
64 | return RestHelper.createResponseMessage("Sync error. No form_data values", HttpStatus.NOT_ACCEPTABLE.value(), HttpStatus.OK);
65 | }
66 |
67 | @Operation(summary = "Pull", description = "Pull read history for Book or Volume/Chapter by Hash")
68 | @GetMapping(value = "/pull/{book_or_archive_hash}")
69 | public List getBookHistory(HttpServletRequest request, @PathVariable(value = "book_or_archive_hash") String bookOrArchiveHash) {
70 | return HistoryRepository.getBookHistory(userService.getUserFromRequest(request), bookOrArchiveHash);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/converter/StringToNonNullEnumConverter.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.converter;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 | import org.springframework.core.convert.converter.Converter;
5 | import xyz.atsumeru.web.util.EnumUtils;
6 |
7 | public class StringToNonNullEnumConverter> implements Converter {
8 | private final Class clazz;
9 |
10 | public StringToNonNullEnumConverter(Class clazz) {
11 | this.clazz = clazz;
12 | }
13 |
14 | @Override
15 | public E convert(@NotNull String source) {
16 | return EnumUtils.valueOf(clazz, EnumUtils.convertHumanizedToEnumName(source));
17 | }
18 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/converter/StringToNullableEnumConverter.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.converter;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 | import org.springframework.core.convert.converter.Converter;
5 | import xyz.atsumeru.web.util.EnumUtils;
6 |
7 | public class StringToNullableEnumConverter> implements Converter {
8 | private final Class clazz;
9 |
10 | public StringToNullableEnumConverter(Class clazz) {
11 | this.clazz = clazz;
12 | }
13 |
14 | @Override
15 | public E convert(@NotNull String source) {
16 | return EnumUtils.valueOfOrNull(clazz, EnumUtils.convertHumanizedToEnumName(source));
17 | }
18 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/AgeRating.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | public enum AgeRating {
4 | UNKNOWN,
5 | EVERYONE,
6 | EVERYONE_TEN_PLUS,
7 | TEEN,
8 | MATURE,
9 | ADULTS_ONLY
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/BookType.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | public enum BookType {
4 | ARCHIVE, EPUB, FB2, PDF, DJVU
5 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/Censorship.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | public enum Censorship {
4 | UNKNOWN(0),
5 | CENSORED(1),
6 | UNCENSORED(2),
7 | DECENSORED(3),
8 | PARTIALLY_CENSORED(4),
9 | MOSAIC_CENSORSHIP(5);
10 |
11 | public final int id;
12 |
13 | Censorship(int id) {
14 | this.id = id;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/Color.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | public enum Color {
4 | UNKNOWN,
5 | MONOCHROME,
6 | PARTIALLY_COLORED,
7 | FULL_COLOR,
8 | COLORED
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/ContentType.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | import java.util.Arrays;
4 | import java.util.List;
5 |
6 | public enum ContentType {
7 | UNKNOWN(0),
8 |
9 | // Text (images)
10 | MANGA(10),
11 | MANHUA(11),
12 | MANHWA(12),
13 | DOUJINSHI(13),
14 | HENTAI_MANGA(14),
15 | YAOI(15),
16 | YAOI_MANGA(16),
17 | WEBCOMICS(17),
18 | RUMANGA(18),
19 | OEL_MANGA(19),
20 | STRIP(20),
21 | COMICS(21),
22 | YURI(26),
23 | YURI_MANGA(27),
24 | HENTAI_MANHWA(28),
25 |
26 | // Text (books)
27 | LIGHT_NOVEL(22),
28 | NOVEL(23),
29 | BOOK(24),
30 | TEXT_PORN(25),
31 |
32 | // Video
33 | ANIME(30),
34 | HENTAI(31),
35 | HENTAI_ANIME(32),
36 | YAOI_ANIME(33),
37 | DORAMA(34),
38 | CARTOON(35),
39 | MOVIES_TV(36),
40 | MOVIE(37),
41 | TV(38),
42 | PORN(39),
43 | YURI_ANIME(40),
44 |
45 | // Audio
46 | PODCAST(50),
47 | AUDIO(51),
48 | AUDIO_MUSIC(52),
49 | AUDIO_BOOK(53),
50 | AUDIO_NOVEL(54),
51 | AUDIO_LIGHT_NOVEL(55),
52 | AUDIO_PORN(56);
53 |
54 | public final int id;
55 |
56 | ContentType(int id) {
57 | this.id = id;
58 | }
59 |
60 | public static List getSupportedTypes() {
61 | return Arrays.asList(UNKNOWN, MANGA, MANHWA, MANHUA, COMICS, WEBCOMICS, LIGHT_NOVEL, HENTAI_MANGA, HENTAI_MANHWA,
62 | DOUJINSHI, YURI_MANGA, YAOI_MANGA, NOVEL, BOOK, RUMANGA, OEL_MANGA, STRIP);
63 | }
64 |
65 | public static boolean isMatureContent(ContentType contentType) {
66 | return contentType == HENTAI
67 | || contentType == HENTAI_ANIME
68 | || contentType == HENTAI_MANGA
69 | || contentType == HENTAI_MANHWA
70 | || contentType == YAOI
71 | || contentType == YAOI_ANIME
72 | || contentType == YAOI_MANGA
73 | || contentType == AUDIO_PORN
74 | || contentType == TEXT_PORN;
75 | }
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/LibraryPresentation.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | import xyz.atsumeru.web.model.book.BookArchive;
4 | import xyz.atsumeru.web.model.book.BookSerie;
5 | import xyz.atsumeru.web.model.book.IBaseBookItem;
6 |
7 | public enum LibraryPresentation {
8 | SERIES,
9 | SINGLES,
10 | ARCHIVES,
11 | SERIES_AND_SINGLES;
12 |
13 | public Class extends IBaseBookItem> getDbClassForPresentation() {
14 | return switch (this) {
15 | case SERIES, SINGLES, SERIES_AND_SINGLES -> BookSerie.class;
16 | case ARCHIVES -> BookArchive.class;
17 | };
18 | }
19 |
20 | public boolean isSeriesOrSinglesPresentation() {
21 | return this == SERIES || this == SINGLES || this == SERIES_AND_SINGLES;
22 | }
23 |
24 | public boolean isSeriesAndSinglesPresentation() {
25 | return this == SERIES_AND_SINGLES;
26 | }
27 |
28 | public boolean isSeriesPresentation() {
29 | return this == SERIES;
30 | }
31 |
32 | public boolean isSinglesPresentation() {
33 | return this == SINGLES;
34 | }
35 |
36 | public boolean isArchivesPresentation() {
37 | return this == ARCHIVES;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/LogicalMode.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | public enum LogicalMode {
4 | AND,
5 | OR
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/PlotType.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | public enum PlotType {
4 | UNKNOWN(Integer.MAX_VALUE),
5 | MAIN_STORY(0), // Основная история
6 | ALTERNATIVE_STORY(1), // Альтернативная история
7 | PREQUEL(2), // Предыстория/Приквел
8 | INTERQUEL(3), // Интерквел
9 | SEQUEL(4), // Продолжение/Сиквел
10 | THREEQUEL(5), // Триквел
11 | QUADRIQUEL(6), // Квадриквел
12 | MIDQUEL(7), // Мидквел
13 | PARALLELQUEL(8), // Параллелквел
14 | REQUEL(9), // Риквел
15 | ADAPTATION(10), // Адаптация
16 | SPIN_OFF(11), // Ответвление от оригинала/Спин-офф
17 | CROSSOVER(12), // Кроссовер
18 | COMMON_CHARACTER(13), // Общий персонаж
19 | COLORED(14), // Цветная версия
20 | OTHER(15); // Прочее
21 |
22 | private final int order;
23 |
24 | PlotType(int order) {
25 | this.order = order;
26 | }
27 |
28 | public int getOrder() {
29 | return order;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/ServiceType.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | import lombok.Getter;
4 | import org.springframework.lang.Nullable;
5 | import xyz.atsumeru.web.model.database.DatabaseFields;
6 | import xyz.atsumeru.web.util.StringUtils;
7 |
8 | import java.util.Arrays;
9 | import java.util.Optional;
10 | import java.util.regex.Matcher;
11 | import java.util.regex.Pattern;
12 |
13 | public enum ServiceType {
14 | MYANIMELIST("mal", "https://myanimelist.net/manga/%s", Pattern.compile("manga/(\\d+)"), DatabaseFields.MAL_ID),
15 | SHIKIMORI("shiki", "https://shikimori.me/mangas/%s", Pattern.compile("/(\\d+)|/\\w(\\d+)"), DatabaseFields.SHIKIMORI_ID),
16 | KITSU("kt", "https://kitsu.io/manga/%s", Pattern.compile("manga/(.*)/|manga/(.*)"), DatabaseFields.KITSU_ID),
17 | ANILIST("al", "https://anilist.co/manga/%s", Pattern.compile("manga/(\\d+)"), DatabaseFields.ANILIST_ID),
18 | MANGAUPDATES("mu", "https://www.mangaupdates.com/series/%s", Pattern.compile("series/(.*?)/|series/(.*?)$"), DatabaseFields.MANGAUPDATES_ID),
19 | ANIMEPLANET("ap", "https://www.anime-planet.com/manga/%s", Pattern.compile("manga/(.*)/|manga/(.*)"), DatabaseFields.ANIMEPLANET_ID),
20 | COMICVINE("cv", "https://comicvine.gamespot.com/comic/%s/", Pattern.compile("(\\d+-\\d+)"), DatabaseFields.COMICVINE_ID),
21 | COMICSDB("cdb", "https://comicsdb.ru/publishers/%s", Pattern.compile("publishers/(.*)"), DatabaseFields.COMICSDB_ID),
22 | HENTAG("htg", "https://hentag.com/vault/%s", Pattern.compile("vault/(.*)"), DatabaseFields.HENTAG_ID);
23 |
24 | @Getter
25 | private final String simpleName;
26 | private final String formatUrl;
27 | private final Pattern idPattern;
28 | @Getter
29 | private final String dbFieldName;
30 |
31 | ServiceType(String simpleName, String formatUrl, Pattern idPattern, String dbFieldName) {
32 | this.simpleName = simpleName;
33 | this.formatUrl = formatUrl;
34 | this.idPattern = idPattern;
35 | this.dbFieldName = dbFieldName;
36 | }
37 |
38 | public String extractId(String str) {
39 | Matcher matcher = idPattern.matcher(str);
40 | if (matcher.find()) {
41 | String firstGroup = matcher.group(1);
42 | String secondGroup = null;
43 | try {
44 | secondGroup = matcher.group(2);
45 | } catch (Exception ignored) {
46 | }
47 | return StringUtils.getFirstNotEmptyValue(firstGroup, secondGroup);
48 | }
49 | return null;
50 | }
51 |
52 | public String createUrl(String id) {
53 | return String.format(formatUrl, id);
54 | }
55 |
56 | public static @Nullable ServiceType getTypeBySimpleName(@Nullable String name) {
57 | return Arrays.stream(ServiceType.values())
58 | .filter(serviceType -> StringUtils.equalsIgnoreCase(serviceType.name(), name) || StringUtils.equalsIgnoreCase(serviceType.getSimpleName(), name))
59 | .findFirst()
60 | .orElse(null);
61 | }
62 |
63 | public static @Nullable String getDbFieldNameForSimpleName(@Nullable String name) {
64 | return Optional.ofNullable(getTypeBySimpleName(name))
65 | .map(ServiceType::getDbFieldName)
66 | .orElse(null);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/Sort.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | public enum Sort {
4 | CREATED_AT,
5 | UPDATED_AT,
6 | TITLE,
7 | POPULARITY,
8 | YEAR,
9 | COUNTRY,
10 | LANGUAGE,
11 | PUBLISHER,
12 | SERIE,
13 | PARODY,
14 | VOLUMES_COUNT,
15 | CHAPTERS_COUNT,
16 | SCORE,
17 | LAST_READ
18 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/Status.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | public enum Status {
4 | UNKNOWN(0),
5 | ONGOING(1),
6 | COMPLETE(2),
7 | SINGLE(3),
8 | OVA(4),
9 | ONA(5),
10 | LICENSED(6),
11 | EMPTY(7),
12 | ANNOUNCEMENT(8),
13 | NOT_RELEASED(9),
14 | CANCELED(10),
15 | ON_HOLD(11),
16 | ANTHOLOGY(12),
17 | MAGAZINE(13);
18 |
19 | public final int id;
20 |
21 | Status(int id) {
22 | this.id = id;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/enums/TranslationStatus.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.enums;
2 |
3 | public enum TranslationStatus {
4 | UNKNOWN(0),
5 | ONGOING(1),
6 | COMPLETE(2),
7 | ON_HOLD(3),
8 | DROPPED(4);
9 |
10 | public final int id;
11 |
12 | TranslationStatus(int id) {
13 | this.id = id;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/ArchiveReadingException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class ArchiveReadingException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/ChapterNotFoundException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class ChapterNotFoundException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/DjVuReadingException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class DjVuReadingException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/DownloadsNotAllowedException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class DownloadsNotAllowedException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/ImportActiveException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class ImportActiveException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/MediaUnsupportedException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class MediaUnsupportedException extends RuntimeException {
4 |
5 | public MediaUnsupportedException(String message) {
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/MetadataUpdateActiveException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class MetadataUpdateActiveException extends RuntimeException {
4 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/NoCoverFoundException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class NoCoverFoundException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/NoReadableFoundException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class NoReadableFoundException extends RuntimeException {
4 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/NotAcceptableForOnlineReadingException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class NotAcceptableForOnlineReadingException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/PDFReadingException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class PDFReadingException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/PageNotFoundException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class PageNotFoundException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/RendererNotImplementedException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class RendererNotImplementedException extends RuntimeException {
4 |
5 | public RendererNotImplementedException(String message) {
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/exception/UserNotFoundException.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.exception;
2 |
3 | public class UserNotFoundException extends RuntimeException {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/filter/StatsFilter.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.filter;
2 |
3 | import jakarta.servlet.*;
4 | import jakarta.servlet.annotation.WebFilter;
5 | import jakarta.servlet.http.HttpServletRequest;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 | import org.springframework.core.Ordered;
9 | import org.springframework.core.annotation.Order;
10 | import org.springframework.stereotype.Component;
11 | import xyz.atsumeru.web.controller.rest.service.ServicesApiController;
12 | import xyz.atsumeru.web.helper.JavaHelper;
13 | import xyz.atsumeru.web.util.StringUtils;
14 |
15 | import java.io.IOException;
16 | import java.time.Duration;
17 | import java.time.Instant;
18 |
19 | @Component
20 | @WebFilter("/*")
21 | @Order(Ordered.HIGHEST_PRECEDENCE)
22 | public class StatsFilter implements Filter {
23 | private static final Logger logger = LoggerFactory.getLogger(StatsFilter.class.getSimpleName());
24 |
25 | @Override
26 | public void init(FilterConfig filterConfig) {
27 | // empty
28 | }
29 |
30 | @Override
31 | public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
32 | String requestURI = ((HttpServletRequest) req).getRequestURI();
33 | Instant start = Instant.now();
34 | try {
35 | chain.doFilter(req, resp);
36 | } finally {
37 | if (JavaHelper.isDebug() && !StringUtils.equalsIgnoreCase(requestURI, ServicesApiController.getStatusEndpoint())) {
38 | Instant finish = Instant.now();
39 | long time = Duration.between(start, finish).toMillis();
40 | logger.info("{}: {} ms ", requestURI, time);
41 | }
42 | }
43 | }
44 |
45 | @Override
46 | public void destroy() {
47 | // empty
48 | }
49 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/helper/Constants.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.helper;
2 |
3 | public final class Constants {
4 |
5 | public static String[] SUPPORTED_SINGLE_FILES = {
6 | // Archive extensions
7 | Formats.SEVEN_ZIP,
8 | Formats.RAR,
9 | Formats.ZIP,
10 | Formats.CB7,
11 | Formats.CBR,
12 | Formats.CBZ,
13 |
14 | // Book extensions
15 | Formats.EPUB,
16 | Formats.FB2,
17 | Formats.PDF,
18 | Formats.DJVU
19 | };
20 |
21 | public static class Hashes {
22 | public static final String ARCHIVE_HASH_TAG = "atsumeru";
23 | public static final String SERIE_HASH_TAG = "atsumeru-serie";
24 |
25 | public static final String ATTRIBUTE_HASH = "user:atsumeru_hash";
26 | public static final String ATTRIBUTE_SERIE_HASH = "user:atsumeru_serie_hash";
27 | }
28 |
29 | public static class Formats {
30 | // Images
31 | public static final String BMP = "bmp";
32 | public static final String JPG = "jpg";
33 | public static final String JPEG = "jpeg";
34 | public static final String PNG = "png";
35 | public static final String GIF = "gif";
36 | public static final String WEBP = "webp";
37 | public static final String AVIF = "avif";
38 | public static final String HEIC = "heic";
39 | public static final String HEIF = "heif";
40 |
41 | // Archives
42 | public static final String SEVEN_ZIP = "7z";
43 | public static final String RAR = "rar";
44 | public static final String ZIP = "zip";
45 | public static final String CB7 = "cb7";
46 | public static final String CBR = "cbr";
47 | public static final String CBZ = "cbz";
48 |
49 | // Books
50 | public static final String EPUB = "epub";
51 | public static final String FB2 = "fb2";
52 | public static final String PDF = "pdf";
53 | public static final String DJVU = "djvu";
54 |
55 | // Json
56 | public static final String JSON = "json";
57 | }
58 |
59 | public static class MimeTypes {
60 | public static final String IMAGE_JPG = "image/jpeg";
61 | public static final String IMAGE_JPEG = "image/jpeg";
62 | public static final String IMAGE_PNG = "image/png";
63 | public static final String IMAGE_GIF = "image/gif";
64 | public static final String IMAGE_WEBP = "image/webp";
65 | public static final String IMAGE_AVIF = "image/avif";
66 | public static final String IMAGE_HEIC = "image/heic";
67 | public static final String IMAGE_HEIF = "image/heif";
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/helper/ExternalIpChecker.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.helper;
2 |
3 | import xyz.atsumeru.web.util.FileUtils;
4 |
5 | import java.io.BufferedReader;
6 | import java.io.InputStreamReader;
7 | import java.net.URL;
8 |
9 | public class ExternalIpChecker {
10 | private static final String AMAZON_IP_CHECKER_URL = "http://checkip.amazonaws.com";
11 |
12 | public static String getExternalIp() {
13 | BufferedReader in = null;
14 | try {
15 | URL url = new URL(AMAZON_IP_CHECKER_URL);
16 | in = new BufferedReader(new InputStreamReader(url.openStream()));
17 | return in.readLine();
18 | } catch (Exception ex) {
19 | return null;
20 | } finally {
21 | FileUtils.closeLoudly(in);
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/helper/HashHelper.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.helper;
2 |
3 | import xyz.atsumeru.web.util.StringUtils;
4 |
5 | public class HashHelper {
6 | public static final String VAR_SCHEME = "scheme";
7 |
8 | public static String getMHash2(String hashTag, String link) {
9 | String hash = getUriHash2(hashTag, link);
10 | return hashTag != null ? hashTag + hash : hash;
11 | }
12 |
13 | public static String getHost(String link) {
14 | final int scheme = link.indexOf("://");
15 | if (scheme < 0) {
16 | return null;
17 | }
18 | final int start = scheme + 3;
19 | int end = link.indexOf(47, start);
20 | if (end < 0) {
21 | end = link.length();
22 | }
23 | return link.substring(start, end);
24 | }
25 |
26 | private static String getPath(String link) {
27 | int q = link.indexOf("?");
28 | int h = link.indexOf("#");
29 | int end;
30 | if (q < 0 && h < 0) {
31 | end = link.length();
32 | } else if (q >= 0 && h >= 0) {
33 | end = Math.min(q, h);
34 | } else if (q >= 0) {
35 | end = q;
36 | } else {
37 | end = h;
38 | }
39 | int scheme = link.contains(VAR_SCHEME) ? link.indexOf("//") : link.indexOf("://");
40 | int start = 0;
41 | if (scheme >= 0) {
42 | start = link.indexOf(47, scheme + 3);
43 | }
44 | if (start < 0) {
45 | return null;
46 | }
47 | return link.substring(start, end);
48 | }
49 |
50 | private static String getUriHash2(String hashTag, String link) {
51 | String path = getPath(link);
52 | if (path == null) {
53 | return StringUtils.md5Hex(link);
54 | }
55 | path = path.replace("//", "/");
56 | return hashTag == null ? StringUtils.md5Hex(path) : StringUtils.md5Hex(hashTag + path);
57 | }
58 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/helper/ImageHelper.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.helper;
2 |
3 | import java.awt.*;
4 | import java.awt.image.BufferedImage;
5 |
6 | public class ImageHelper {
7 |
8 | public static BufferedImage toBufferedImage(Image image) {
9 | if (image instanceof BufferedImage bufferedImage) {
10 | return bufferedImage;
11 | }
12 |
13 | // Create a buffered image with transparency
14 | BufferedImage bufferedImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_RGB);
15 |
16 | // Draw the image on to the buffered image
17 | Graphics2D bGr = bufferedImage.createGraphics();
18 | bGr.drawImage(image, 0, 0, null);
19 | bGr.dispose();
20 |
21 | // Return the buffered image
22 | return bufferedImage;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/helper/JSONLogHelper.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.helper;
2 |
3 | import org.json.JSONException;
4 | import org.json.JSONObject;
5 | import xyz.atsumeru.web.util.ArrayUtils;
6 | import xyz.atsumeru.web.util.StringUtils;
7 |
8 | import java.util.Collection;
9 |
10 | public class JSONLogHelper {
11 |
12 | public static void putJSON(JSONObject obj, String name, Collection> collection) throws JSONException {
13 | if (ArrayUtils.isNotEmpty(collection)) {
14 | obj.put(name, collection);
15 | }
16 | }
17 |
18 | public static > void putJSON(JSONObject obj, String name, E e) throws JSONException {
19 | if (e != null) {
20 | obj.put(name, e.toString());
21 | }
22 | }
23 |
24 | public static void putJSON(JSONObject obj, String name, String value) throws JSONException {
25 | if (!StringUtils.isEmpty(value)) {
26 | obj.put(name, value);
27 | }
28 | }
29 |
30 | public static void putJSON(JSONObject obj, String name, int value) throws JSONException {
31 | if (value > 0) {
32 | obj.put(name, value);
33 | }
34 | }
35 |
36 | public static void putJSON(JSONObject obj, String name, long value) throws JSONException {
37 | if (value > 0) {
38 | obj.put(name, value);
39 | }
40 | }
41 |
42 | public static void putJSON(JSONObject obj, String name, float value) throws JSONException {
43 | if (value > 0) {
44 | obj.put(name, value);
45 | }
46 | }
47 |
48 | public static void putJSON(JSONObject obj, String name, boolean value) throws JSONException {
49 | obj.put(name, value);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/helper/JavaHelper.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.helper;
2 |
3 | import lombok.Getter;
4 | import lombok.Setter;
5 |
6 | import java.io.PrintWriter;
7 | import java.io.StringWriter;
8 | import java.net.JarURLConnection;
9 | import java.text.SimpleDateFormat;
10 |
11 | public class JavaHelper {
12 | private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyy-MM-dd");
13 | @Getter
14 | @Setter
15 | private static boolean isDebug = false;
16 |
17 | public static boolean isWindows() {
18 | String os = System.getProperty("os.name").toLowerCase();
19 | return os.contains("win");
20 | }
21 |
22 | public static boolean isMac() {
23 | String os = System.getProperty("os.name").toLowerCase();
24 | return os.contains("mac");
25 | }
26 |
27 | public static boolean isUnix() {
28 | String os = System.getProperty("os.name").toLowerCase();
29 | return os.contains("nix") || os.contains("nux");
30 | }
31 |
32 | public static boolean isAndroid() {
33 | boolean isAndroid;
34 | try {
35 | Class.forName("android.app.Activity");
36 | isAndroid = true;
37 | } catch (ClassNotFoundException e) {
38 | isAndroid = false;
39 | }
40 | return isAndroid;
41 | }
42 |
43 | public static String getAppVersion(Class> cls) {
44 | String jarVersion = cls.getPackage().getImplementationVersion();
45 | String result;
46 | if (jarVersion != null && jarVersion.length() > 0) {
47 | result = jarVersion;
48 | } else {
49 | result = "debug";
50 | }
51 | try {
52 | String rn = cls.getName().replace('.', '/') + ".class";
53 | JarURLConnection j = (JarURLConnection)ClassLoader.getSystemResource(rn).openConnection();
54 | long time = j.getJarFile().getEntry("META-INF/MANIFEST.MF").getTime();
55 | return result + "-" + JavaHelper.SIMPLE_DATE_FORMAT.format(time);
56 | }
57 | catch (Exception e) {
58 | return result;
59 | }
60 | }
61 |
62 | public static String stackTraceToString(Exception e) {
63 | StringWriter sw = new StringWriter();
64 | try (PrintWriter pw = new PrintWriter(sw)) {
65 | e.printStackTrace(pw);
66 | return sw.toString();
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/helper/RestHelper.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.helper;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.http.ResponseEntity;
5 | import xyz.atsumeru.web.model.AtsumeruMessage;
6 |
7 | public class RestHelper {
8 |
9 | public static ResponseEntity createResponseMessage(String message, HttpStatus status) {
10 | return new ResponseEntity<>(new AtsumeruMessage(status.value(), message), status);
11 | }
12 |
13 | public static ResponseEntity createResponseMessage(String message, int code, HttpStatus status) {
14 | return new ResponseEntity<>(new AtsumeruMessage(code, message), status);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/helper/ServerHelper.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.helper;
2 |
3 | import jakarta.servlet.http.HttpServletRequest;
4 | import org.springframework.core.env.Environment;
5 | import xyz.atsumeru.web.util.StringUtils;
6 |
7 | import java.net.InetAddress;
8 | import java.net.NetworkInterface;
9 | import java.net.SocketException;
10 | import java.util.Enumeration;
11 | import java.util.Optional;
12 | import java.util.function.Predicate;
13 |
14 | public class ServerHelper {
15 | private static final String DEFAULT_PORT = "8080";
16 | private static final Predicate NOT_SUPPORTED_ADDRESS =
17 | inetAddress -> inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress() || inetAddress.isMulticastAddress();
18 |
19 | public static String getPort(Environment environment) {
20 | return Optional.ofNullable(environment.getProperty("server.port"))
21 | .filter(StringUtils::isNotEmpty)
22 | .orElse(DEFAULT_PORT);
23 | }
24 |
25 | private static InetAddress getInetAddress() throws SocketException {
26 | Enumeration iterNetwork;
27 | Enumeration iterAddress;
28 | NetworkInterface network;
29 | InetAddress address;
30 |
31 | iterNetwork = NetworkInterface.getNetworkInterfaces();
32 | while (iterNetwork.hasMoreElements()) {
33 | network = iterNetwork.nextElement();
34 |
35 | if (!network.isUp()) {
36 | continue;
37 | }
38 |
39 | if (network.isLoopback()) {
40 | continue;
41 | }
42 |
43 | iterAddress = network.getInetAddresses();
44 | while (iterAddress.hasMoreElements()) {
45 | address = iterAddress.nextElement();
46 |
47 | if (NOT_SUPPORTED_ADDRESS.test(address)) {
48 | continue;
49 | }
50 |
51 | return address;
52 | }
53 | }
54 | return null;
55 | }
56 |
57 | public static String getLocalAddress() {
58 | try {
59 | return Optional.ofNullable(getInetAddress())
60 | .map(InetAddress::getHostAddress)
61 | .orElse(null);
62 | } catch (SocketException ex) {
63 | return null;
64 | }
65 | }
66 |
67 | public static String getLocalHostName() {
68 | try {
69 | return Optional.ofNullable(getInetAddress())
70 | .map(InetAddress::getHostName)
71 | .orElse(null);
72 | } catch (SocketException ex) {
73 | return null;
74 | }
75 | }
76 |
77 | public static String getRemoteAddress() {
78 | return InetAddress.getLoopbackAddress().getHostAddress();
79 | }
80 |
81 | public static String getRemoteHostName() {
82 | return InetAddress.getLoopbackAddress().getHostName();
83 | }
84 |
85 | public static String getExternalAddress() {
86 | return ExternalIpChecker.getExternalIp();
87 | }
88 |
89 | public static String getRequestedRelativeURL(HttpServletRequest req) {
90 | String contextPath = req.getContextPath(); // /mywebapp
91 | String servletPath = req.getServletPath(); // /servlet/MyServlet
92 | String pathInfo = req.getPathInfo(); // /a/b;c=123
93 | String queryString = req.getQueryString(); // d=789
94 |
95 | // Reconstruct original requesting URL
96 | StringBuilder url = new StringBuilder();
97 | url.append(contextPath).append(servletPath);
98 |
99 | if (pathInfo != null) {
100 | url.append(pathInfo);
101 | }
102 | if (queryString != null) {
103 | url.append("?").append(queryString);
104 | }
105 |
106 | return url.toString();
107 | }
108 |
109 | public static String getRequestedURLPath(HttpServletRequest req) {
110 | String contextPath = req.getContextPath(); // /mywebapp
111 | String servletPath = req.getServletPath(); // /servlet/MyServlet
112 | String pathInfo = req.getPathInfo(); // /a/b;c=123
113 |
114 | // Reconstruct original requesting URL
115 | StringBuilder url = new StringBuilder();
116 | url.append(contextPath).append(servletPath);
117 |
118 | if (pathInfo != null) {
119 | url.append(pathInfo);
120 | }
121 | return url.toString();
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/helper/ValuesMapper.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.helper;
2 |
3 | import xyz.atsumeru.web.model.book.BaseBook;
4 |
5 | import java.util.Optional;
6 |
7 | public class ValuesMapper {
8 |
9 | public static String getMangaValue(BaseBook baseBook, String name, boolean lowerCaseArrays) {
10 | return switch (name) {
11 | case "title" -> baseBook.getTitle();
12 | case "alt_title", "alternative_title" -> baseBook.getAltTitle();
13 | case "jap_title" -> baseBook.getJapTitle();
14 | case "korean_title" -> baseBook.getKorTitle();
15 | case "link" -> baseBook.getContentLink();
16 | case "ihash" -> baseBook.getContentId();
17 | case "author", "authors" -> baseBook.getAuthors();
18 | case "summary" -> baseBook.getDescription();
19 | case "event", "events" -> baseBook.getEvent();
20 | case "publisher", "publishers" -> baseBook.getPublisher();
21 | case "production_year", "year", "years" -> baseBook.getYear();
22 | case "country", "countries" -> baseBook.getCountry();
23 | case "language", "languages" -> Optional.ofNullable(baseBook.getLanguage())
24 | .map(value -> lowerCaseArrays ? value.toLowerCase() : value)
25 | .orElse(baseBook.getLanguage());
26 | case "artists" -> Optional.ofNullable(baseBook.getArtists())
27 | .map(value -> lowerCaseArrays ? value.toLowerCase() : value)
28 | .orElse(baseBook.getArtists());
29 | case "translators" -> Optional.ofNullable(baseBook.getTranslators())
30 | .map(value -> lowerCaseArrays ? value.toLowerCase() : value)
31 | .orElse(baseBook.getTranslators());
32 | case "volume" -> String.valueOf(baseBook.getVolume());
33 | case "rating" -> String.valueOf(baseBook.getRating());
34 | case "score" -> baseBook.getScore();
35 | case "is_mature" -> String.valueOf(baseBook.getIsMature());
36 | case "is_adult" -> String.valueOf(baseBook.getIsAdult());
37 | case "censorship" -> String.valueOf(baseBook.getCensorship().toString());
38 | case "color" -> String.valueOf(baseBook.getColor().toString());
39 | case "status" -> baseBook.getStatus().name();
40 | case "translation_status" -> baseBook.getTranslationStatus().name();
41 | case "plot_type" -> baseBook.getPlotType().name();
42 | case "content_type" -> baseBook.getContentType().name();
43 | case "cover" -> baseBook.getCover();
44 | case "genres", "genre" -> Optional.ofNullable(baseBook.getGenres())
45 | .map(value -> lowerCaseArrays ? value.toLowerCase() : value)
46 | .orElse(baseBook.getGenres());
47 | case "tags" -> Optional.ofNullable(baseBook.getTags())
48 | .map(value -> lowerCaseArrays ? value.toLowerCase() : value)
49 | .orElse(baseBook.getTags());
50 | case "series" -> Optional.ofNullable(baseBook.getSeries())
51 | .map(value -> lowerCaseArrays ? value.toLowerCase() : value)
52 | .orElse(baseBook.getSeries());
53 | case "parodies" -> Optional.ofNullable(baseBook.getParodies())
54 | .map(value -> lowerCaseArrays ? value.toLowerCase() : value)
55 | .orElse(baseBook.getParodies());
56 | case "circles" -> Optional.ofNullable(baseBook.getCircles())
57 | .map(value -> lowerCaseArrays ? value.toLowerCase() : value)
58 | .orElse(baseBook.getCircles());
59 | case "magazines" -> Optional.ofNullable(baseBook.getMagazines())
60 | .map(value -> lowerCaseArrays ? value.toLowerCase() : value)
61 | .orElse(baseBook.getMagazines());
62 | case "characters" -> Optional.ofNullable(baseBook.getCharacters())
63 | .map(value -> lowerCaseArrays ? value.toLowerCase() : value)
64 | .orElse(baseBook.getCharacters());
65 | case "volumes", "volumes_count" -> String.valueOf(baseBook.getVolumesCount());
66 | case "chapters", "chapters_count" -> String.valueOf(baseBook.getChaptersCount());
67 | default -> null;
68 | };
69 | }
70 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/importer/listener/OnImportCallback.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.importer.listener;
2 |
3 | public interface OnImportCallback {
4 | void onProgressChanged(int count, int total);
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/interceptor/InterceptorRegistry.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.interceptor;
2 |
3 | import org.springframework.stereotype.Component;
4 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
5 |
6 | @Component
7 | public class InterceptorRegistry implements WebMvcConfigurer {
8 | private final RequestLogInterceptor requestLogInterceptor;
9 |
10 | public InterceptorRegistry(RequestLogInterceptor requestLogInterceptor) {
11 | this.requestLogInterceptor = requestLogInterceptor;
12 | }
13 |
14 | @Override
15 | public void addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) {
16 | registry.addInterceptor(requestLogInterceptor);
17 | }
18 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/io/image/ImageOutStreamWriter.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.io.image;
2 |
3 | import jakarta.servlet.http.HttpServletResponse;
4 | import org.springframework.lang.Nullable;
5 |
6 | import java.io.File;
7 | import java.io.FileOutputStream;
8 | import java.io.OutputStream;
9 |
10 | public interface ImageOutStreamWriter {
11 |
12 | default void write(File outputFile, int page) {
13 | try (OutputStream out = new FileOutputStream(outputFile)) {
14 | write(null, out, page, false);
15 | } catch (Exception ex) {
16 | outputFile.delete();
17 | }
18 | }
19 |
20 | void write(@Nullable HttpServletResponse response, OutputStream responseOut, int page, boolean convertImage);
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/io/image/ImageOutStreamWriterFactory.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.io.image;
2 |
3 | import org.springframework.lang.Nullable;
4 | import xyz.atsumeru.web.exception.NoReadableFoundException;
5 | import xyz.atsumeru.web.io.image.impl.ArchivedImageOutStreamWriter;
6 | import xyz.atsumeru.web.io.image.impl.RenderedImageOutStreamWriter;
7 | import xyz.atsumeru.web.model.book.BookArchive;
8 | import xyz.atsumeru.web.model.book.IBaseBookItem;
9 | import xyz.atsumeru.web.repository.BooksRepository;
10 | import xyz.atsumeru.web.util.StringUtils;
11 |
12 | import java.util.Optional;
13 |
14 | public class ImageOutStreamWriterFactory {
15 |
16 | public static ImageOutStreamWriter create(String archiveHash, @Nullable String chapterHash) {
17 | if (StringUtils.isEmpty(archiveHash) && StringUtils.isNotEmpty(chapterHash) || BooksRepository.isArchiveHash(archiveHash)) {
18 | IBaseBookItem baseBookItem = findArchive(archiveHash, chapterHash);
19 |
20 | return baseBookItem instanceof BookArchive archive && archive.isBook()
21 | ? new RenderedImageOutStreamWriter(baseBookItem)
22 | : new ArchivedImageOutStreamWriter(baseBookItem, chapterHash);
23 | }
24 | throw new NoReadableFoundException();
25 | }
26 |
27 | private static IBaseBookItem findArchive(@Nullable String archiveHash, @Nullable String chapterHash) {
28 | return BooksRepository.getBookDetails(
29 | Optional.ofNullable(archiveHash)
30 | .filter(StringUtils::isNotEmpty)
31 | .orElseGet(() -> BooksRepository.getChapter(chapterHash).getArchiveId()));
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/io/image/impl/ArchivedImageOutStreamWriter.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.io.image.impl;
2 |
3 | import jakarta.servlet.http.HttpServletResponse;
4 | import org.apache.catalina.connector.ClientAbortException;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.springframework.lang.Nullable;
8 | import xyz.atsumeru.web.archive.ArchiveReader;
9 | import xyz.atsumeru.web.archive.iterator.IArchiveIterator;
10 | import xyz.atsumeru.web.exception.ArchiveReadingException;
11 | import xyz.atsumeru.web.exception.PageNotFoundException;
12 | import xyz.atsumeru.web.helper.FilesHelper;
13 | import xyz.atsumeru.web.io.image.ImageOutStreamWriter;
14 | import xyz.atsumeru.web.model.book.IBaseBookItem;
15 | import xyz.atsumeru.web.repository.BooksRepository;
16 | import xyz.atsumeru.web.util.AppUtils;
17 | import xyz.atsumeru.web.util.ArrayUtils;
18 | import xyz.atsumeru.web.util.StringUtils;
19 |
20 | import java.io.IOException;
21 | import java.io.OutputStream;
22 | import java.util.List;
23 |
24 | public class ArchivedImageOutStreamWriter implements ImageOutStreamWriter {
25 | private static final Logger logger = LoggerFactory.getLogger(ArchivedImageOutStreamWriter.class.getSimpleName());
26 |
27 | private final IBaseBookItem baseBookItem;
28 | private final String chapterHash;
29 |
30 | public ArchivedImageOutStreamWriter(IBaseBookItem baseBookItem, String chapterHash) {
31 | this.baseBookItem = baseBookItem;
32 | this.chapterHash = chapterHash;
33 | }
34 |
35 | @Override
36 | public void write(HttpServletResponse response, OutputStream responseOut, int page, boolean convertImage) {
37 | tryWrite(response, responseOut, page, convertImage, 1);
38 | }
39 |
40 | private void tryWrite(HttpServletResponse response, OutputStream responseOut, int page, boolean convertImage, int tryCount) {
41 | long time = System.currentTimeMillis();
42 |
43 | try (IArchiveIterator archiveIterator = ArchiveReader.getArchiveIterator(baseBookItem.getFolder())) {
44 | List pages = StringUtils.isNotEmpty(chapterHash)
45 | ? BooksRepository.getChapter(chapterHash).getPageEntryNames()
46 | : baseBookItem.getPageEntryNames();
47 |
48 | if (!writePageIntoResponse(response, responseOut, archiveIterator, pages, page, convertImage, time)) {
49 | throw new PageNotFoundException();
50 | }
51 | } catch (Exception ex) {
52 | if ((ex.getMessage().equals("Stream closed") || ex instanceof ClientAbortException) && tryCount < 5) {
53 | AppUtils.sleepThread(1000);
54 | tryWrite(response, responseOut, page, convertImage, ++tryCount);
55 | } else {
56 | ex.printStackTrace();
57 | throw new ArchiveReadingException();
58 | }
59 | }
60 | }
61 |
62 | private boolean writePageIntoResponse(@Nullable HttpServletResponse response, OutputStream outputStream, IArchiveIterator archiveIterator,
63 | List pages, int page, boolean convertImage, long time) throws IOException {
64 | if (ArrayUtils.isNotEmpty(pages) && pages.size() >= page) {
65 | return FilesHelper.writeEntryStreamIntoResponseOrOutputStream(
66 | logger,
67 | response,
68 | outputStream,
69 | archiveIterator.getEntryInputStreamByName(pages.get(page - 1)),
70 | archiveIterator.getEntrySize(),
71 | archiveIterator.getEntryName(),
72 | convertImage,
73 | time
74 | );
75 | }
76 | return false;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/io/image/impl/RenderedImageOutStreamWriter.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.io.image.impl;
2 |
3 | import jakarta.servlet.http.HttpServletResponse;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.lang.Nullable;
7 | import xyz.atsumeru.web.enums.BookType;
8 | import xyz.atsumeru.web.helper.Constants;
9 | import xyz.atsumeru.web.io.image.ImageOutStreamWriter;
10 | import xyz.atsumeru.web.model.book.IBaseBookItem;
11 | import xyz.atsumeru.web.renderer.AbstractRenderer;
12 | import xyz.atsumeru.web.renderer.RendererFactory;
13 | import xyz.atsumeru.web.util.ContentDetector;
14 | import xyz.atsumeru.web.util.FileUtils;
15 |
16 | import javax.imageio.ImageIO;
17 | import java.awt.image.BufferedImage;
18 | import java.io.ByteArrayOutputStream;
19 | import java.io.IOException;
20 | import java.io.OutputStream;
21 | import java.nio.file.Paths;
22 |
23 | public class RenderedImageOutStreamWriter implements ImageOutStreamWriter {
24 | private static final Logger logger = LoggerFactory.getLogger(RenderedImageOutStreamWriter.class.getSimpleName());
25 |
26 | private final IBaseBookItem baseBookItem;
27 |
28 | public RenderedImageOutStreamWriter(IBaseBookItem baseBookItem) {
29 | this.baseBookItem = baseBookItem;
30 | }
31 |
32 | @Override
33 | public void write(@Nullable HttpServletResponse response, OutputStream responseOut, int page, boolean convertImage) {
34 | long time = System.currentTimeMillis();
35 |
36 | BookType bookType = ContentDetector.detectBookType(Paths.get(baseBookItem.getFolder()));
37 | AbstractRenderer renderer = RendererFactory.create(bookType, baseBookItem.getFolder());
38 | BufferedImage bufferedImage = renderer.renderPage(page, renderer.getScaleOrDpi());
39 | writeBufferedImageIntoResponseOrOutputStream(response, responseOut, bufferedImage, time);
40 | }
41 |
42 | private void writeBufferedImageIntoResponseOrOutputStream(@Nullable HttpServletResponse response, OutputStream outputStream,
43 | BufferedImage bufferedImage, long timeStart) {
44 | try {
45 | ByteArrayOutputStream tmp = new ByteArrayOutputStream();
46 | ImageIO.write(bufferedImage, Constants.Formats.JPEG, tmp);
47 | FileUtils.closeLoudly(tmp);
48 |
49 | int contentLength = tmp.size();
50 | if (response != null) {
51 | response.setContentType(Constants.MimeTypes.IMAGE_JPEG);
52 | response.setContentLength(contentLength);
53 | }
54 | ImageIO.write(bufferedImage, Constants.Formats.JPEG, outputStream);
55 | if (response != null) {
56 | logger.info("Image unpacking and writing time: " + (System.currentTimeMillis() - timeStart) + "ms. Image length: " + contentLength + " bytes");
57 | }
58 | } catch (IOException ex) {
59 | logger.error("Unable to write image", ex);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/json/adapter/AdminFieldAdapter.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.json.adapter;
2 |
3 | import com.google.gson.*;
4 | import org.springframework.security.core.Authentication;
5 | import org.springframework.security.core.context.SecurityContextHolder;
6 | import xyz.atsumeru.web.security.service.UsersDetailsService;
7 |
8 | import java.lang.reflect.Type;
9 |
10 | public class AdminFieldAdapter implements JsonSerializer, JsonDeserializer {
11 |
12 | @Override
13 | public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) {
14 | Authentication auth = SecurityContextHolder.getContext().getAuthentication();
15 | return UsersDetailsService.isUserInRole(auth, "ADMIN") ? context.serialize(src) : null;
16 | }
17 |
18 | @Override
19 | public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
20 | return null;
21 | }
22 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/json/adapter/CategoriesFieldAdapter.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.json.adapter;
2 |
3 | import com.google.gson.JsonArray;
4 | import com.google.gson.JsonElement;
5 | import com.google.gson.JsonSerializationContext;
6 | import xyz.atsumeru.web.model.database.Category;
7 | import xyz.atsumeru.web.repository.CategoryRepository;
8 | import xyz.atsumeru.web.util.ArrayUtils;
9 | import xyz.atsumeru.web.util.StringUtils;
10 | import xyz.atsumeru.web.util.TypeUtils;
11 |
12 | import java.lang.reflect.Type;
13 |
14 | public class CategoriesFieldAdapter extends StringListBidirectionalAdapter {
15 |
16 | @Override
17 | public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) {
18 | if (StringUtils.isEmpty(src)) {
19 | return null;
20 | }
21 |
22 | String[] array = src.split(",");
23 | if (ArrayUtils.isEmpty(array)) {
24 | return null;
25 | }
26 |
27 | JsonArray jsonArray = new JsonArray();
28 | for (String value : array) {
29 | long categoryDbId = TypeUtils.getLongDef(CategoryRepository.getRealIdFromCategoryDbId(value), -1);
30 | Category category = CategoryRepository.getCategoryByDbId(categoryDbId);
31 | if (category != null) {
32 | jsonArray.add(category.getCategoryId());
33 | }
34 | }
35 |
36 | return jsonArray;
37 | }
38 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/json/adapter/LinksBidirectionalAdapter.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.json.adapter;
2 |
3 | import com.google.gson.*;
4 | import xyz.atsumeru.web.util.ArrayUtils;
5 | import xyz.atsumeru.web.util.LinkUtils;
6 | import xyz.atsumeru.web.util.StringUtils;
7 |
8 | import java.lang.reflect.Type;
9 | import java.util.ArrayList;
10 | import java.util.Arrays;
11 | import java.util.List;
12 |
13 | public class LinksBidirectionalAdapter implements JsonSerializer, JsonDeserializer {
14 |
15 | @Override
16 | public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) {
17 | if (StringUtils.isEmpty(src)) {
18 | return null;
19 | }
20 |
21 | String[] array = src.split(",");
22 | if (ArrayUtils.isEmpty(array)) {
23 | return null;
24 | }
25 |
26 | JsonArray jsonArray = new JsonArray();
27 |
28 | Arrays.stream(array)
29 | .filter(StringUtils::isNotEmpty)
30 | .forEach(link -> {
31 | JsonObject links = new JsonObject();
32 | links.addProperty("source", LinkUtils.getHostName(link));
33 | links.addProperty("link", link);
34 | jsonArray.add(links);
35 | });
36 |
37 | return jsonArray;
38 | }
39 |
40 | @Override
41 | public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
42 | List list = new ArrayList<>();
43 | if (json.isJsonArray()) {
44 | json.getAsJsonArray().forEach(it -> list.add(it.getAsJsonObject().get("link").getAsString()));
45 | }
46 |
47 | return StringUtils.join(",", list);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/json/adapter/OmitEmptyStringsAdapter.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.json.adapter;
2 |
3 | import com.google.gson.JsonElement;
4 | import com.google.gson.JsonPrimitive;
5 | import com.google.gson.JsonSerializationContext;
6 | import com.google.gson.JsonSerializer;
7 | import xyz.atsumeru.web.util.StringUtils;
8 |
9 | import java.lang.reflect.Type;
10 |
11 | public class OmitEmptyStringsAdapter implements JsonSerializer {
12 |
13 | @Override
14 | public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) {
15 | return StringUtils.isNotEmpty(src) ? new JsonPrimitive(src) : null;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/json/adapter/StringListBidirectionalAdapter.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.json.adapter;
2 |
3 | import com.google.gson.*;
4 | import xyz.atsumeru.web.util.ArrayUtils;
5 | import xyz.atsumeru.web.util.StringUtils;
6 |
7 | import java.lang.reflect.Type;
8 | import java.util.ArrayList;
9 | import java.util.List;
10 |
11 | public class StringListBidirectionalAdapter implements JsonSerializer, JsonDeserializer {
12 |
13 | @Override
14 | public JsonElement serialize(String src, Type typeOfSrc, JsonSerializationContext context) {
15 | if (StringUtils.isEmpty(src)) {
16 | return null;
17 | }
18 |
19 | String[] array = src.split(",");
20 | if (ArrayUtils.isEmpty(array)) {
21 | return null;
22 | }
23 |
24 | JsonArray jsonArray = new JsonArray();
25 | for (String value : array) {
26 | jsonArray.add(value);
27 | }
28 |
29 | return jsonArray;
30 | }
31 |
32 | @Override
33 | public String deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
34 | List list = new ArrayList<>();
35 | if (json.isJsonArray()) {
36 | json.getAsJsonArray().forEach(it -> list.add(it.getAsString()));
37 | }
38 |
39 | return StringUtils.join(",", list);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/json/annotation/Exclude.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.json.annotation;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | @Retention(RetentionPolicy.RUNTIME)
9 | @Target(ElementType.FIELD)
10 | public @interface Exclude {}
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/logger/FileLogger.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.logger;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.util.logging.FileHandler;
6 | import java.util.logging.Formatter;
7 | import java.util.logging.LogRecord;
8 | import java.util.logging.Logger;
9 |
10 | public class FileLogger {
11 |
12 | public static Logger createLogger(String loggerName, File file) {
13 | Logger logger = Logger.getLogger(loggerName);
14 |
15 | try {
16 | // This block configure the logger with handler and formatter
17 | FileHandler fileHandler = new FileHandler(file.getAbsolutePath(), true);
18 | logger.addHandler(fileHandler);
19 | Formatter formatter = new Formatter() {
20 | @Override
21 | public String format(LogRecord record) {
22 | return String.valueOf(record.getLevel()) + ':' + record.getMessage() + '\n';
23 | }
24 | };
25 | fileHandler.setFormatter(formatter);
26 |
27 | logger.setUseParentHandlers(false);
28 | } catch (SecurityException | IOException e) {
29 | e.printStackTrace();
30 | }
31 |
32 | return logger;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/manager/ImageCache.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.manager;
2 |
3 | import lombok.Getter;
4 | import net.coobird.thumbnailator.Thumbnails;
5 | import net.coobird.thumbnailator.resizers.configurations.ScalingMode;
6 | import org.apache.commons.io.IOUtils;
7 | import xyz.atsumeru.web.exception.NoCoverFoundException;
8 | import xyz.atsumeru.web.helper.Constants;
9 | import xyz.atsumeru.web.helper.FilesHelper;
10 | import xyz.atsumeru.web.io.image.ImageOutStreamWriterFactory;
11 | import xyz.atsumeru.web.model.book.chapter.BookChapter;
12 | import xyz.atsumeru.web.model.book.image.Images;
13 | import xyz.atsumeru.web.repository.BooksRepository;
14 | import xyz.atsumeru.web.util.FileUtils;
15 |
16 | import javax.imageio.ImageIO;
17 | import java.awt.image.BufferedImage;
18 | import java.io.File;
19 | import java.io.FileInputStream;
20 | import java.io.IOException;
21 | import java.io.InputStream;
22 |
23 | public class ImageCache {
24 | private ImageCache() {
25 | }
26 |
27 | public static boolean isInCache(String imageHash, ImageCacheType cacheType) {
28 | return getImage(imageHash, cacheType).exists();
29 | }
30 |
31 | public static File getImage(String imageHash, ImageCacheType cacheType) {
32 | return getImage(imageHash, Constants.Formats.PNG, cacheType);
33 | }
34 |
35 | public static File getImage(String imageHash, String extension, ImageCacheType cacheType) {
36 | return new File(new File(Workspace.CACHE_DIR, cacheType.getFolder()), String.format("%s.%s", imageHash, extension));
37 | }
38 |
39 | public static byte[] getImageBytesFromCache(String imageHash, ImageCache.ImageCacheType cacheType) {
40 | File image = ImageCache.getImage(imageHash, cacheType);
41 | try (FileInputStream fis = new FileInputStream(image)) {
42 | return IOUtils.toByteArray(fis);
43 | } catch (IOException e) {
44 | throw new NoCoverFoundException();
45 | }
46 | }
47 |
48 | public static boolean saveImageIntoCache(String imageHash) {
49 | return BooksRepository.isChapterHash(imageHash)
50 | ? saveChapterImageIntoCache(imageHash)
51 | : FilesHelper.saveBookImageIntoCache(imageHash);
52 | }
53 |
54 | private static boolean saveChapterImageIntoCache(String imageHash) {
55 | BookChapter chapter = BooksRepository.getChapter(imageHash);
56 | File originalImage = ImageCache.getImage(imageHash, "tmp", ImageCache.ImageCacheType.THUMBNAIL);
57 | ImageOutStreamWriterFactory.create(chapter.getArchiveId(), imageHash).write(originalImage, 1);
58 |
59 | try {
60 | createThumbnail(ImageIO.read(originalImage), ImageCache.getImage(imageHash, ImageCache.ImageCacheType.THUMBNAIL));
61 | } catch (IOException e) {
62 | e.printStackTrace();
63 | return false;
64 | }
65 |
66 | originalImage.delete();
67 | return true;
68 | }
69 |
70 |
71 | public static Images saveToFile(InputStream inputStream, String imageHash, String extension) {
72 | String imageName = String.format("%s.%s", imageHash, extension);
73 |
74 | File thumbnailFolder = new File(Workspace.CACHE_DIR, ImageCacheType.THUMBNAIL.getFolder());
75 | File thumbnailImage = new File(thumbnailFolder, imageName);
76 | thumbnailFolder.mkdirs();
77 |
78 | BufferedImage bImage = null;
79 | try {
80 | createThumbnail(bImage = ImageIO.read(inputStream), thumbnailImage);
81 | } catch (IOException e) {
82 | e.printStackTrace();
83 | } finally {
84 | FileUtils.closeLoudly(inputStream);
85 | }
86 |
87 | return new Images(thumbnailImage.getPath(), bImage);
88 | }
89 |
90 | private static void createThumbnail(BufferedImage image, File thumbnailImage) {
91 | try {
92 | Thumbnails.of(image)
93 | .scalingMode(ScalingMode.PROGRESSIVE_BILINEAR)
94 | .size(230, 320)
95 | .toFile(thumbnailImage);
96 | } catch (IOException | IllegalArgumentException e) {
97 | e.printStackTrace();
98 | }
99 | }
100 |
101 | public enum ImageCacheType {
102 | ORIGINAL("original"),
103 | THUMBNAIL("thumbnail");
104 |
105 | @Getter
106 | private String folder;
107 |
108 | ImageCacheType(String folder) {
109 | this.folder = folder;
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/manager/Workspace.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.manager;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.boot.system.ApplicationHome;
6 | import org.springframework.stereotype.Component;
7 | import xyz.atsumeru.web.AtsumeruApplication;
8 |
9 | import java.io.File;
10 | import java.nio.file.Files;
11 | import java.util.Arrays;
12 |
13 | @Component
14 | public class Workspace {
15 | private static final Logger logger = LoggerFactory.getLogger(Workspace.class.getSimpleName());
16 |
17 | private static final String WORKING_DIR = getWorkingDir();
18 |
19 | public static final File DATABASES_DIR = new File(WORKING_DIR, "database");
20 | public static final File CONFIG_DIR = new File(WORKING_DIR, "config");
21 | public static final File LOGS_DIR = new File(WORKING_DIR, "logs");
22 | public static final File CACHE_DIR = new File(WORKING_DIR, "cache");
23 | public static final File BIN_DIR = new File(WORKING_DIR, "bin");
24 | public static final File TEMP_DIR = new File(WORKING_DIR, "temp");
25 |
26 | final private static File[] FOLDERS = new File[]{
27 | DATABASES_DIR,
28 | CONFIG_DIR,
29 | LOGS_DIR,
30 | CACHE_DIR,
31 | BIN_DIR,
32 | TEMP_DIR
33 | };
34 |
35 | public Workspace() {
36 | checkWorkspace();
37 | }
38 |
39 | public static void checkWorkspace() {
40 | logger.info("Checking Workspace...");
41 | Arrays.stream(FOLDERS)
42 | .filter(folder -> !Files.isDirectory(folder.toPath()))
43 | .peek(file -> logger.info("Creating folder: {}", file))
44 | .forEach(File::mkdirs);
45 | logger.info("All set!");
46 | }
47 |
48 | private static String getWorkingDir() {
49 | return AtsumeruApplication.isInDevMode() ? getInDevModeWorkingDir() : new ApplicationHome(AtsumeruApplication.class).getDir().getAbsolutePath();
50 | }
51 |
52 | private static String getInDevModeWorkingDir() {
53 | File workDir = new File(System.getProperty("user.dir"));
54 | String workDirString = workDir.toString();
55 | if (workDir.isFile()) {
56 | workDirString = workDirString.substring(0, workDirString.lastIndexOf(File.separator));
57 | }
58 | return workDirString + File.separator;
59 | }
60 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/manager/cache/AtsumeruCache.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.manager.cache;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 | import org.springframework.beans.BeansException;
5 | import org.springframework.beans.factory.annotation.Configurable;
6 | import org.springframework.cache.Cache;
7 | import org.springframework.cache.CacheManager;
8 | import org.springframework.cache.annotation.CacheEvict;
9 | import org.springframework.cache.annotation.CachingConfigurer;
10 | import org.springframework.cache.annotation.EnableCaching;
11 | import org.springframework.cache.concurrent.ConcurrentMapCache;
12 | import org.springframework.cache.support.SimpleCacheManager;
13 | import org.springframework.context.ApplicationContext;
14 | import org.springframework.context.ApplicationContextAware;
15 | import org.springframework.context.annotation.Bean;
16 | import org.springframework.context.annotation.Configuration;
17 | import org.springframework.stereotype.Component;
18 |
19 | import java.util.Arrays;
20 | import java.util.Optional;
21 |
22 | @Component
23 | @Configurable
24 | @Configuration
25 | @EnableCaching(proxyTargetClass = true)
26 | public class AtsumeruCache implements ApplicationContextAware, CachingConfigurer {
27 | public static final String KEY_BOOKS = "books";
28 | public static final String KEY_BOOKS_BY_BOUND_SERVICE = "books_by_bound_service";
29 | public static final String KEY_FILTERS = "filters";
30 | public static final String KEY_HUB_UPDATES = "hub-updates";
31 | public static final String KEY_HISTORY = "history";
32 |
33 | private static ApplicationContext context;
34 |
35 | public static void evictAll() {
36 | context.getBean(AtsumeruCache.class).evictAllInternal();
37 | }
38 |
39 | @Override
40 | public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
41 | context = applicationContext;
42 | }
43 |
44 | @CacheEvict(cacheNames = {KEY_BOOKS, KEY_BOOKS_BY_BOUND_SERVICE, KEY_FILTERS, KEY_HUB_UPDATES, KEY_HISTORY}, allEntries = true)
45 | public void evictAllInternal() {
46 | CacheManager cacheManager = context.getBean(CacheManager.class);
47 | for (String name : cacheManager.getCacheNames()) {
48 | Optional.ofNullable(cacheManager.getCache(name)).ifPresent(Cache::clear);
49 | }
50 | }
51 |
52 | @Bean
53 | public CacheManager cacheManager() {
54 | SimpleCacheManager cacheManager = new SimpleCacheManager();
55 | cacheManager.setCaches(Arrays.asList(
56 | new ConcurrentMapCache(KEY_BOOKS),
57 | new ConcurrentMapCache(KEY_BOOKS_BY_BOUND_SERVICE),
58 | new ConcurrentMapCache(KEY_FILTERS),
59 | new ConcurrentMapCache(KEY_HUB_UPDATES),
60 | new ConcurrentMapCache(KEY_HISTORY)
61 | ));
62 | return cacheManager;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/manager/cache/AtsumeruRenderersCache.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.manager.cache;
2 |
3 | import com.djvu2image.DjVuBook;
4 | import com.github.benmanes.caffeine.cache.Cache;
5 | import com.github.benmanes.caffeine.cache.Caffeine;
6 | import org.apache.pdfbox.pdmodel.PDDocument;
7 | import xyz.atsumeru.web.util.FileUtils;
8 |
9 | import java.io.Closeable;
10 | import java.io.File;
11 | import java.util.concurrent.TimeUnit;
12 | import java.util.function.Function;
13 |
14 | public class AtsumeruRenderersCache {
15 | private static final Cache PDF_CACHE = Caffeine.newBuilder()
16 | .maximumSize(20)
17 | .expireAfterAccess(1, TimeUnit.MINUTES)
18 | .removalListener((file, pdf, cause) -> FileUtils.closeLoudly((Closeable) pdf))
19 | .build();
20 |
21 | private static final Cache DJVU_CACHE = Caffeine.newBuilder()
22 | .maximumSize(20)
23 | .expireAfterAccess(1, TimeUnit.MINUTES)
24 | .build();
25 |
26 | public static PDDocument getPDDocument(String filePath, Function bookFunction) {
27 | return PDF_CACHE.get(new File(filePath), bookFunction);
28 | }
29 |
30 | public static DjVuBook getDjvuBook(String filePath, Function bookFunction) {
31 | return DJVU_CACHE.get(new File(filePath), bookFunction);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/manager/fswatcher/ChangedFile.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.manager.fswatcher;
2 |
3 | import org.springframework.util.Assert;
4 | import org.springframework.util.StringUtils;
5 |
6 | import java.io.File;
7 |
8 | /**
9 | * A single file that has changed.
10 | *
11 | * @author Phillip Webb
12 | * @since 1.3.0
13 | * @see ChangedFiles
14 | */
15 | public final class ChangedFile {
16 |
17 | private final File sourceFolder;
18 |
19 | private final File file;
20 |
21 | private final Type type;
22 |
23 | /**
24 | * Create a new {@link ChangedFile} instance.
25 | * @param sourceFolder the source folder
26 | * @param file the file
27 | * @param type the type of change
28 | */
29 | public ChangedFile(File sourceFolder, File file, Type type) {
30 | Assert.notNull(sourceFolder, "SourceFolder must not be null");
31 | Assert.notNull(file, "File must not be null");
32 | Assert.notNull(type, "Type must not be null");
33 | this.sourceFolder = sourceFolder;
34 | this.file = file;
35 | this.type = type;
36 | }
37 |
38 | /**
39 | * Return the file that was changed.
40 | * @return the file
41 | */
42 | public File getFile() {
43 | return this.file;
44 | }
45 |
46 | /**
47 | * Return the type of change.
48 | * @return the type of change
49 | */
50 | public Type getType() {
51 | return this.type;
52 | }
53 |
54 | /**
55 | * Return the name of the file relative to the source folder.
56 | * @return the relative name
57 | */
58 | public String getRelativeName() {
59 | File folder = this.sourceFolder.getAbsoluteFile();
60 | File file = this.file.getAbsoluteFile();
61 | String folderName = StringUtils.cleanPath(folder.getPath());
62 | String fileName = StringUtils.cleanPath(file.getPath());
63 | Assert.state(fileName.startsWith(folderName),
64 | () -> "The file " + fileName + " is not contained in the source folder " + folderName);
65 | return fileName.substring(folderName.length() + 1);
66 | }
67 |
68 | @Override
69 | public boolean equals(Object obj) {
70 | if (obj == this) {
71 | return true;
72 | }
73 | if (obj == null) {
74 | return false;
75 | }
76 | if (obj instanceof ChangedFile other) {
77 | return this.file.equals(other.file) && this.type.equals(other.type);
78 | }
79 | return super.equals(obj);
80 | }
81 |
82 | @Override
83 | public int hashCode() {
84 | return this.file.hashCode() * 31 + this.type.hashCode();
85 | }
86 |
87 | @Override
88 | public String toString() {
89 | return this.file + " (" + this.type + ")";
90 | }
91 |
92 | /**
93 | * Change types.
94 | */
95 | public enum Type {
96 |
97 | /**
98 | * A new file has been added.
99 | */
100 | ADD,
101 |
102 | /**
103 | * An existing file has been modified.
104 | */
105 | MODIFY,
106 |
107 | /**
108 | * An existing file has been deleted.
109 | */
110 | DELETE
111 |
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/manager/fswatcher/ChangedFiles.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.manager.fswatcher;
2 |
3 | import java.io.File;
4 | import java.util.Collections;
5 | import java.util.Iterator;
6 | import java.util.Set;
7 |
8 | /**
9 | * A collections of files from a specific source folder that have changed.
10 | *
11 | * @author Phillip Webb
12 | * @since 1.3.0
13 | * @see FileChangeListener
14 | * @see ChangedFiles
15 | */
16 | public final class ChangedFiles implements Iterable {
17 |
18 | private final File sourceFolder;
19 |
20 | private final Set files;
21 |
22 | public ChangedFiles(File sourceFolder, Set files) {
23 | this.sourceFolder = sourceFolder;
24 | this.files = Collections.unmodifiableSet(files);
25 | }
26 |
27 | /**
28 | * The source folder being watched.
29 | * @return the source folder
30 | */
31 | public File getSourceFolder() {
32 | return this.sourceFolder;
33 | }
34 |
35 | @Override
36 | public Iterator iterator() {
37 | return getFiles().iterator();
38 | }
39 |
40 | /**
41 | * The files that have been changed.
42 | * @return the changed files
43 | */
44 | public Set getFiles() {
45 | return this.files;
46 | }
47 |
48 | @Override
49 | public boolean equals(Object obj) {
50 | if (obj == null) {
51 | return false;
52 | }
53 | if (obj == this) {
54 | return true;
55 | }
56 | if (obj instanceof ChangedFiles other) {
57 | return this.sourceFolder.equals(other.sourceFolder) && this.files.equals(other.files);
58 | }
59 | return super.equals(obj);
60 | }
61 |
62 | @Override
63 | public int hashCode() {
64 | return this.files.hashCode();
65 | }
66 |
67 | @Override
68 | public String toString() {
69 | return this.sourceFolder + " " + this.files;
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/manager/fswatcher/FileChangeListener.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.manager.fswatcher;
2 |
3 | import java.util.Set;
4 |
5 | /**
6 | * Callback interface when file changes are detected.
7 | *
8 | * @author Andy Clement
9 | * @author Phillip Webb
10 | * @since 1.3.0
11 | */
12 | @FunctionalInterface
13 | public interface FileChangeListener {
14 |
15 | /**
16 | * Called when files have been changed.
17 | * @param changeSet a set of the {@link ChangedFiles}
18 | */
19 | void onChange(Set changeSet);
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/manager/fswatcher/FileSnapshot.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.manager.fswatcher;
2 |
3 | import org.springframework.util.Assert;
4 |
5 | import java.io.File;
6 |
7 | /**
8 | * A snapshot of a File at a given point in time.
9 | *
10 | * @author Phillip Webb
11 | */
12 | class FileSnapshot {
13 |
14 | private final File file;
15 |
16 | private final boolean exists;
17 |
18 | private final long length;
19 |
20 | private final long lastModified;
21 |
22 | FileSnapshot(File file) {
23 | Assert.notNull(file, "File must not be null");
24 | Assert.isTrue(file.isFile() || !file.exists(), "File must not be a folder");
25 | this.file = file;
26 | this.exists = file.exists();
27 | this.length = file.length();
28 | this.lastModified = file.lastModified();
29 | }
30 |
31 | File getFile() {
32 | return this.file;
33 | }
34 |
35 | @Override
36 | public boolean equals(Object obj) {
37 | if (this == obj) {
38 | return true;
39 | }
40 | if (obj == null) {
41 | return false;
42 | }
43 | if (obj instanceof FileSnapshot other) {
44 | boolean equals = this.file.equals(other.file);
45 | equals = equals && this.exists == other.exists;
46 | equals = equals && this.length == other.length;
47 | equals = equals && this.lastModified == other.lastModified;
48 | return equals;
49 | }
50 | return super.equals(obj);
51 | }
52 |
53 | @Override
54 | public int hashCode() {
55 | int hashCode = this.file.hashCode();
56 | hashCode = 31 * hashCode + Boolean.hashCode(this.exists);
57 | hashCode = 31 * hashCode + Long.hashCode(this.length);
58 | hashCode = 31 * hashCode + Long.hashCode(this.lastModified);
59 | return hashCode;
60 | }
61 |
62 | @Override
63 | public String toString() {
64 | return this.file.toString();
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/metadata/ComicInfo.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.metadata;
2 |
3 | import org.w3c.dom.Document;
4 | import org.w3c.dom.Node;
5 | import org.w3c.dom.NodeList;
6 | import xyz.atsumeru.web.model.book.BookArchive;
7 | import xyz.atsumeru.web.util.StringUtils;
8 |
9 | import javax.xml.parsers.DocumentBuilder;
10 | import javax.xml.parsers.DocumentBuilderFactory;
11 | import java.io.InputStream;
12 |
13 | public class ComicInfo {
14 |
15 | public static boolean readComicInfo(BookArchive bookArchive, InputStream comicInfoStream) {
16 | DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
17 | try {
18 | DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
19 | Document document = documentBuilder.parse(comicInfoStream);
20 | NodeList root = document.getElementsByTagName("ComicInfo");
21 |
22 | String year = null;
23 | String month = null;
24 |
25 | for (int i = 0; i < root.getLength(); i++) {
26 | NodeList nodes = root.item(i).getChildNodes();
27 | for (int j = 0; j < nodes.getLength(); j++) {
28 | Node node = nodes.item(j);
29 | switch (node.getNodeName()) {
30 | case "Title":
31 | bookArchive.setTitle(node.getTextContent());
32 | break;
33 | case "Circles":
34 | try {
35 | bookArchive.setCover(node.getTextContent());
36 | } catch (Exception ex) {
37 | System.err.println("Unable to parse Circles from ComicInfo.xml");
38 | }
39 | break;
40 | case "Summary":
41 | bookArchive.setDescription(node.getTextContent());
42 | break;
43 | case "Volume":
44 | try {
45 | bookArchive.setVolume(Float.parseFloat(node.getTextContent()));
46 | } catch (NumberFormatException ex) {
47 | System.err.println("Unable to parse volume number from ComicInfo.xml");
48 | }
49 | break;
50 | case "Year":
51 | year = node.getTextContent();
52 | break;
53 | case "Month":
54 | month = node.getTextContent();
55 | break;
56 | case "Writer":
57 | bookArchive.setAuthors(node.getTextContent());
58 | break;
59 | case "Publisher":
60 | bookArchive.setPublisher(node.getTextContent());
61 | break;
62 | case "Genre":
63 | bookArchive.setGenres(node.getTextContent());
64 | break;
65 | case "Characters":
66 | bookArchive.setCharacters(node.getTextContent());
67 | break;
68 | }
69 | }
70 | }
71 |
72 | bookArchive.setYear(StringUtils.isNotEmpty(month) ? year + "-" + month + "-01" : year);
73 | return true;
74 | } catch (Exception ex) {
75 | ex.printStackTrace();
76 | return false;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/metadata/DjVuInfo.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.metadata;
2 |
3 | import xyz.atsumeru.web.model.book.BookArchive;
4 |
5 | public class DjVuInfo {
6 |
7 | public static void readInfo(BookArchive bookArchive, Integer pagesCount) {
8 | bookArchive.setTitle(bookArchive.getTitle());
9 | bookArchive.setPagesCount(pagesCount);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/metadata/EpubOPF.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.metadata;
2 |
3 | import org.jsoup.Jsoup;
4 | import org.jsoup.nodes.Document;
5 | import org.jsoup.nodes.Element;
6 | import org.jsoup.parser.Parser;
7 | import org.jsoup.safety.Whitelist;
8 | import org.jsoup.select.Elements;
9 | import xyz.atsumeru.web.model.book.BookArchive;
10 |
11 | import java.io.InputStream;
12 | import java.time.LocalDate;
13 | import java.time.format.DateTimeFormatter;
14 | import java.util.Optional;
15 |
16 | public class EpubOPF {
17 |
18 | public static boolean readInfo(BookArchive bookArchive, InputStream stream) {
19 | try {
20 | Document opf = Jsoup.parse(stream, "UTF-8", "", Parser.xmlParser());
21 |
22 | String title = Optional.ofNullable(opf.selectFirst("metadata > dc|title"))
23 | .map(Element::text)
24 | .orElse(null);
25 |
26 | String author = Optional.ofNullable(opf.selectFirst("metadata > dc|creator"))
27 | .map(Element::text)
28 | .orElse(null);
29 |
30 | String description = Optional.ofNullable(opf.selectFirst("metadata > dc|description"))
31 | .map(Element::text)
32 | .map(value -> Jsoup.clean(value, Whitelist.none()))
33 | .orElse(null);
34 |
35 | LocalDate date = Optional.ofNullable(opf.selectFirst("metadata > dc|date"))
36 | .map(Element::text)
37 | .map(EpubOPF::parseDate)
38 | .orElse(null);
39 |
40 | // TODO: ISBN support
41 | String isbn = Optional.ofNullable(opf.select("metadata > dc|identifier"))
42 | .map(Elements::text)
43 | .map(value -> value.toLowerCase().replace("isbn:", ""))
44 | .orElse(null);
45 |
46 | bookArchive.setTitle(title);
47 | bookArchive.setAuthors(author);
48 | bookArchive.setDescription(description);
49 | bookArchive.setYear(Optional.ofNullable(date).map(LocalDate::toString).orElse(null));
50 |
51 | return true;
52 | } catch (Exception ex) {
53 | ex.printStackTrace();
54 | return false;
55 | }
56 | }
57 |
58 | private static LocalDate parseDate(String date) {
59 | try {
60 | return LocalDate.parse(date, DateTimeFormatter.ISO_DATE);
61 | } catch (Exception e) {
62 | try {
63 | return LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE);
64 | } catch (Exception e1) {
65 | try {
66 | return LocalDate.parse(date, DateTimeFormatter.ISO_DATE_TIME);
67 | } catch (Exception e2) {
68 | return null;
69 | }
70 | }
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/metadata/FictionBookInfo.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.metadata;
2 |
3 | import com.kursx.parser.fb2.*;
4 | import org.jsoup.Jsoup;
5 | import org.jsoup.nodes.Document;
6 | import org.jsoup.parser.Parser;
7 | import org.jsoup.select.Elements;
8 | import xyz.atsumeru.web.component.Localization;
9 | import xyz.atsumeru.web.model.book.BookArchive;
10 | import xyz.atsumeru.web.util.TypeUtils;
11 |
12 | import java.io.FileInputStream;
13 | import java.io.IOException;
14 | import java.util.Optional;
15 | import java.util.stream.Collectors;
16 |
17 | public class FictionBookInfo {
18 |
19 | public static boolean readInfo(BookArchive bookArchive, String filePath, FictionBook fictionBook) {
20 | try (FileInputStream fis = new FileInputStream(filePath)) {
21 | Document xml = Jsoup.parse(fis, "UTF-8", "", Parser.xmlParser());
22 |
23 | TitleInfo titleInfo = fictionBook.getDescription().getTitleInfo();
24 | PublishInfo publishInfo = fictionBook.getDescription().getPublishInfo();
25 |
26 | bookArchive.setTitle(Optional.ofNullable(titleInfo.getSequence())
27 | .map(Sequence::getName)
28 | .orElse(titleInfo.getBookTitle()));
29 | bookArchive.setAltTitle(fictionBook.getTitle());
30 | bookArchive.setAuthors(titleInfo.getAuthors()
31 | .stream()
32 | .map(Person::getFullName)
33 | .collect(Collectors.joining(",")));
34 | bookArchive.setTranslators(titleInfo.getTranslators()
35 | .stream()
36 | .map(Person::getFullName)
37 | .collect(Collectors.joining(",")));
38 | bookArchive.setPublisher(publishInfo.getPublisher());
39 | bookArchive.setYear(publishInfo.getYear());
40 | // TODO: Language localization
41 | bookArchive.setLanguage(fictionBook.getLang());
42 | bookArchive.setVolume(Optional.ofNullable(titleInfo.getSequence())
43 | .map(Sequence::getNumber)
44 | .map(number -> TypeUtils.getFloatDef(number, -1f))
45 | .orElse(-1f));
46 | bookArchive.setTags(titleInfo.getGenres()
47 | .stream()
48 | .map(genre -> "fb2_" + genre)
49 | .map(Localization::toLocale)
50 | .collect(Collectors.joining(",")));
51 |
52 | bookArchive.setDescription(Optional.ofNullable(xml.select("description > title-info > annotation"))
53 | .map(Elements::text)
54 | .orElse(null));
55 |
56 | return true;
57 | } catch (IOException e) {
58 | e.printStackTrace();
59 | return false;
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/metadata/PDFInfo.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.metadata;
2 |
3 | import org.apache.pdfbox.pdmodel.PDDocumentInformation;
4 | import xyz.atsumeru.web.model.book.BookArchive;
5 |
6 | public class PDFInfo {
7 |
8 | public static boolean readInfo(BookArchive bookArchive, PDDocumentInformation info, Integer pagesCount) {
9 | bookArchive.setTitle(info.getTitle());
10 | bookArchive.setAuthors(info.getAuthor());
11 | bookArchive.setPublisher(info.getProducer());
12 | bookArchive.setTags(info.getKeywords());
13 | bookArchive.setPagesCount(pagesCount);
14 | return true;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/AtsumeruMessage.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 |
6 | @Data
7 | @AllArgsConstructor
8 | public class AtsumeruMessage {
9 | private int code;
10 | private String message;
11 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/GenreModel.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model;
2 |
3 | import xyz.atsumeru.web.component.Localization;
4 | import xyz.atsumeru.web.enums.Genre;
5 |
6 | public class GenreModel {
7 | private final String name;
8 | private final int id;
9 |
10 | public GenreModel(Genre genre) {
11 | this.name = Localization.toLocale("genre_" + genre.toString().toLowerCase());
12 | this.id = genre.ordinal();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/ServerInfo.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import lombok.Data;
5 | import lombok.experimental.Accessors;
6 |
7 | @Data
8 | @Accessors(chain = true)
9 | public class ServerInfo {
10 | private String name;
11 |
12 | private String version;
13 |
14 | @SerializedName("version_name")
15 | private String versionName;
16 |
17 | @SerializedName("has_password")
18 | private boolean hasPassword;
19 |
20 | @SerializedName("debug_mode")
21 | private boolean debugMode;
22 |
23 | private Stats stats;
24 |
25 | @Data
26 | @Accessors(chain = true)
27 | public static class Stats {
28 | @SerializedName("total_series")
29 | private long totalSeries;
30 |
31 | @SerializedName("total_archives")
32 | private long totalArchives;
33 |
34 | @SerializedName("total_chapters")
35 | private long totalChapters;
36 |
37 | @SerializedName("total_categories")
38 | private long totalCategories;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/UserAccessConstants.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 | import xyz.atsumeru.web.model.database.Category;
6 |
7 | import java.util.List;
8 |
9 | @Data
10 | @AllArgsConstructor
11 | public class UserAccessConstants {
12 | private List roles;
13 | private List authorities;
14 | private List categories;
15 | private List genres;
16 | private List tags;
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/book/DownloadedLinks.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.book;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import lombok.AllArgsConstructor;
5 |
6 | import java.util.List;
7 |
8 | @AllArgsConstructor
9 | public class DownloadedLinks {
10 | private List downloaded;
11 |
12 | @SerializedName("not_downloaded")
13 | private List notDownloaded;
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/book/IBaseBookItem.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.book;
2 |
3 | import xyz.atsumeru.web.enums.*;
4 | import xyz.atsumeru.web.model.book.chapter.BookChapter;
5 | import xyz.atsumeru.web.model.book.volume.VolumeItem;
6 | import xyz.atsumeru.web.model.database.History;
7 |
8 | import java.util.List;
9 |
10 | public interface IBaseBookItem {
11 | VolumeItem createVolumeItem(List chapters, History history, List historyList,
12 | boolean isSingleMode, boolean archiveMode, boolean withChapters, boolean includeFileInfo);
13 | Long getDbId();
14 | Long getSerieDbId();
15 |
16 | BookSerie getSerie();
17 | String getContentId();
18 | String getFolder();
19 | String getContentLink();
20 | String getContentLinks();
21 | String getTitle();
22 | String getAltTitle();
23 | String getJapTitle();
24 | String getKorTitle();
25 | String getCover();
26 | String getAuthors();
27 | String getArtists();
28 | String getTranslators();
29 | String getPublisher();
30 | String getGenres();
31 | String getTags();
32 | String getYear();
33 | String getCountry();
34 | String getLanguage();
35 | String getEvent();
36 | String getCharacters();
37 | String getSeries();
38 | String getParodies();
39 | String getCircles();
40 | String getMagazines();
41 | String getDescription();
42 | Float getVolume();
43 | Long getVolumesCount();
44 | String getScore();
45 | Integer getRating();
46 | Boolean getMature();
47 | Boolean getAdult();
48 | Boolean isSingle();
49 |
50 | Integer getPagesCount();
51 |
52 | Long getCreatedAt();
53 | Long getUpdatedAt();
54 |
55 | ContentType getContentType();
56 | Status getStatus();
57 | TranslationStatus getTranslationStatus();
58 |
59 | PlotType getPlotType();
60 | Censorship getCensorship();
61 | Color getColor();
62 | AgeRating getAgeRating();
63 | List getVolumes();
64 |
65 | List getPageEntryNames();
66 |
67 | void setSerie(BookSerie serie);
68 | void setContentId(String contentId);
69 | void setFolder(String folder);
70 | void setContentLink(String contentLink);
71 | void setContentLinks(String contentLinks);
72 | void setContentType(String contentType);
73 | void setTitle(String serieTitle);
74 | void setAltTitle(String alternativeTitle);
75 | void setJapTitle(String japTitle);
76 | void setKorTitle(String korTitle);
77 | void setCover(String cover);
78 | void setAuthors(String authors);
79 | void setTranslators(String translators);
80 | void setGenres(String genres);
81 | void setTags(String tags);
82 | void setYear(String year);
83 | void setCountry(String country);
84 | void setLanguage(String language);
85 | void setDescription(String description);
86 | void setVolume(float volume);
87 | void setVolumesCount(Long volumesCount);
88 | void setIsMature(Boolean isMature);
89 | void setIsAdult(Boolean isAdult);
90 |
91 | void setCreatedAt(Long timestamp);
92 | void setUpdatedAt(Long timestamp);
93 |
94 | void setStatus(String status);
95 | void setTranslationStatus(String translationStatus);
96 | void setPlotType(String plotType);
97 | void setCensorship(String censorship);
98 | void setVolumes(List volumes);
99 |
100 | void setCategories(String categories);
101 | String getCategories();
102 |
103 | Long getChaptersCount();
104 |
105 | void setChaptersCount(Long value);
106 |
107 | boolean isRemoved();
108 | void setRemoved(boolean removed);
109 |
110 | void addVolume(VolumeItem volumeItem);
111 | void addVolumes(List volumeItems);
112 | }
113 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/book/franchise/Franchise.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.book.franchise;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import lombok.Data;
5 | import xyz.atsumeru.web.model.book.service.BoundService;
6 |
7 | import java.util.List;
8 |
9 | @Data
10 | public class Franchise {
11 | private int order;
12 |
13 | @SerializedName("content_type")
14 | private String contentType;
15 |
16 | private String title;
17 |
18 | @SerializedName("alt_title")
19 | private String altTitle;
20 |
21 | @SerializedName("jap_title")
22 | private String japTitle;
23 |
24 | private String year;
25 |
26 | // Base64 image
27 | private String cover;
28 |
29 | @SerializedName("bound_content")
30 | private List boundContent;
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/book/image/Images.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.book.image;
2 |
3 | import lombok.Getter;
4 | import lombok.Setter;
5 |
6 | import java.awt.image.BufferedImage;
7 |
8 | public class Images {
9 | @Getter @Setter private String thumbnail;
10 | @Getter @Setter private String accent;
11 | @Getter @Setter private BufferedImage originalBufferedImage;
12 |
13 | public Images(String thumbnail, BufferedImage bufferedImage) {
14 | this.thumbnail = thumbnail;
15 | this.originalBufferedImage = bufferedImage;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/book/service/BoundService.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.book.service;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import lombok.Getter;
5 | import xyz.atsumeru.web.enums.ServiceType;
6 | import xyz.atsumeru.web.util.StringUtils;
7 |
8 | public class BoundService {
9 | @Getter
10 | @SerializedName("service_type")
11 | private final ServiceType serviceType;
12 |
13 | private final String id;
14 |
15 | @Getter
16 | private final String link;
17 |
18 | public BoundService(ServiceType serviceType, String idOrLink) {
19 | this.serviceType = serviceType;
20 | this.id = getRealId(idOrLink);
21 | this.link = serviceType.createUrl(id);
22 | }
23 |
24 | public BoundService(ServiceType serviceType, String id, String link) {
25 | this.serviceType = serviceType;
26 | this.id = id;
27 | this.link = link;
28 | }
29 |
30 | public String getId() {
31 | return getRealId(StringUtils.getFirstNotEmptyValue(id, link));
32 | }
33 |
34 | private String getRealId(String idOrLink) {
35 | return StringUtils.startsWithIgnoreCase(idOrLink, "http")
36 | ? serviceType.extractId(idOrLink)
37 | : idOrLink;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/book/volume/VolumeItem.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.book.volume;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import lombok.Data;
5 | import org.springframework.lang.Nullable;
6 | import xyz.atsumeru.web.model.book.chapter.BookChapter;
7 | import xyz.atsumeru.web.model.database.History;
8 |
9 | import java.io.Serializable;
10 | import java.util.Collection;
11 | import java.util.Optional;
12 |
13 | @Data
14 | public class VolumeItem implements Serializable {
15 | private String id;
16 | private String title;
17 |
18 | @SerializedName("additional_title")
19 | private String additionalTitle;
20 |
21 | private String year;
22 |
23 | @SerializedName("cover_accent")
24 | private String coverAccent;
25 |
26 | @SerializedName("file_name")
27 | private String fileName;
28 |
29 | @SerializedName("file_path")
30 | private String filePath;
31 |
32 | @Nullable
33 | @SerializedName("volume")
34 | private Float volume;
35 |
36 | @SerializedName("pages_count")
37 | private int pagesCount;
38 |
39 | @SerializedName("is_book")
40 | private boolean isBook;
41 |
42 | @SerializedName("created_at")
43 | private long createdAt;
44 |
45 | private History history;
46 |
47 | private Collection chapters;
48 |
49 | public boolean isRead() {
50 | return Optional.ofNullable(history)
51 | .map(model -> model.getCurrentPage().equals(model.getPagesCount()))
52 | .orElse(false);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/category/Metacategory.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.category;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 |
6 | @Data
7 | @AllArgsConstructor
8 | public class Metacategory {
9 | private String id;
10 | private String name;
11 | private long count;
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/covers/CoversCachingStatus.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.covers;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import lombok.Data;
5 |
6 | @Data
7 | public class CoversCachingStatus {
8 | @SerializedName("covers_caching_active")
9 | private boolean isCoversCachingActive;
10 |
11 | @SerializedName("running_ms")
12 | private long runningMs;
13 |
14 | private int saved;
15 | private int total;
16 | private float percent;
17 |
18 | public CoversCachingStatus(boolean isCoversCachingActive, long runningMs, int saved, int total) {
19 | this.isCoversCachingActive = isCoversCachingActive;
20 | this.runningMs = runningMs;
21 | this.saved = saved;
22 | this.total = total;
23 | percent = total > 0 ? (float)saved / total * 100 : 0;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/database/AtsumeruUser.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.database;
2 |
3 | import com.google.gson.annotations.Expose;
4 | import com.google.gson.annotations.JsonAdapter;
5 | import com.google.gson.annotations.SerializedName;
6 | import com.j256.ormlite.field.DatabaseField;
7 | import com.j256.ormlite.table.DatabaseTable;
8 | import lombok.Data;
9 | import lombok.NonNull;
10 | import xyz.atsumeru.web.json.adapter.StringListBidirectionalAdapter;
11 | import xyz.atsumeru.web.repository.CategoryRepository;
12 | import xyz.atsumeru.web.util.ArrayUtils;
13 | import xyz.atsumeru.web.util.StringUtils;
14 |
15 | import java.util.*;
16 | import java.util.stream.Collectors;
17 |
18 | @Data
19 | @DatabaseTable(tableName = "USERS")
20 | public class AtsumeruUser {
21 | @DatabaseField(generatedId = true)
22 | private Long id;
23 |
24 | @SerializedName("user_name")
25 | @DatabaseField(columnName = "USERNAME")
26 | private String userName;
27 |
28 | @Expose(serialize = false)
29 | @DatabaseField(columnName = "PASSWORD")
30 | private String password;
31 |
32 | @JsonAdapter(StringListBidirectionalAdapter.class)
33 | @DatabaseField(columnName = "ROLES")
34 | private String roles;
35 |
36 | @JsonAdapter(StringListBidirectionalAdapter.class)
37 | @DatabaseField(columnName = "AUTHORITIES")
38 | private String authorities;
39 |
40 | @JsonAdapter(StringListBidirectionalAdapter.class)
41 | @SerializedName("allowed_categories")
42 | @DatabaseField(columnName = "ALLOWED_CATEGORIES")
43 | private String allowedCategories;
44 |
45 | @JsonAdapter(StringListBidirectionalAdapter.class)
46 | @SerializedName("disallowed_genres")
47 | @DatabaseField(columnName = "DISALLOWED_GENRES")
48 | private String disallowedGenres;
49 |
50 | @JsonAdapter(StringListBidirectionalAdapter.class)
51 | @SerializedName("disallowed_tags")
52 | @DatabaseField(columnName = "DISALLOWED_TAGS")
53 | private String disallowedTags;
54 |
55 | public Set getAuthoritiesSet() {
56 | Set authorities = new HashSet<>(ArrayUtils.splitString(getAuthorities(), ","));
57 |
58 | List roles = ArrayUtils.splitString(getRoles(), ",");
59 | roles.forEach(role -> authorities.add(role.startsWith("ROLE_") ? role : "ROLE_" + role));
60 |
61 | return authorities;
62 | }
63 |
64 | public Map getAllowedCategoriesMap() {
65 | Map allowedCategories = new HashMap<>();
66 | List categoryIds = ArrayUtils.splitString(getAllowedCategories(), ",");
67 | categoryIds.forEach(id -> {
68 | Category category = CategoryRepository.getCategoryById(id);
69 | if (category != null) {
70 | allowedCategories.put(category.getCategoryId(), category);
71 | }
72 | });
73 | return allowedCategories;
74 | }
75 |
76 | public List getAllowedContentTypes() {
77 | return getAllowedCategoriesMap().values().stream()
78 | .map(Category::getContentType)
79 | .filter(StringUtils::isNotEmpty)
80 | .collect(Collectors.toList());
81 | }
82 |
83 | public List getAllowedCategoryIds() {
84 | return getAllowedCategoriesMap().values()
85 | .stream()
86 | .map(category -> CategoryRepository.createDbIdForCategoryId(category.getCategoryId()))
87 | .collect(Collectors.toList());
88 | }
89 |
90 | @NonNull
91 | public Set getDisallowedGenres() {
92 | return Optional.ofNullable(disallowedGenres)
93 | .filter(StringUtils::isNotEmpty)
94 | .map(genres -> ArrayUtils.splitString(genres, ","))
95 | .map(genres -> genres.stream()
96 | .map(String::toLowerCase)
97 | .collect(Collectors.toSet()))
98 | .orElseGet(HashSet::new);
99 | }
100 |
101 | @NonNull
102 | public Set getDisallowedTags() {
103 | return Optional.ofNullable(disallowedTags)
104 | .filter(StringUtils::isNotEmpty)
105 | .map(tags -> ArrayUtils.splitString(tags, ","))
106 | .map(tags -> tags.stream()
107 | .map(String::toLowerCase)
108 | .collect(Collectors.toSet()))
109 | .orElseGet(HashSet::new);
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/database/Category.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.database;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import com.j256.ormlite.field.DatabaseField;
5 | import com.j256.ormlite.table.DatabaseTable;
6 | import lombok.Getter;
7 | import lombok.Setter;
8 | import org.jetbrains.annotations.Nullable;
9 | import xyz.atsumeru.web.component.Localization;
10 | import xyz.atsumeru.web.enums.ContentType;
11 | import xyz.atsumeru.web.json.annotation.Exclude;
12 | import xyz.atsumeru.web.util.StringUtils;
13 |
14 | @Getter
15 | @DatabaseTable(tableName = "CATEGORIES")
16 | public class Category {
17 | public static final String CATEGORY_TAG = "atsumeru-category";
18 |
19 | @Exclude
20 | @SerializedName("_id")
21 | @DatabaseField(generatedId = true)
22 | private Long id;
23 |
24 | @SerializedName("id")
25 | @DatabaseField(columnName = "CATEGORY_ID")
26 | private String categoryId;
27 |
28 | @Setter
29 | @DatabaseField(columnName = "NAME")
30 | private String name;
31 |
32 | @SerializedName("content_type")
33 | @DatabaseField(columnName = "CONTENT_TYPE")
34 | private String contentType;
35 |
36 | @Setter
37 | @DatabaseField(columnName = "SORT_ORDER")
38 | private Integer order;
39 |
40 | public Category() {
41 | }
42 |
43 | public Category(String categoryId, String name, @Nullable ContentType contentType, int order) {
44 | this.categoryId = categoryId;
45 | this.name = name;
46 | if (contentType != null) {
47 | this.contentType = contentType.name();
48 | }
49 | this.order = order;
50 | }
51 |
52 | public static Category createFromName(String categoryName, int order) {
53 | return new Category(CATEGORY_TAG + StringUtils.md5Hex(categoryName), categoryName, null, order);
54 | }
55 |
56 | public static Category createFromContentType(ContentType contentType, int order) {
57 | return new Category(
58 | CATEGORY_TAG + StringUtils.md5Hex(contentType.name() + contentType.ordinal()),
59 | Localization.toLocale("enum." + contentType.name().toLowerCase()),
60 | contentType,
61 | order
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/database/DatabaseFields.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.database;
2 |
3 | public class DatabaseFields {
4 | public static final String MAL_ID = "MAL_ID";
5 | public static final String SHIKIMORI_ID = "SHIKIMORI_ID";
6 | public static final String KITSU_ID = "KITSU_ID";
7 | public static final String ANILIST_ID = "ANILIST_ID";
8 | public static final String MANGAUPDATES_ID = "MANGAUPDATES_ID";
9 | public static final String ANIMEPLANET_ID = "ANIMEPLANET_ID";
10 | public static final String COMICVINE_ID = "COMICVINE_ID";
11 | public static final String COMICSDB_ID = "COMICSDB_ID";
12 | public static final String HENTAG_ID = "HENTAG_ID";
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/database/DatabaseVersion.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.database;
2 |
3 | import com.j256.ormlite.field.DatabaseField;
4 | import com.j256.ormlite.table.DatabaseTable;
5 | import lombok.Getter;
6 | import lombok.Setter;
7 |
8 | @Setter
9 | @Getter
10 | @DatabaseTable(tableName = "DATABASE_VERSION")
11 | public class DatabaseVersion {
12 | @DatabaseField(generatedId = true)
13 | private Long id;
14 |
15 | @DatabaseField(columnName = "VERSION")
16 | private long version;
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/database/History.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.database;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import com.j256.ormlite.field.DatabaseField;
5 | import com.j256.ormlite.table.DatabaseTable;
6 | import lombok.Data;
7 | import xyz.atsumeru.web.json.annotation.Exclude;
8 | import xyz.atsumeru.web.model.book.BookSerie;
9 |
10 | @Data
11 | @DatabaseTable(tableName = "HISTORY")
12 | public class History {
13 | @Exclude
14 | @SerializedName("_id")
15 | @DatabaseField(generatedId = true)
16 | private Long id;
17 |
18 | @Exclude
19 | @DatabaseField(columnName = "USER_ID")
20 | private Long userId;
21 |
22 | @Exclude
23 | @DatabaseField(foreign = true, columnName = "SERIE")
24 | private BookSerie serie;
25 |
26 | @SerializedName("serie_hash")
27 | @DatabaseField(columnName = "SERIE_HASH")
28 | private String serieHash;
29 |
30 | @SerializedName("archive_hash")
31 | @DatabaseField(columnName = "ARCHIVE_HASH")
32 | private String archiveHash;
33 |
34 | @SerializedName("chapter_hash")
35 | @DatabaseField(columnName = "CHAPTER_HASH")
36 | private String chapterHash;
37 |
38 | @SerializedName("current_page")
39 | @DatabaseField(columnName = "CURRENT_PAGE")
40 | private Integer currentPage;
41 |
42 | @SerializedName("pages_count")
43 | @DatabaseField(columnName = "PAGES_COUNT")
44 | private Integer pagesCount;
45 |
46 | @SerializedName("last_read_at")
47 | @DatabaseField(columnName = "LAST_READ_AT")
48 | private Long lastReadAt;
49 |
50 | public History() {
51 | }
52 |
53 | public History(long userId, BookSerie serie, String archiveHash, String chapterHash, int pagesCount) {
54 | this.serie = serie;
55 | this.userId = userId;
56 | this.serieHash = serie.getSerieId();
57 | this.archiveHash = archiveHash;
58 | this.chapterHash = chapterHash;
59 | this.pagesCount = pagesCount;
60 | }
61 |
62 | public Long getDbId() {
63 | return id;
64 | }
65 |
66 | public String getBookHash(boolean isSerie) {
67 | return isSerie ? getSerieHash() : getArchiveHash();
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/filter/Filters.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.filter;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 |
5 | import java.util.List;
6 |
7 | public class Filters {
8 | private String id;
9 | private String name;
10 | @SerializedName("has_and_mode")
11 | private boolean hasAndMode;
12 | @SerializedName("single_mode")
13 | private boolean singleMode;
14 | private List values;
15 |
16 | public static Filters create(String id, String name, boolean hasAndMode, boolean singleMode, List values) {
17 | Filters filters = new Filters();
18 | filters.id = id;
19 | filters.name = name;
20 | filters.hasAndMode = hasAndMode;
21 | filters.singleMode = singleMode;
22 | filters.values = values;
23 | return filters;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/importer/ImportStatus.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.importer;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import lombok.Data;
5 |
6 | @Data
7 | public class ImportStatus {
8 | @SerializedName("import_active")
9 | private boolean isActive;
10 |
11 | @SerializedName("last_start_time")
12 | private long lastStartTime;
13 |
14 | @SerializedName("running_ms")
15 | private long runningMs;
16 |
17 | private int imported;
18 | private int total;
19 | private float percent;
20 |
21 | public ImportStatus(boolean isActive, long lastStartTime, long runningMs, int imported, int total) {
22 | this.isActive = isActive;
23 | this.lastStartTime = lastStartTime;
24 | this.runningMs = runningMs;
25 | this.imported = imported;
26 | this.total = total;
27 | percent = total > 0 ? (float)imported / total * 100 : 0;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/metadata/MetadataUpdateStatus.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.metadata;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import lombok.Data;
5 |
6 | @Data
7 | public class MetadataUpdateStatus {
8 | @SerializedName("metadata_update_active")
9 | private boolean isUpdateActive;
10 |
11 | @SerializedName("running_ms")
12 | private long runningMs;
13 |
14 | private int updated;
15 | private int total;
16 |
17 | private float percent;
18 |
19 | public MetadataUpdateStatus(boolean isUpdateActive, long runningMs, int updated, int total) {
20 | this.isUpdateActive = isUpdateActive;
21 | this.runningMs = runningMs;
22 | this.updated = updated;
23 | this.total = total;
24 | percent = total > 0 ? (float)updated / total * 100 : 0;
25 | }
26 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/service/ServicesStatus.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.service;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Data;
6 | import xyz.atsumeru.web.model.covers.CoversCachingStatus;
7 | import xyz.atsumeru.web.model.importer.ImportStatus;
8 | import xyz.atsumeru.web.model.metadata.MetadataUpdateStatus;
9 |
10 | @Data
11 | @AllArgsConstructor
12 | public class ServicesStatus {
13 | @SerializedName("importer")
14 | private ImportStatus importStatus;
15 |
16 | @SerializedName("metadata_update")
17 | private MetadataUpdateStatus metadataUpdateStatus;
18 |
19 | @SerializedName("covers_caching")
20 | private CoversCachingStatus coversCachingStatus;
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/model/settings/ServerSettings.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.model.settings;
2 |
3 | import com.google.gson.annotations.SerializedName;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Data;
6 | import xyz.atsumeru.web.manager.Settings;
7 |
8 | @Data
9 | @AllArgsConstructor
10 | public class ServerSettings {
11 | @SerializedName(Settings.KEY_ALLOW_LOADING_LIST_WITH_VOLUMES)
12 | private boolean allowLoadingListWithVolumes;
13 |
14 | @SerializedName(Settings.KEY_ALLOW_LOADING_LIST_WITH_CHAPTERS)
15 | private boolean allowLoadingListWithChapters;
16 |
17 | @SerializedName(Settings.KEY_DISABLE_REQUEST_LOGGING_INTO_CONSOLE)
18 | private boolean isDisableRequestLoggingIntoConsole;
19 |
20 | @SerializedName(Settings.KEY_DISABLE_FILE_WATCHER)
21 | private boolean disableFileWatcher;
22 |
23 | @SerializedName(Settings.KEY_DISABLE_WATCH_FOR_MODIFIED_FILES)
24 | private boolean disableWatchForModifiedFiles;
25 |
26 | @SerializedName(Settings.KEY_DISABLE_CHAPTERS)
27 | private boolean disableChapters;
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/properties/ImportFolders.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.properties;
2 |
3 | import com.google.gson.Gson;
4 | import com.google.gson.reflect.TypeToken;
5 | import com.google.gson.stream.JsonReader;
6 | import lombok.Getter;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 | import org.springframework.context.annotation.DependsOn;
10 | import org.springframework.stereotype.Component;
11 | import xyz.atsumeru.web.configuration.FileWatcherConfiguration;
12 | import xyz.atsumeru.web.manager.Workspace;
13 | import xyz.atsumeru.web.model.importer.ImportFolder;
14 | import xyz.atsumeru.web.util.FileUtils;
15 | import xyz.atsumeru.web.util.StringUtils;
16 |
17 | import java.io.File;
18 | import java.io.FileReader;
19 | import java.io.IOException;
20 | import java.lang.reflect.Type;
21 | import java.util.ArrayList;
22 | import java.util.List;
23 | import java.util.stream.Collectors;
24 |
25 | @Component
26 | @DependsOn("workspace")
27 | public class ImportFolders {
28 | private static final Logger logger = LoggerFactory.getLogger(ImportFolders.class.getSimpleName());
29 | private static final String FOLDERS_PROPERTIES_FILENAME = "folders.properties";
30 |
31 | @Getter private static List folderProperties;
32 |
33 | public ImportFolders() {
34 | try (JsonReader reader = new JsonReader(new FileReader(new File(Workspace.CONFIG_DIR, FOLDERS_PROPERTIES_FILENAME)))) {
35 | final Type folderType = new TypeToken>() {}.getType();
36 | folderProperties = new Gson().fromJson(reader, folderType);
37 | logger.info("Import folders from " + FOLDERS_PROPERTIES_FILENAME + " loaded successfully!");
38 | } catch (IOException e) {
39 | folderProperties = new ArrayList<>();
40 | logger.warn("Unable to load " + FOLDERS_PROPERTIES_FILENAME + "... Maybe there is no folders yet?");
41 | }
42 | }
43 |
44 | public static void addFolder(ImportFolder importFolder) {
45 | folderProperties.add(importFolder);
46 | saveProperties();
47 | }
48 |
49 | public static void removeFolder(ImportFolder importFolder) {
50 | folderProperties = folderProperties.stream()
51 | .filter(property -> !StringUtils.equalsIgnoreCase(property.getHash(), importFolder.getHash()))
52 | .collect(Collectors.toList());
53 |
54 | saveProperties();
55 | }
56 |
57 | public static boolean containsFolder(String path) {
58 | String fixedPath = FileUtils.addPathSlash(path);
59 | return folderProperties.stream()
60 | .map(property -> FileUtils.addPathSlash(property.getPath()))
61 | .anyMatch(propertyPath -> StringUtils.equalsIgnoreCase(propertyPath, fixedPath));
62 | }
63 |
64 | private static void saveProperties() {
65 | FileUtils.writeStringToFile(new File(Workspace.CONFIG_DIR, FOLDERS_PROPERTIES_FILENAME), new Gson().toJson(folderProperties));
66 | FileWatcherConfiguration.start();
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/renderer/AbstractRenderer.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.renderer;
2 |
3 | import org.slf4j.Logger;
4 | import xyz.atsumeru.web.helper.Constants;
5 |
6 | import javax.imageio.ImageIO;
7 | import java.awt.image.BufferedImage;
8 | import java.io.IOException;
9 | import java.io.OutputStream;
10 |
11 | public abstract class AbstractRenderer {
12 |
13 | public abstract BufferedImage renderPage(int pageIndex, double scaleOrDpi);
14 |
15 | public abstract double getScaleOrDpi();
16 |
17 | public abstract Logger getLogger();
18 |
19 | public boolean renderPage(OutputStream outputStream, int pageIndex, double scale) {
20 | BufferedImage bim = renderPage(pageIndex, scale);
21 | if (bim != null) {
22 | try {
23 | ImageIO.write(bim, Constants.Formats.JPEG, outputStream);
24 | return true;
25 | } catch (IOException e) {
26 | e.printStackTrace();
27 | }
28 | }
29 | return false;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/renderer/RendererFactory.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.renderer;
2 |
3 | import org.springframework.lang.Nullable;
4 | import xyz.atsumeru.web.enums.BookType;
5 | import xyz.atsumeru.web.exception.RendererNotImplementedException;
6 | import xyz.atsumeru.web.renderer.impl.DjVuRenderer;
7 | import xyz.atsumeru.web.renderer.impl.PDFRenderer;
8 |
9 | public class RendererFactory {
10 |
11 | public static AbstractRenderer create(@Nullable BookType bookType, String filePath) {
12 | if (bookType != null) {
13 | return switch (bookType) {
14 | case PDF -> PDFRenderer.create(filePath);
15 | case DJVU -> DjVuRenderer.create(filePath);
16 | case ARCHIVE, EPUB, FB2 -> throw new RendererNotImplementedException("Renderer for type " + bookType + " not yet implemented!");
17 | };
18 | }
19 | throw new RendererNotImplementedException("Renderer for type [null] not yet implemented!");
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/renderer/impl/DjVuRenderer.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.renderer.impl;
2 |
3 | import com.djvu2image.DjVuBook;
4 | import com.djvu2image.PaperFormat;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import xyz.atsumeru.web.helper.ImageHelper;
8 | import xyz.atsumeru.web.manager.cache.AtsumeruRenderersCache;
9 | import xyz.atsumeru.web.renderer.AbstractRenderer;
10 |
11 | import java.awt.image.BufferedImage;
12 | import java.io.File;
13 | import java.io.IOException;
14 | import java.util.Optional;
15 |
16 | public class DjVuRenderer extends AbstractRenderer {
17 | private static final Logger logger = LoggerFactory.getLogger(DjVuRenderer.class.getSimpleName());
18 |
19 | private final String filePath;
20 |
21 | public static DjVuRenderer create(String filePath) {
22 | return new DjVuRenderer(filePath);
23 | }
24 |
25 | private DjVuRenderer(String filePath) {
26 | this.filePath = filePath;
27 | }
28 |
29 | public DjVuBook getBook() {
30 | return AtsumeruRenderersCache.getDjvuBook(filePath, DjVuRenderer::load);
31 | }
32 |
33 | @Override
34 | public double getScaleOrDpi() {
35 | return 2.0;
36 | }
37 |
38 | @Override
39 | public BufferedImage renderPage(int pageIndex, double scale) {
40 | return Optional.ofNullable(getBook())
41 | .map(book -> book.getPageImage(pageIndex, false, scale))
42 | .map(ImageHelper::toBufferedImage)
43 | .orElse(null);
44 | }
45 |
46 | @Override
47 | public Logger getLogger() {
48 | return logger;
49 | }
50 |
51 | private static DjVuBook load(File file) {
52 | try {
53 | return DjVuBook.open(file, PaperFormat.A4, false);
54 | } catch (IOException e) {
55 | e.printStackTrace();
56 | return null;
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/renderer/impl/PDFRenderer.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.renderer.impl;
2 |
3 | import org.apache.pdfbox.pdmodel.PDDocument;
4 | import org.apache.pdfbox.rendering.ImageType;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import xyz.atsumeru.web.manager.cache.AtsumeruRenderersCache;
8 | import xyz.atsumeru.web.renderer.AbstractRenderer;
9 |
10 | import java.awt.image.BufferedImage;
11 | import java.io.File;
12 | import java.io.IOException;
13 | import java.util.Optional;
14 |
15 | public class PDFRenderer extends AbstractRenderer {
16 | private static final Logger logger = LoggerFactory.getLogger(PDFRenderer.class.getSimpleName());
17 |
18 | private final String filePath;
19 | private PDDocument nonCacheableDocument;
20 |
21 | public static PDFRenderer create(String filePath) {
22 | return new PDFRenderer(filePath);
23 | }
24 |
25 | private PDFRenderer(String filePath) {
26 | this.filePath = filePath;
27 | }
28 |
29 | public PDDocument getDocument() {
30 | return Optional.ofNullable(nonCacheableDocument).orElseGet(() -> AtsumeruRenderersCache.getPDDocument(filePath, PDFRenderer::load));
31 | }
32 |
33 | public PDDocument getDocumentNonCacheable() {
34 | return nonCacheableDocument = PDFRenderer.load(new File(filePath));
35 | }
36 |
37 | @Override
38 | public double getScaleOrDpi() {
39 | return 300;
40 | }
41 |
42 | @Override
43 | public BufferedImage renderPage(int pageIndex, double dpi) {
44 | return Optional.ofNullable(getDocument())
45 | .map(document -> renderPage(document, pageIndex - 1, dpi))
46 | .orElse(null);
47 | }
48 |
49 | private BufferedImage renderPage(PDDocument document, int pageIndex, double dpi) {
50 | try {
51 | org.apache.pdfbox.rendering.PDFRenderer pdfRenderer = new org.apache.pdfbox.rendering.PDFRenderer(document);
52 | return pdfRenderer.renderImageWithDPI(pageIndex, (int) dpi, ImageType.RGB);
53 | } catch (IOException e) {
54 | e.printStackTrace();
55 | }
56 | return null;
57 | }
58 |
59 | @Override
60 | public Logger getLogger() {
61 | return logger;
62 | }
63 |
64 | private static PDDocument load(File file) {
65 | try {
66 | return PDDocument.load(file);
67 | } catch (IOException e) {
68 | e.printStackTrace();
69 | return null;
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/repository/dao/BaseDaoManager.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.repository.dao;
2 |
3 | import com.j256.ormlite.dao.Dao;
4 | import com.j256.ormlite.dao.DaoManager;
5 | import com.j256.ormlite.jdbc.JdbcConnectionSource;
6 | import com.j256.ormlite.stmt.QueryBuilder;
7 | import com.j256.ormlite.stmt.SelectArg;
8 | import com.j256.ormlite.support.ConnectionSource;
9 | import com.j256.ormlite.table.TableUtils;
10 | import org.sqlite.SQLiteException;
11 | import xyz.atsumeru.web.helper.OrmLiteUpgradeTable;
12 | import xyz.atsumeru.web.model.database.DatabaseVersion;
13 | import xyz.atsumeru.web.util.FileUtils;
14 |
15 | import java.io.Closeable;
16 | import java.io.IOException;
17 | import java.sql.SQLException;
18 | import java.util.ArrayList;
19 | import java.util.List;
20 |
21 | public abstract class BaseDaoManager implements Closeable {
22 | protected String databaseUrl;
23 | protected ConnectionSource connectionSource;
24 |
25 | static {
26 | System.setProperty("com.j256.ormlite.logger.type", "LOCAL");
27 | System.setProperty("com.j256.ormlite.logger.level", "FATAL");
28 | }
29 |
30 | public BaseDaoManager(String dbName) throws SQLException {
31 | databaseUrl = "jdbc:sqlite:" + dbName;
32 | connectionSource = new JdbcConnectionSource(databaseUrl);
33 | }
34 |
35 | public void clearTable(Class clazz) {
36 | try {
37 | TableUtils.clearTable(this.connectionSource, clazz);
38 | } catch (SQLException e) {
39 | e.printStackTrace();
40 | }
41 | }
42 |
43 | @Override
44 | public void close() {
45 | FileUtils.closeLoudly(connectionSource);
46 | }
47 |
48 | protected void upgradeSchema(long schemaVersion, Dao dao, Class clazz) throws SQLException {
49 | if (isDatabaseObsolete(schemaVersion)) {
50 | try {
51 | OrmLiteUpgradeTable.migrateTable(connectionSource, clazz);
52 | dao.executeRawNoArgs("VACUUM");
53 | } catch (IOException ex) {
54 | ex.printStackTrace();
55 | }
56 | }
57 | }
58 |
59 | protected boolean isDatabaseObsolete(long schemaVersion) throws SQLException {
60 | TableUtils.createTableIfNotExists(connectionSource, DatabaseVersion.class);
61 | Dao dbVersionDao = DaoManager.createDao(connectionSource, DatabaseVersion.class);
62 |
63 | List dbVersions = new ArrayList<>();
64 | QueryBuilder queryBuilder = dbVersionDao.queryBuilder();
65 | try {
66 | queryBuilder.where().eq("VERSION", new SelectArg(schemaVersion));
67 | dbVersions = dbVersionDao.query(queryBuilder.prepare());
68 | } catch (SQLiteException ex) {
69 | System.err.println("DB column VERSION not found. Creating...");
70 | }
71 |
72 | if (dbVersions.isEmpty()) {
73 | DatabaseVersion version = new DatabaseVersion();
74 | version.setVersion(schemaVersion);
75 | dbVersionDao.create(version);
76 | }
77 | return dbVersions.isEmpty();
78 | }
79 |
80 | static {
81 | try {
82 | Class.forName("org.sqlite.JDBC");
83 | } catch (ClassNotFoundException e) {
84 | e.printStackTrace();
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/repository/dao/UsersDaoManager.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.repository.dao;
2 |
3 | import com.j256.ormlite.dao.Dao;
4 | import com.j256.ormlite.dao.DaoManager;
5 | import com.j256.ormlite.stmt.DeleteBuilder;
6 | import com.j256.ormlite.table.TableUtils;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 | import org.springframework.context.annotation.DependsOn;
10 | import org.springframework.stereotype.Component;
11 | import xyz.atsumeru.web.manager.Workspace;
12 | import xyz.atsumeru.web.model.database.AtsumeruUser;
13 |
14 | import java.io.File;
15 | import java.sql.SQLException;
16 | import java.util.List;
17 |
18 | @Component
19 | @DependsOn("workspace")
20 | public class UsersDaoManager extends BaseDaoManager {
21 | public static final long DB_VERSION = 1;
22 | private static final Logger logger = LoggerFactory.getLogger(UsersDaoManager.class.getSimpleName());
23 | private static final String dbPath = new File(Workspace.DATABASES_DIR, "users.db").getAbsolutePath();
24 |
25 | private Dao usersDao;
26 |
27 | public UsersDaoManager() throws SQLException {
28 | super(dbPath);
29 | try {
30 | usersDao = DaoManager.createDao(connectionSource, AtsumeruUser.class);
31 | TableUtils.createTableIfNotExists(connectionSource, AtsumeruUser.class);
32 | upgradeSchema(DB_VERSION, usersDao, AtsumeruUser.class);
33 | logger.info("Connected to users database!");
34 | } catch (Exception e) {
35 | logger.error("Failed to connect to users database!", e);
36 | }
37 | }
38 |
39 | public boolean save(AtsumeruUser item) {
40 | try {
41 | return (
42 | item.getId() == null || item.getId() < 0
43 | ? usersDao.create(item)
44 | : usersDao.update(item)
45 | ) == 1;
46 | } catch (SQLException e) {
47 | e.printStackTrace();
48 | return false;
49 | }
50 | }
51 |
52 | public AtsumeruUser query(long id) {
53 | try {
54 | return usersDao.queryForEq("id", id).get(0);
55 | } catch (SQLException e) {
56 | e.printStackTrace();
57 | } catch (IndexOutOfBoundsException ignored) {
58 | }
59 | return null;
60 | }
61 |
62 | public AtsumeruUser query(String userName) {
63 | try {
64 | return usersDao.queryForEq("USERNAME", userName).get(0);
65 | } catch (SQLException e) {
66 | e.printStackTrace();
67 | } catch (IndexOutOfBoundsException ignored) {
68 | }
69 | return null;
70 | }
71 |
72 | public List queryAll() {
73 | try {
74 | return usersDao.queryForAll();
75 | } catch (SQLException e) {
76 | e.printStackTrace();
77 | }
78 | return null;
79 | }
80 |
81 | public boolean deleteById(long id) {
82 | return deleteByColumnEq("id", String.valueOf(id));
83 | }
84 |
85 | public boolean deleteByColumnEq(String columnName, String columnValue) {
86 | try {
87 | DeleteBuilder deleteBuilder = usersDao.deleteBuilder();
88 | deleteBuilder.where().eq(columnName, columnValue);
89 | return usersDao.delete(deleteBuilder.prepare()) > 0;
90 | } catch (SQLException e) {
91 | e.printStackTrace();
92 | return false;
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/security/configuration/NoSecurityConfiguration.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.security.configuration;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.context.annotation.Profile;
6 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
10 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
11 | import org.springframework.security.crypto.password.PasswordEncoder;
12 | import org.springframework.security.web.SecurityFilterChain;
13 |
14 | @Profile("dev")
15 | @Configuration
16 | @EnableWebSecurity
17 | @EnableMethodSecurity
18 | public class NoSecurityConfiguration {
19 |
20 | @Bean
21 | public PasswordEncoder passwordEncoder() {
22 | return new BCryptPasswordEncoder();
23 | }
24 |
25 | @Bean
26 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
27 | return http.authorizeHttpRequests(
28 | auth -> auth.anyRequest().permitAll()
29 | )
30 | .cors(AbstractHttpConfigurer::disable)
31 | .csrf(AbstractHttpConfigurer::disable)
32 | .build();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/security/configuration/WebSecurityConfiguration.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.security.configuration;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.context.annotation.Profile;
6 | import org.springframework.security.config.Customizer;
7 | import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
10 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
11 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
12 | import org.springframework.security.crypto.password.PasswordEncoder;
13 | import org.springframework.security.web.SecurityFilterChain;
14 |
15 | @Profile("!dev")
16 | @Configuration
17 | @EnableWebSecurity
18 | @EnableMethodSecurity
19 | public class WebSecurityConfiguration {
20 |
21 | @Bean
22 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
23 | return http.authorizeHttpRequests(
24 | auth -> auth.requestMatchers("/admin/**")
25 | .hasRole("ADMIN")
26 | .anyRequest()
27 | .authenticated()
28 | )
29 | .httpBasic(Customizer.withDefaults())
30 | .cors(AbstractHttpConfigurer::disable)
31 | .csrf(AbstractHttpConfigurer::disable)
32 | .build();
33 | }
34 |
35 | @Bean
36 | public PasswordEncoder passwordEncoder() {
37 | return new BCryptPasswordEncoder();
38 | }
39 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/security/service/UsersDetailsService.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.security.service;
2 |
3 | import org.springframework.security.core.Authentication;
4 | import org.springframework.security.core.GrantedAuthority;
5 | import org.springframework.security.core.authority.SimpleGrantedAuthority;
6 | import org.springframework.security.core.context.SecurityContextHolder;
7 | import org.springframework.security.core.userdetails.User;
8 | import org.springframework.security.core.userdetails.UserDetails;
9 | import org.springframework.security.core.userdetails.UserDetailsService;
10 | import org.springframework.security.core.userdetails.UsernameNotFoundException;
11 | import org.springframework.stereotype.Service;
12 | import xyz.atsumeru.web.model.database.AtsumeruUser;
13 | import xyz.atsumeru.web.security.repository.UsersRepository;
14 |
15 | import java.util.HashSet;
16 | import java.util.Set;
17 |
18 | @Service
19 | public class UsersDetailsService implements UserDetailsService {
20 | private final UsersRepository repository;
21 |
22 | public UsersDetailsService(UsersRepository repository) {
23 | this.repository = repository;
24 | }
25 |
26 | @Override
27 | public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
28 | AtsumeruUser atsumeruUser = repository.getUserByUsername(userName);
29 | if (atsumeruUser == null) {
30 | throw new UsernameNotFoundException("User with username " + userName + " not found");
31 | }
32 |
33 | Set authorities = new HashSet<>();
34 | for (String authority : atsumeruUser.getAuthoritiesSet()) {
35 | authorities.add(new SimpleGrantedAuthority(authority));
36 | }
37 |
38 | return new User(atsumeruUser.getUserName(), atsumeruUser.getPassword(), authorities);
39 | }
40 |
41 | public static boolean isUserInRole(Authentication authentication, String... roles) {
42 | boolean hasAnyRole = false;
43 | for (GrantedAuthority authority : authentication.getAuthorities()) {
44 | for (String role : roles) {
45 | if (authority.getAuthority().replace("ROLE_", "").equals(role)) {
46 | hasAnyRole = true;
47 | break;
48 | }
49 | }
50 | }
51 | return hasAnyRole;
52 | }
53 |
54 | public static boolean isUserCanDownloadFiles(Authentication authentication) {
55 | return isUserInRole(authentication, "ADMIN", "DOWNLOAD_FILES");
56 | }
57 |
58 | public static boolean isIncludeFileInfoIntoResponse() {
59 | return UsersDetailsService.isUserInRole(
60 | SecurityContextHolder.getContext().getAuthentication(),
61 | "ADMIN", "UPLOADER"
62 | );
63 | }
64 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/util/AppUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.util;
2 |
3 | import java.util.function.Supplier;
4 |
5 | public class AppUtils {
6 |
7 | public static void sleepThread(int millis) {
8 | if (millis > 0) {
9 | try {
10 | Thread.sleep(millis);
11 | } catch (InterruptedException e) {
12 | e.printStackTrace();
13 | }
14 | }
15 | }
16 |
17 | public static void sleepWhile(int millis, Supplier supplier) {
18 | do {
19 | sleepThread(millis);
20 | } while (supplier.get());
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/util/ArrayUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.util;
2 |
3 | import java.util.*;
4 | import java.util.stream.Collectors;
5 |
6 | public class ArrayUtils {
7 |
8 | public static boolean isInSet(Set set, T item) {
9 | return isNotEmpty(set) && set.contains(item);
10 | }
11 |
12 | public static List splitString(String str) {
13 | return splitString(str, ",");
14 | }
15 |
16 | public static List splitString(String str, String regex) {
17 | try {
18 | return Arrays.stream(str.split(regex))
19 | .filter(StringUtils::isNotEmpty)
20 | .collect(Collectors.toList());
21 | } catch (NullPointerException ex) {
22 | return new ArrayList<>();
23 | }
24 | }
25 |
26 | public static String safeGetString(List list, int index, String def) {
27 | try {
28 | return list.get(index);
29 | } catch (Exception ex) {
30 | return def;
31 | }
32 | }
33 |
34 | public static void fillSetWithStringAsArray(Set set, String strArray) {
35 | if (StringUtils.isNotEmpty(strArray)) {
36 | fillSetWithList(set, splitString(strArray, ","));
37 | }
38 | }
39 |
40 | public static void fillSetWithList(Set set, List strings) {
41 | for (String str : strings) {
42 | if (StringUtils.isEmpty(str)) {
43 | continue;
44 | }
45 | set.add(str.trim());
46 | }
47 | }
48 |
49 | public static List getNotNullList(List list) {
50 | return isNotEmpty(list) ? list : new ArrayList<>();
51 | }
52 |
53 | /**
54 | * Check is collection empty
55 | *
56 | * @param collectionMapArray Collection, Map or Array
57 | * @return boolean - true if empty / false if not
58 | */
59 | @SuppressWarnings("rawtypes")
60 | public static boolean isEmpty(Object collectionMapArray, Integer... lengthArr) {
61 | int length = lengthArr != null && lengthArr.length > 0 ? lengthArr[0] : 1;
62 | if (collectionMapArray == null) {
63 | return true;
64 | } else if (collectionMapArray instanceof Collection) {
65 | return ((Collection) collectionMapArray).size() < length;
66 | } else if (collectionMapArray instanceof Map) {
67 | return ((Map) collectionMapArray).size() < length;
68 | } else if (collectionMapArray instanceof Object[]) {
69 | return ((Object[]) collectionMapArray).length < length || ((Object[]) collectionMapArray)[length - 1] == null;
70 | } else return true;
71 | }
72 |
73 | /**
74 | * Acts like {@link #isEmpty(Object, Integer...) isEmpty} method but reverse
75 | *
76 | * @param collectionMapArray Collection, Map or Array
77 | * @return boolean - false if empty / true if not
78 | */
79 | public static boolean isNotEmpty(Object collectionMapArray) {
80 | return !isEmpty(collectionMapArray);
81 | }
82 |
83 | public static boolean isNotNull(Object collectionMapArray) {
84 | return collectionMapArray != null;
85 | }
86 |
87 | public static T getLastItem(List collection) {
88 | if (isNotEmpty(collection)) {
89 | return collection.get(collection.size() - 1);
90 | }
91 | return null;
92 | }
93 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/util/ContentDetector.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.util;
2 |
3 | import org.apache.tika.config.TikaConfig;
4 | import org.apache.tika.exception.TikaException;
5 | import org.apache.tika.io.TikaInputStream;
6 | import org.apache.tika.metadata.Metadata;
7 | import org.apache.tika.metadata.TikaCoreProperties;
8 | import xyz.atsumeru.web.enums.BookType;
9 |
10 | import java.io.IOException;
11 | import java.io.InputStream;
12 | import java.nio.file.Path;
13 | import java.util.Arrays;
14 | import java.util.List;
15 |
16 | public class ContentDetector {
17 | private static TikaConfig tika;
18 | private static final List REPACKABLE_ARCHIVES = Arrays.asList(
19 | "application/zip",
20 | "application/x-rar-compressed",
21 | "application/x-rar-compressed; version=4",
22 | "application/x-7z-compressed"
23 | );
24 |
25 | private static final List BOOK_FILES = Arrays.asList(
26 | "application/epub",
27 | "application/epub+zip",
28 | "application/x-fictionbook",
29 | "application/x-fictionbook+xml",
30 | "application/pdf",
31 | "image/vnd.djvu"
32 | );
33 |
34 | public static final String WEBP_MIME_TYPE = "image/webp";
35 |
36 | public static TikaInputStream createTikaInputStream(InputStream stream) {
37 | return TikaInputStream.get(stream);
38 | }
39 |
40 | public static String detectMediaType(Path path) {
41 | Metadata metadata = new Metadata();
42 | metadata.add(TikaCoreProperties.RESOURCE_NAME_KEY, path.getFileName().toString());
43 |
44 | TikaInputStream tikaInputStream = null;
45 | try {
46 | return tika.getDetector().detect(tikaInputStream = TikaInputStream.get(path), metadata).toString();
47 | } catch (Exception ex) {
48 | return "unknown";
49 | } finally {
50 | FileUtils.closeLoudly(tikaInputStream);
51 | }
52 | }
53 |
54 | public static String detectMediaType(TikaInputStream tikaInputStream) {
55 | try {
56 | return tika.getDetector().detect(tikaInputStream, new Metadata()).toString();
57 | } catch (Exception ex) {
58 | return "unknown";
59 | }
60 | }
61 |
62 | public static BookType detectBookType(Path path) {
63 | String mediaType = detectMediaType(path);
64 | return switch (mediaType) {
65 | case "application/zip", "application/x-rar-compressed", "application/x-rar-compressed; version=4", "application/x-7z-compressed" -> BookType.ARCHIVE;
66 | case "application/epub", "application/epub+zip" -> BookType.EPUB;
67 | case "application/x-fictionbook", "application/x-fictionbook+xml" -> BookType.FB2;
68 | case "application/pdf" -> BookType.PDF;
69 | case "image/vnd.djvu" -> BookType.DJVU;
70 | default -> null;
71 | };
72 | }
73 |
74 | public static boolean isRepackableArchive(Path path) {
75 | return REPACKABLE_ARCHIVES.contains(detectMediaType(path));
76 | }
77 |
78 | public static boolean isBookFile(Path path) {
79 | return BOOK_FILES.contains(detectMediaType(path));
80 | }
81 |
82 | public static boolean isWebP(Path path) {
83 | return StringUtils.equalsIgnoreCase(detectMediaType(path), WEBP_MIME_TYPE);
84 | }
85 |
86 | public static boolean isWebP(TikaInputStream tikaInputStream) {
87 | return StringUtils.equalsIgnoreCase(detectMediaType(tikaInputStream), WEBP_MIME_TYPE);
88 | }
89 |
90 | static {
91 | try {
92 | tika = new TikaConfig();
93 | } catch (TikaException | IOException e) {
94 | e.printStackTrace();
95 | }
96 | }
97 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/util/EnumUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.util;
2 |
3 | import java.util.Arrays;
4 | import java.util.Optional;
5 |
6 | public class EnumUtils {
7 | private static final String ENUM_NAME_PATTERN = "^.|.$";
8 |
9 | /**
10 | * Converts human-readable enum name to allowed enum name replacing spaces to _
11 | *
12 | * @param name {@link String} human readable enum name
13 | * @return {@link String} allowed enum name
14 | */
15 | public static String convertHumanizedToEnumName(String name) {
16 | return Optional.ofNullable(name)
17 | .filter(StringUtils::isNotEmpty)
18 | .map(input -> input.replace(" ", "_").toUpperCase())
19 | .orElse(name);
20 | }
21 |
22 | /**
23 | * Returns {@link String[]} of all names for provided Enum class
24 | *
25 | * @param e enum class
26 | * @return {@link String[]} of all enum names
27 | */
28 | public static String[] getNames(Class extends Enum>> e) {
29 | return Arrays.toString(e.getEnumConstants()).replaceAll(ENUM_NAME_PATTERN, "").split(", ");
30 | }
31 |
32 | /**
33 | * Returns Enum by id
34 | *
35 | * @param e enum
36 | * @param n enum id
37 | * @param type of enum class
38 | * @return enum that match provided id
39 | */
40 | public static > E get(E e, Integer n) {
41 | E[] values = e.getDeclaringClass().getEnumConstants();
42 | for (E value : values) {
43 | if (value.ordinal() == n) {
44 | return value;
45 | }
46 | }
47 | return values[0];
48 | }
49 |
50 | /**
51 | * Returns Enum by id
52 | *
53 | * @param classE enum class
54 | * @param n enum id
55 | * @param type of enum class
56 | * @return enum that match provided id
57 | */
58 | public static > E get(Class classE, Integer n) {
59 | E[] values = classE.getEnumConstants();
60 | for (E value : values) {
61 | if (value.ordinal() == n) {
62 | return value;
63 | }
64 | }
65 | return values[0];
66 | }
67 |
68 | /**
69 | * Returns enum that match provided {@link String} value
70 | *
71 | * @param classE enum class
72 | * @param name {@link String} enum name
73 | * @param type of enum class
74 | * @return enum that match provided {@link String} value
75 | */
76 | public static > E valueOf(Class classE, String name) {
77 | return valueOf(classE, name, classE.getEnumConstants()[0]);
78 | }
79 |
80 | public static > E valueOf(Class classE, String name, E def) {
81 | E[] values = classE.getEnumConstants();
82 | for (E value : values) {
83 | if (value.name().equalsIgnoreCase(name)) {
84 | return value;
85 | }
86 | }
87 | return def;
88 | }
89 |
90 | /**
91 | * Returns enum that match provided {@link String} value or null
92 | *
93 | * @param classE enum class
94 | * @param name {@link String} enum value
95 | * @param type of enum class
96 | * @return enum that match provided {@link String} value or null
97 | */
98 | public static > E valueOfOrNull(Class classE, String name) {
99 | for (E value : classE.getEnumConstants()) {
100 | if (value.name().equalsIgnoreCase(name)) {
101 | return value;
102 | }
103 | }
104 | return null;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/util/LinkUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.util;
2 |
3 | import xyz.atsumeru.web.helper.HashHelper;
4 |
5 | public class LinkUtils {
6 |
7 | /**
8 | * https://subdomain.domain.com/path -> domain
9 | */
10 | public static String getHostName(String link) {
11 | String host = HashHelper.getHost(link);
12 | if (host == null) {
13 | return "";
14 | }
15 | int levelTop = host.lastIndexOf(46);
16 | if (levelTop >= 0) {
17 | host = host.substring(0, levelTop);
18 | }
19 | if (host.contains(".")) {
20 | host = host.substring(host.lastIndexOf(".") + 1);
21 | }
22 | return host;
23 | }
24 |
25 |
26 | public static String getPath(String url) {
27 | if (url.startsWith("/")) {
28 | return url;
29 | }
30 | int i1 = url.indexOf("://");
31 | if (i1 < 0) {
32 | return null;
33 | }
34 | i1 += 3;
35 | i1 = url.indexOf(47, i1);
36 | if (i1 < 0) {
37 | return null;
38 | }
39 | int i2 = url.lastIndexOf(63);
40 | final int i3 = url.lastIndexOf(35);
41 | if (i2 >= 0 && i3 >= 0) {
42 | i2 = Math.min(i2, i3);
43 | } else if (i3 >= 0) {
44 | i2 = i3;
45 | } else if (i2 < 0) {
46 | i2 = url.length();
47 | }
48 | return url.substring(i1, i2).replace("//", "/");
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/xyz/atsumeru/web/util/StreamUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.atsumeru.web.util;
2 |
3 | import java.util.Set;
4 | import java.util.concurrent.ConcurrentHashMap;
5 | import java.util.function.Function;
6 | import java.util.function.Predicate;
7 |
8 | public class StreamUtils {
9 |
10 | public static Predicate distinctByKey(Function super T, ?> keyExtractor) {
11 | Set