├── 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 | Atsumeru Icon 3 |

4 | 5 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/AtsumeruDev/Atsumeru?color=blue&label=Latest%20release&sort=semver)](https://github.com/AtsumeruDev/Atsumeru/releases) [![GitHub all releases](https://img.shields.io/github/downloads/AtsumeruDev/Atsumeru/total?color=blue&label=Downloads)](https://github.com/AtsumeruDev/Atsumeru/releases) 6 | [![Docker Pulls](https://img.shields.io/docker/pulls/atsumerudev/atsumeru)](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 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> 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 keyExtractor) { 11 | Set seen = ConcurrentHashMap.newKeySet(); 12 | return t -> seen.add(keyExtractor.apply(t)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/xyz/atsumeru/web/util/TypeUtils.java: -------------------------------------------------------------------------------- 1 | package xyz.atsumeru.web.util; 2 | 3 | public class TypeUtils { 4 | 5 | public static boolean isTrailingSignificant(Float fFloat) { 6 | return ((fFloat - fFloat.intValue()) > 0); 7 | } 8 | 9 | /** 10 | * Safely parses Long from String. If provided {@param value} can't be parsed, provided default {@param def} 11 | * value will be returned. 12 | * 13 | * @param value - input string for parsing 14 | * @param def - default return value 15 | * @return parsed Long or {@param def} if {@param value} is null or can't be parsed 16 | */ 17 | public static long getLongDef(String value, long def) { 18 | if (value == null) { 19 | return def; 20 | } 21 | try { 22 | return Long.parseLong(value); 23 | } catch (NumberFormatException ex) { 24 | return def; 25 | } 26 | } 27 | 28 | /** 29 | * Safely parses Double from String. If provided {@param value} can't be parsed, provided default {@param def} 30 | * value will be returned. 31 | * 32 | * @param value - input string for parsing 33 | * @param def - default return value 34 | * @return parsed Double or {@param def} if {@param value} is null or can't be parsed 35 | */ 36 | public static double getDoubleDef(String value, long def) { 37 | if (value == null) { 38 | return def; 39 | } 40 | try { 41 | return Double.parseDouble(value); 42 | } catch (NumberFormatException ex) { 43 | return def; 44 | } 45 | } 46 | 47 | /** 48 | * Safely parses Float from String. If provided {@param value} can't be parsed, provided default {@param def} 49 | * value will be returned. 50 | * 51 | * @param value - input string for parsing 52 | * @param def - default return value 53 | * @return parsed Float or {@param def} if {@param value} is null or can't be parsed 54 | */ 55 | public static float getFloatDef(String value, float def) { 56 | if (value == null) { 57 | return def; 58 | } 59 | try { 60 | return Float.parseFloat(value); 61 | } catch (NumberFormatException ex) { 62 | return def; 63 | } 64 | } 65 | 66 | /** 67 | * Safely parses Integer from String. If provided {@param value} can't be parsed, provided default {@param def} 68 | * value will be returned. 69 | * 70 | * @param value - input string for parsing 71 | * @param def - default return value 72 | * @return parsed Integer or {@param def} if {@param value} is null or can't be parsed 73 | */ 74 | public static int getIntDef(String value, int def) { 75 | if (value == null) { 76 | return def; 77 | } 78 | try { 79 | return Integer.parseInt(value); 80 | } catch (NumberFormatException ex) { 81 | return def; 82 | } 83 | } 84 | 85 | /** 86 | * Safely parses Boolean from String. If provided {@param value} can't be parsed, provided default {@param def} 87 | * value will be returned. 88 | * 89 | * @param value - input string for parsing 90 | * @param def - default return value 91 | * @return parsed Boolean or {@param def} if {@param value} is null or can't be parsed 92 | */ 93 | public static boolean getBoolDef(String value, boolean def) { 94 | if (value == null) { 95 | return def; 96 | } 97 | try { 98 | return Boolean.parseBoolean(value); 99 | } catch (NumberFormatException ex) { 100 | return def; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/xyz/atsumeru/web/util/comparator/AlphanumComparator.java: -------------------------------------------------------------------------------- 1 | package xyz.atsumeru.web.util.comparator; 2 | 3 | import java.util.Comparator; 4 | 5 | public class AlphanumComparator implements Comparator { 6 | 7 | @Override 8 | public int compare(T obj1, T obj2) { 9 | return compareStrings(obj1.toString(), obj2.toString()); 10 | } 11 | 12 | public static int compareObjToString(T obj1, T obj2) { 13 | return compareStrings(obj1.toString(), obj2.toString()); 14 | } 15 | 16 | public static int compareStrings(String string1, String string2) { 17 | int thisMarker = 0; 18 | int thatMarker = 0; 19 | int s1Length = string1.length(); 20 | int s2Length = string2.length(); 21 | while (thisMarker < s1Length && thatMarker < s2Length) { 22 | String thisChunk = getChunk(string1, s1Length, thisMarker); 23 | thisMarker += thisChunk.length(); 24 | String thatChunk = getChunk(string2, s2Length, thatMarker); 25 | thatMarker += thatChunk.length(); 26 | int result; 27 | if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0))) { 28 | int thisChunkLength = thisChunk.length(); 29 | result = thisChunkLength - thatChunk.length(); 30 | if (result == 0) { 31 | for (int i = 0; i < thisChunkLength; ++i) { 32 | result = thisChunk.charAt(i) - thatChunk.charAt(i); 33 | if (result != 0) { 34 | return result; 35 | } 36 | } 37 | } 38 | } else { 39 | result = thisChunk.compareTo(thatChunk); 40 | } 41 | if (result != 0) { 42 | return result; 43 | } 44 | } 45 | return s1Length - s2Length; 46 | } 47 | 48 | private static boolean isDigit(char ch) { 49 | return ch >= '0' && ch <= '9'; 50 | } 51 | 52 | private static String getChunk(String string, int stringLength, int marker) { 53 | final StringBuilder chunk = new StringBuilder(); 54 | char c = string.charAt(marker); 55 | chunk.append(c); 56 | ++marker; 57 | if (isDigit(c)) { 58 | while (marker < stringLength) { 59 | c = string.charAt(marker); 60 | if (!isDigit(c)) { 61 | break; 62 | } 63 | chunk.append(c); 64 | ++marker; 65 | } 66 | } else { 67 | while (marker < stringLength) { 68 | c = string.charAt(marker); 69 | if (isDigit(c)) { 70 | break; 71 | } 72 | chunk.append(c); 73 | ++marker; 74 | } 75 | } 76 | return chunk.toString(); 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/java/xyz/atsumeru/web/util/comparator/NaturalStringComparator.java: -------------------------------------------------------------------------------- 1 | package xyz.atsumeru.web.util.comparator; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Comparator; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | public class NaturalStringComparator implements Comparator { 11 | private static final Pattern splitPattern = Pattern.compile("[0-90-9]+|\\.|\\s"); 12 | 13 | @Override 14 | public int compare(String str1, String str2) { 15 | return compareStrings(str1, str2); 16 | } 17 | 18 | public static int compareStrings(String str1, String str2) { 19 | Iterator i1 = splitStringPreserveDelimiter(str1).iterator(); 20 | Iterator i2 = splitStringPreserveDelimiter(str2).iterator(); 21 | while (true) { 22 | //Til here all is equal. 23 | if (!i1.hasNext() && !i2.hasNext()) { 24 | return 0; 25 | } 26 | //first has no more parts -> comes first 27 | if (!i1.hasNext()) { 28 | return -1; 29 | } 30 | //first has more parts than i2 -> comes after 31 | if (!i2.hasNext()) { 32 | return 1; 33 | } 34 | 35 | String data1 = i1.next(); 36 | String data2 = i2.next(); 37 | int result; 38 | try { 39 | //If both datas are numbers, then compare numbers 40 | result = Long.compare(Long.parseLong(data1), Long.parseLong(data2)); 41 | //If numbers are equal than longer comes first 42 | if (result == 0) { 43 | result = -Integer.compare(data1.length(), data2.length()); 44 | } 45 | } catch (NumberFormatException ex) { 46 | //compare text case insensitive 47 | result = data1.compareToIgnoreCase(data2); 48 | } 49 | 50 | if (result != 0) { 51 | return result; 52 | } 53 | } 54 | } 55 | 56 | private static List splitStringPreserveDelimiter(String str) { 57 | Matcher matcher = splitPattern.matcher(str); 58 | List list = new ArrayList<>(); 59 | int pos = 0; 60 | while (matcher.find()) { 61 | list.add(str.substring(pos, matcher.start())); 62 | list.add(matcher.group()); 63 | pos = matcher.end(); 64 | } 65 | list.add(str.substring(pos)); 66 | return list; 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Application name 2 | spring.application.name=Atsumeru 3 | 4 | # Server default port 5 | server.port=31337 6 | 7 | # Active (default) profile 8 | spring.profiles.active=production 9 | 10 | # GSON 11 | spring.mvc.converters.preferred-json-mapper=gson 12 | 13 | # Increase header buffer size 14 | server.max-http-request-header-size=15360 15 | 16 | # SwaggerUI configuration 17 | springdoc.swagger-ui.tagsSorter=alpha -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | _ _ 3 | / \ | |_ ___ _ _ _ __ ___ ___ _ __ _ _ 4 | / _ \| __/ __| | | | '_ ` _ \ / _ \ '__| | | | 5 | / ___ \ |_\__ \ |_| | | | | | | __/ | | |_| | 6 | /_/ \_\__|___/\__,_|_| |_| |_|\___|_| \__,_| 7 | 8 | Version: ${application.formatted-version} 9 | --------------------------------------------------------------------------------