This commit is contained in:
wushuo
2026-02-26 01:12:45 +08:00
parent e6bd84e7bd
commit d102946e2a
247 changed files with 3506 additions and 3719 deletions

View File

@@ -16,13 +16,13 @@ jobs:
- name: Build with Project
run: |
bash ./package.sh
version=$(cat pom.xml | grep -oPm1 '(?<=<version>).*?(?=</version>)')
version=$(cat ani-rss-application/pom.xml | grep -oPm1 '(?<=<version>).*?(?=</version>)')
echo "version=v$version" >> $GITHUB_ENV
- name: Upload to Artifacts
uses: actions/upload-artifact@v4
with:
name: Artifacts
path: ./ani-rss-application/target/ani-rss-jar-with-dependencies.jar
path: ./ani-rss-application/target/ani-rss.jar
- name: Login to Docker Hub
uses: docker/login-action@v3
with:

View File

@@ -16,10 +16,9 @@ jobs:
- name: Build with Project
run: |
bash ./package.sh
bash ./package-win.sh
time=$(date +%s%3N)
version=$(cat pom.xml | grep -oPm1 '(?<=<version>).*?(?=</version>)')
version=$(cat ani-rss-application/pom.xml | grep -oPm1 '(?<=<version>).*?(?=</version>)')
echo "{\"time\":$time,\"version\":\"$version\"}" > info.json
jq --arg content "$(cat UPDATE.md)" '. += {markdown: $content}' info.json > temp.json && mv temp.json info.json
@@ -35,12 +34,10 @@ jobs:
append_body: false
token: ${{ secrets.GITHUB_TOKEN }}
files: |
./ani-rss-application/target/ani-rss-jar-with-dependencies.jar
./ani-rss-application/target/ani-rss-jar-with-dependencies.jar.md5
./ani-rss-application/target/ani-rss-launcher.exe
./ani-rss-application/target/ani-rss-launcher.exe.md5
./ani-rss-application/target/ani-rss.win.x86_64.zip
./ani-rss-application/target/ani-rss.win.x86_64.zip.md5
./ani-rss-application/target/ani-rss.jar
./ani-rss-application/target/ani-rss.jar.md5
./ani-rss-application/target/ani-rss.exe
./ani-rss-application/target/ani-rss.exe.md5
./info.json
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

54
.gitignore vendored
View File

@@ -1,57 +1,7 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
package-lock.json
pnpm-lock.yaml
### IntelliJ IDEA ###
.idea
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
config.json
ani.json
files
target
logs
*.log
/config
.git
java-17-openjdk-17.0.3.0.6-1.jre.win.x86_64.zip
*.log
ani-rss-update.exe
/info.json
build_info
*.versionsBackup
jre.zip

View File

@@ -1,4 +1,35 @@
- refactor: 优化通知测试
## 增加Swagger接口文档
设置环境变量 `SWAGGER_ENABLED=true`
通过链接访问 http://127.0.0.1:7789/swagger-ui/index.html
## 破坏性改动
### 环境变量与参数
| 旧 | 新 |
|----------|--------------------|
| `--port` | `--server.port` |
| `--host` | `--server.address` |
| `PORT` | `SERVER_PORT` |
| `HOST` | `SERVER_ADDRESS` |
### emby点格子
旧: `http://[IP]:7789/api/web_hook?s=[ApiKey]`
新: `http://[IP]:7789/api/embyWebhook?s=[ApiKey]`
## Windows端
不再提供内置jdk的压缩包, 需自行安装
## 此次更新方式
Docker需要重新部署
Windows需要手动重新下载
[请不要将本项目在国内宣传](https://github.com/wushuo894/ani-rss/discussions/504)

33
ani-rss-application/.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

View File

@@ -1,12 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ani.rss</groupId>
<artifactId>ani-rss</artifactId>
<version>2.5.10</version>
<version>3.0.0</version>
</parent>
<artifactId>ani-rss-application</artifactId>
@@ -17,14 +16,88 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
<repository>
<id>ebml-reader</id>
<url>https://raw.github.com/wushuo894/EBMLReader/mvn-repo</url>
</repository>
<repository>
<id>tmdb-api</id>
<url>https://raw.github.com/wushuo894/tmdb-api/mvn-repo</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>4.0.0-M2</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>bittorrent</artifactId>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>io.github.biezhi</groupId>
<artifactId>TinyPinyin</artifactId>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>ebml.reader</groupId>
<artifactId>ebml-reader</artifactId>
</dependency>
<dependency>
<groupId>wushuo.tmdb.api</groupId>
<artifactId>tmdb-api</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>ani.rss</groupId>
<artifactId>ani-rss-ui</artifactId>
</dependency>
<dependency>
<groupId>ani.rss</groupId>
<artifactId>ani-rss-web</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
@@ -32,28 +105,8 @@
<finalName>ani-rss</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>ani.rss.ApplicationMain</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.akathist.maven.plugins.launch4j</groupId>
@@ -67,8 +120,8 @@
</goals>
<configuration>
<headerType>gui</headerType>
<outfile>target/ani-rss-launcher.exe</outfile>
<jar>target/ani-rss-jar-with-dependencies.jar</jar>
<outfile>target/ani-rss.exe</outfile>
<jar>target/ani-rss.jar</jar>
<errTitle>Java environment is required!</errTitle>
<cmdLine>--gui</cmdLine>
<chdir>.</chdir>
@@ -79,11 +132,11 @@
<restartOnCrash>false</restartOnCrash>
<icon>${project.parent.basedir}/ani-rss-ui/public/favicon.ico</icon>
<singleInstance>
<mutexName>ani-rss-launcher (${project.version})</mutexName>
<mutexName>ani-rss (${project.version})</mutexName>
<windowTitle>ani-rss (${project.version})</windowTitle>
</singleInstance>
<jre>
<path>jre/bin;%JAVA_HOME%/bin;%PATH%</path>
<path>%JAVA_HOME%/bin;%PATH%</path>
<minVersion>17</minVersion>
<opts>
<opt>-Xms60m -Xmx1g -Xss256k</opt>
@@ -99,7 +152,7 @@
<copyright>Copyright (C) 2024-2025</copyright>
<productName>${project.artifactId}</productName>
<internalName>${project.artifactId}</internalName>
<originalFilename>ani-rss-launcher.exe</originalFilename>
<originalFilename>ani-rss.exe</originalFilename>
<language>SIMPLIFIED_CHINESE</language>
</versionInfo>
</configuration>
@@ -144,4 +197,5 @@
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,33 @@
package ani.rss;
import ani.rss.entity.Global;
import ani.rss.util.other.MenuUtil;
import cn.hutool.core.util.ObjectUtil;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.security.Security;
import java.util.List;
@EnableScheduling
@SpringBootApplication
public class AniRssApplication {
public static void main(String[] args) {
Global.ARGS = List.of(ObjectUtil.defaultIfNull(args, new String[]{}));
loadProperty();
MenuUtil.start();
SpringApplication.run(AniRssApplication.class, args);
}
public static void loadProperty() {
// 启用Basic认证
System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
// DNS解析成功过期时间
Security.setProperty("networkaddress.cache.ttl", "30");
// DNS解析失败过期时间
Security.setProperty("networkaddress.cache.negative.ttl", "5");
}
}

View File

@@ -1,54 +0,0 @@
package ani.rss;
import ani.rss.commons.ExceptionUtils;
import ani.rss.commons.MavenUtils;
import ani.rss.entity.Global;
import ani.rss.other.Cron;
import ani.rss.service.TaskService;
import ani.rss.util.other.AniUtil;
import ani.rss.util.other.ConfigUtil;
import ani.rss.util.other.MenuUtil;
import ani.rss.web.util.ServerUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RuntimeUtil;
import lombok.extern.slf4j.Slf4j;
import java.security.Security;
import java.util.List;
@Slf4j
public class ApplicationMain {
public static void main(String[] args) {
Global.ARGS = List.of(ObjectUtil.defaultIfNull(args, new String[]{}));
loadProperty();
try {
ConfigUtil.load();
ConfigUtil.backup();
MenuUtil.start();
ServerUtil.start();
AniUtil.load();
TaskService.start();
String version = MavenUtils.getVersion();
log.info("version {}", version);
Cron.start();
} catch (Exception e) {
String message = ExceptionUtils.getMessage(e);
log.error(message, e);
System.exit(1);
}
RuntimeUtil.addShutdownHook(() -> log.info("程序退出..."));
}
public static void loadProperty() {
// 启用Basic认证
System.setProperty("jdk.http.auth.tunneling.disabledSchemes", "");
// DNS解析成功过期时间
Security.setProperty("networkaddress.cache.ttl", "30");
// DNS解析失败过期时间
Security.setProperty("networkaddress.cache.negative.ttl", "5");
}
}

View File

@@ -1,24 +0,0 @@
package ani.rss.action;
import ani.rss.util.other.UpdateUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
/**
* 关于
*/
@Auth
@Slf4j
@Path("/about")
public class AboutAction implements BaseAction {
@Override
public void doAction(HttpServerRequest req, HttpServerResponse res) {
resultSuccess(UpdateUtil.about());
}
}

View File

@@ -1,102 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Config;
import ani.rss.entity.Result;
import ani.rss.entity.TryOut;
import ani.rss.util.basic.HttpReq;
import ani.rss.util.other.AfdianUtil;
import ani.rss.util.other.ConfigUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import ani.rss.web.auth.enums.AuthType;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Date;
/**
* 爱发电
*/
@Auth(type = {
AuthType.IP_WHITE_LIST,
AuthType.HEADER,
AuthType.FORM
})
@Slf4j
@Path("/afdian")
public class AfdianAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String type = request.getParam("type");
if (type.equals("verifyNo")) {
Config config = getBody(Config.class);
String outTradeNo = config.getOutTradeNo();
Result<Void> result = AfdianUtil.verifyNo(outTradeNo);
result(result);
int code = result.getCode();
if (code == 200) {
Long time = DateUtil.offsetYear(new Date(), 999).getTime();
ConfigUtil.CONFIG.setOutTradeNo(outTradeNo)
.setExpirationTime(time)
.setTryOut(false);
ConfigUtil.sync();
}
return;
}
if (type.equals("tryOut")) {
Config config = getBody(Config.class);
if (AfdianUtil.verifyExpirationTime()) {
resultError(result ->
result
.setMessage("还在试用中!")
);
return;
}
String githubToken = config.getGithubToken();
Assert.notBlank(githubToken, "GithubToken 不能为空");
Boolean ok = HttpReq.get("https://api.github.com/user/starred/wushuo894/ani-rss")
.header("Authorization", "Bearer " + githubToken)
.thenFunction(HttpResponse::isOk);
Assert.isTrue(ok, "未点击star");
TryOut tryOut = AfdianUtil.getTryOut();
Boolean enable = tryOut.getEnable();
Boolean renewal = tryOut.getRenewal();
Integer day = tryOut.getDay();
String message = tryOut.getMessage();
Assert.isTrue(enable, message);
if (config.getTryOut()) {
// 已经有过试用
Assert.isTrue(renewal, message);
}
long time = DateUtil.offsetDay(new Date(), day).getTime();
ConfigUtil.CONFIG
.setGithubToken(githubToken)
.setExpirationTime(time)
.setTryOut(true);
ConfigUtil.sync();
resultSuccess(result ->
result
.setMessage(message)
.setData(time)
);
}
}
}

View File

@@ -1,77 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Ani;
import ani.rss.entity.BgmInfo;
import ani.rss.util.other.AniUtil;
import ani.rss.util.other.BgmUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.lang.Opt;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import com.google.gson.JsonObject;
import lombok.extern.slf4j.Slf4j;
import wushuo.tmdb.api.entity.Tmdb;
import java.io.IOException;
import java.util.Objects;
/**
* bgm
*/
@Auth
@Slf4j
@Path("/bgm")
public class BgmAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String type = request.getParam("type");
switch (type) {
case "search" -> {
// 搜索
String name = request.getParam("name");
resultSuccess(BgmUtil.search(name));
}
case "getAniBySubjectId" -> {
// 将指定id的BGM番剧转换为订阅
String id = request.getParam("id");
BgmInfo bgmInfo = BgmUtil.getBgmInfo(id, true);
Ani ani = BgmUtil.toAni(bgmInfo, AniUtil.createAni());
ani
.setCustomDownloadPath(true);
resultSuccess(ani);
}
case "getTitle" -> {
// 获取BGM标题
Ani ani = getBody(Ani.class);
Tmdb tmdb = ani.getTmdb();
BgmInfo bgmInfo = BgmUtil.getBgmInfo(ani);
resultSuccess(BgmUtil.getFinalName(bgmInfo, tmdb));
}
case "rate" -> {
// 评分
Ani ani = getBody(Ani.class);
String subjectId = BgmUtil.getSubjectId(ani);
Integer score = Opt.ofNullable(ani.getScore())
.map(Double::intValue)
.orElse(null);
resultSuccess(result -> {
result.setData(BgmUtil.rate(subjectId, score))
.setMessage("保存评分成功");
if (Objects.isNull(score)) {
result.setMessage("");
}
});
}
case "me" -> {
// 获取当前BGM账号信息
Long expiresDays = BgmUtil.getExpiresDays();
JsonObject me = BgmUtil.me();
me.addProperty("expires_days", expiresDays);
resultSuccess(me);
}
}
}
}

View File

@@ -1,53 +0,0 @@
package ani.rss.action;
import ani.rss.commons.GsonStatic;
import ani.rss.entity.Config;
import ani.rss.util.basic.HttpReq;
import ani.rss.util.other.ConfigUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import com.google.gson.JsonObject;
import java.io.IOException;
import java.util.Map;
/**
* BGM 授权回调
*/
@Auth
@Path("/bgm/oauth/callback")
public class BgmCallbackAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String code = request.getParam("code");
Config config = ConfigUtil.CONFIG;
String bgmAppID = config.getBgmAppID();
String bgmAppSecret = config.getBgmAppSecret();
String bgmRedirectUri = config.getBgmRedirectUri();
Map<String, String> map = Map.of(
"grant_type", "authorization_code",
"client_id", bgmAppID,
"client_secret", bgmAppSecret,
"code", code,
"redirect_uri", bgmRedirectUri
);
HttpReq.post("https://bgm.tv/oauth/access_token")
.body(GsonStatic.toJson(map))
.then(res -> {
HttpReq.assertStatus(res);
JsonObject jsonObject = GsonStatic.fromJson(res.body(), JsonObject.class);
String accessToken = jsonObject.get("access_token").getAsString();
String refreshToken = jsonObject.get("refresh_token").getAsString();
config.setBgmToken(accessToken)
.setBgmRefreshToken(refreshToken);
});
ConfigUtil.sync();
resultSuccessMsg("授权成功, 现在你可以关闭此窗口");
}
}

View File

@@ -1,72 +0,0 @@
package ani.rss.action;
import ani.rss.commons.FileUtils;
import ani.rss.entity.Ani;
import ani.rss.service.ClearService;
import ani.rss.util.other.AniUtil;
import ani.rss.util.other.ConfigUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 缓存清理
*/
@Slf4j
@Auth
@Path("/clearCache")
public class ClearCacheAction implements BaseAction {
@Override
public synchronized void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
File configDir = ConfigUtil.getConfigDir();
String configDirStr = FileUtils.getAbsolutePath(configDir);
Set<String> covers = AniUtil.ANI_LIST
.stream()
.map(Ani::getCover)
.map(s -> FileUtils.getAbsolutePath(new File(configDirStr + "/files/" + s)))
.collect(Collectors.toSet());
FileUtil.mkdir(configDirStr + "/files");
FileUtil.mkdir(configDirStr + "/img");
Set<File> files = FileUtil.loopFiles(configDirStr + "/files")
.stream()
.filter(file -> {
String fileName = FileUtils.getAbsolutePath(file);
return !covers.contains(fileName);
}).collect(Collectors.toSet());
long filesSize = files.stream()
.mapToLong(File::length)
.sum();
long imgSize = FileUtil.size(new File(configDirStr + "/img"));
long sumSize = filesSize + imgSize;
if (sumSize < 1) {
resultSuccessMsg("清理完成, 共清理{}MB", 0);
return;
}
for (File file : files) {
FileUtil.del(file);
ClearService.clearParentFile(file);
}
FileUtil.del(configDirStr + "/img");
resultSuccessMsg("清理完成, 共清理{}MB", NumberUtil.decimalFormat("0.00", sumSize / 1024.0 / 1024.0));
}
}

View File

@@ -1,123 +0,0 @@
package ani.rss.action;
import ani.rss.commons.MavenUtils;
import ani.rss.entity.Config;
import ani.rss.entity.Login;
import ani.rss.service.TaskService;
import ani.rss.util.other.AfdianUtil;
import ani.rss.util.other.ConfigUtil;
import ani.rss.util.other.TorrentUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.IOException;
import java.util.Objects;
/**
* 设置
*/
@Auth
@Path("/config")
public class ConfigAction implements BaseAction {
@Override
public void doAction(HttpServerRequest req, HttpServerResponse res) throws IOException {
String method = req.getMethod();
if (method.equals("GET")) {
String version = MavenUtils.getVersion();
String buildInfo = buildInfo();
Config config = ObjectUtil.clone(ConfigUtil.CONFIG);
config.getLogin().setPassword("");
config.setVersion(version)
.setBuildInfo(buildInfo)
.setVerifyExpirationTime(AfdianUtil.verifyExpirationTime());
resultSuccess(config);
return;
}
if (!method.equals("POST")) {
return;
}
Config config = ConfigUtil.CONFIG;
Login login = config.getLogin();
String username = login.getUsername();
String password = login.getPassword();
Integer renameSleepSeconds = config.getRenameSleepSeconds();
Integer sleep = config.getRssSleepMinutes();
String download = config.getDownloadToolType();
Config newConfig = getBody(Config.class);
newConfig.setExpirationTime(null)
.setOutTradeNo(null)
.setTryOut(null);
CopyOptions copyOptions = CopyOptions
.create()
.setIgnoreNullValue(true);
BeanUtil.copyProperties(
newConfig,
config,
copyOptions
);
String loginPassword = config.getLogin().getPassword();
// 密码未发生修改
if (StrUtil.isBlank(loginPassword)) {
config.getLogin().setPassword(password);
}
String loginUsername = config.getLogin().getUsername();
if (StrUtil.isBlank(loginUsername)) {
config.getLogin().setUsername(username);
}
Boolean proxy = config.getProxy();
if (proxy) {
String proxyHost = config.getProxyHost();
Integer proxyPort = config.getProxyPort();
if (StrUtil.isBlank(proxyHost) || Objects.isNull(proxyPort)) {
resultErrorMsg("代理参数不完整");
return;
}
}
ConfigUtil.sync();
Integer newRenameSleepSeconds = config.getRenameSleepSeconds();
Integer newSleep = config.getRssSleepMinutes();
// 时间间隔发生改变,重启任务
if (
!Objects.equals(newSleep, sleep) ||
!Objects.equals(newRenameSleepSeconds, renameSleepSeconds)
) {
TaskService.restart();
}
// 下载工具发生改变
if (!download.equals(config.getDownloadToolType())) {
TorrentUtil.load();
}
resultSuccessMsg("修改成功");
}
/**
* 构建信息
*/
public String buildInfo() {
String buildInfo = "";
try {
buildInfo = ResourceUtil.readUtf8Str("build_info");
} catch (Exception ignored) {
}
return buildInfo;
}
}

View File

@@ -1,27 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Ani;
import ani.rss.util.other.AniUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* 刷新封面
*/
@Slf4j
@Auth
@Path("/cover")
public class CoverAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
Ani ani = getBody(Ani.class);
String s = AniUtil.saveJpg(ani.getImage(), true);
resultSuccess(s);
}
}

View File

@@ -1,33 +0,0 @@
package ani.rss.action;
import ani.rss.util.other.ConfigUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.Header;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* 自定义css
*/
@Slf4j
@Auth(value = false)
@Path("/custom.css")
public class CustomCssAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
response.setHeader(Header.CACHE_CONTROL, "no-store, no-cache, must-revalidate, max-age=0");
response.setHeader(Header.PRAGMA, "no-cache");
response.setHeader("Expires", "0");
String customCss = ConfigUtil.CONFIG.getCustomCss();
customCss = StrUtil.blankToDefault(customCss, "/* empty css */");
String contentType = "text/css";
response.write(customCss, contentType);
}
}

View File

@@ -1,33 +0,0 @@
package ani.rss.action;
import ani.rss.util.other.ConfigUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.Header;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* 自定义js
*/
@Slf4j
@Auth(value = false)
@Path("/custom.js")
public class CustomJsAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
response.setHeader(Header.CACHE_CONTROL, "no-store, no-cache, must-revalidate, max-age=0");
response.setHeader(Header.PRAGMA, "no-cache");
response.setHeader("Expires", "0");
String customJs = ConfigUtil.CONFIG.getCustomJs();
customJs = StrUtil.blankToDefault(customJs, "// empty js");
String contentType = "application/javascript; charset=utf-8";
response.write(customJs, contentType);
}
}

View File

@@ -1,53 +0,0 @@
package ani.rss.action;
import ani.rss.util.other.ConfigUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import cn.hutool.http.Header;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.Cleanup;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
/**
* 下载日志
*/
@Auth
@Path("/downloadLogs")
public class DownloadLogsAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
File configDir = ConfigUtil.getConfigDir();
String logsPath = configDir + "/logs";
String filename = "logs.zip";
String contentType = getContentType(filename);
response.setContentType(contentType);
response.setHeader(Header.CONTENT_DISPOSITION, StrFormatter.format("inline; filename=\"{}\"", filename));
@Cleanup
OutputStream outputStream = response.getOut();
ZipUtil.zip(outputStream, StandardCharsets.UTF_8, false, name -> {
if (FileUtil.isDirectory(name)) {
return true;
}
String extName = FileUtil.extName(name);
if (StrUtil.isBlank(extName)) {
return false;
}
return extName.equals("log");
}, new File(logsPath));
}
}

View File

@@ -1,45 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Ani;
import ani.rss.service.DownloadService;
import ani.rss.util.other.AniUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
/**
* 获取下载位置
*/
@Auth
@Path("/downloadPath")
public class DownloadPathAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
Ani ani = getBody(Ani.class);
String downloadPath = DownloadService.getDownloadPath(ani);
boolean change = false;
Optional<Ani> first = AniUtil.ANI_LIST.stream()
.filter(it -> it.getId().equals(ani.getId()))
.findFirst();
if (first.isPresent()) {
Ani oldAni = ObjectUtil.clone(first.get());
// 只在名称改变时移动
oldAni.setSeason(ani.getSeason());
String oldDownloadPath = DownloadService.getDownloadPath(oldAni);
change = !downloadPath.equals(oldDownloadPath);
}
resultSuccess(Map.of(
"change", change,
"downloadPath", downloadPath
));
}
}

View File

@@ -1,36 +0,0 @@
package ani.rss.action;
import ani.rss.download.BaseDownload;
import ani.rss.entity.Config;
import ani.rss.util.other.ConfigUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.IOException;
/**
* 测试下载工具
*/
@Auth
@Path("/downloadLoginTest")
public class DownloadTestAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
Config config = getBody(Config.class);
ConfigUtil.format(config);
String download = config.getDownloadToolType();
Class<Object> loadClass = ClassUtil.loadClass("ani.rss.download." + download);
BaseDownload baseDownload = (BaseDownload) ReflectUtil.newInstance(loadClass);
Boolean login = baseDownload.login(true, config);
if (login) {
resultSuccessMsg("登录成功");
return;
}
resultErrorMsg("登录失败");
}
}

View File

@@ -1,39 +0,0 @@
package ani.rss.action;
import ani.rss.entity.EmbyViews;
import ani.rss.entity.NotificationConfig;
import ani.rss.util.other.EmbyUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.IOException;
import java.util.List;
/**
* Emby
*/
@Auth
@Path("/emby")
public class EmbyAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
NotificationConfig notificationConfig = getBody(NotificationConfig.class);
String type = request.getParam("type");
if (type.equals("getViews")) {
List<EmbyViews> views = EmbyUtil.getViews(notificationConfig);
resultSuccess(views);
return;
}
if (type.equals("refresh")) {
EmbyUtil.refresh(notificationConfig);
resultSuccess();
}
}
}

View File

@@ -1,209 +0,0 @@
package ani.rss.action;
import ani.rss.commons.ExceptionUtils;
import ani.rss.util.basic.HttpReq;
import ani.rss.util.other.ConfigUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import ani.rss.web.util.ServerUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpConnection;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.net.URI;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.util.function.Consumer;
/**
* 文件
*/
@Slf4j
@Auth
@Path("/file")
public class FileAction implements BaseAction {
public void getImg(String url, Consumer<InputStream> consumer) {
URI host = URLUtil.getHost(URLUtil.url(url));
HttpReq.get(url)
.then(res -> {
HttpConnection httpConnection = (HttpConnection) ReflectUtil.getFieldValue(res, "httpConnection");
URI host1 = URLUtil.getHost(httpConnection.getUrl());
if (host.toString().equals(host1.toString())) {
try {
@Cleanup
InputStream inputStream = res.bodyStream();
consumer.accept(inputStream);
} catch (Exception ignored) {
}
return;
}
String newUrl = url.replace(host.toString(), host1.toString());
getImg(newUrl, consumer);
});
}
/**
* 处理图片文件
*
* @param img 图片名
*/
public void doImg(String img) {
HttpServerResponse response = ServerUtil.RESPONSE.get();
// 30 天
long maxAge = 86400 * 30;
response.setHeader(Header.CACHE_CONTROL, "private, max-age=" + maxAge);
String contentType = getContentType(URLUtil.getPath(img));
File configDir = ConfigUtil.getConfigDir();
File file = new File(URLUtil.getPath(img));
configDir = new File(configDir + "/img/" + file.getParentFile().getName());
FileUtil.mkdir(configDir);
File imgFile = new File(configDir, file.getName());
if (imgFile.exists()) {
try {
@Cleanup
InputStream inputStream = FileUtil.getInputStream(imgFile);
response.write(inputStream, (int) imgFile.length(), contentType);
} catch (Exception ignored) {
}
return;
}
getImg(img, is -> {
try {
FileUtil.writeFromStream(is, imgFile, true);
@Cleanup
BufferedInputStream inputStream = FileUtil.getInputStream(imgFile);
response.write(inputStream, (int) imgFile.length(), contentType);
} catch (Exception ignored) {
}
});
}
/**
* 处理文件
*
* @param filename 文件名
*/
private void doFile(String filename) {
HttpServerRequest request = ServerUtil.REQUEST.get();
HttpServerResponse response = ServerUtil.RESPONSE.get();
File file = new File(filename);
if (!file.exists()) {
File configDir = ConfigUtil.getConfigDir();
file = new File(configDir + "/files/" + filename);
if (!file.exists()) {
BaseAction.writeNotFound();
return;
}
}
boolean hasRange = false;
long fileLength = file.length();
long start = 0;
long end = fileLength - 1;
String contentType = getContentType(file.getName());
response.setHeader(Header.CONTENT_DISPOSITION, StrFormatter.format("inline; filename=\"{}\"", URLUtil.encode(file.getName())));
if (contentType.startsWith("video/")) {
response.setContentType(contentType);
response.setHeader("Accept-Ranges", "bytes");
String rangeHeader = request.getHeader("Range");
if (StrUtil.isNotBlank(rangeHeader) && rangeHeader.startsWith("bytes=")) {
String[] range = rangeHeader.substring(6).split("-");
if (range.length > 0) {
start = Long.parseLong(range[0]);
}
if (range.length > 1) {
end = Long.parseLong(range[1]);
} else {
long maxEnd = start + (1024 * 1024 * 10);
end = Math.min(end, maxEnd);
}
}
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength);
hasRange = true;
} else {
long maxAge = 0;
// 小于或者等于 3M 缓存
if (fileLength <= 1024 * 1024 * 3) {
// 30 天
maxAge = 86400 * 30;
}
response.setHeader(Header.CACHE_CONTROL, "private, max-age=" + maxAge);
response.setContentType(contentType);
}
try {
if (hasRange) {
long length = end - start;
response.send(206, length);
@Cleanup
OutputStream out = response.getOut();
@Cleanup
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
randomAccessFile.seek(start);
@Cleanup
FileChannel channel = randomAccessFile.getChannel();
@Cleanup
InputStream inputStream = Channels.newInputStream(channel);
IoUtil.copy(inputStream, out, 40960, length, null);
} else {
@Cleanup
InputStream inputStream = FileUtil.getInputStream(file);
response.write(inputStream, (int) fileLength);
}
} catch (Exception e) {
String message = ExceptionUtils.getMessage(e);
log.debug(message, e);
}
}
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String img = request.getParam("img");
if (StrUtil.isNotBlank(img)) {
if (Base64.isBase64(img)) {
img = Base64.decodeStr(img);
}
doImg(img);
return;
}
String filename = request.getParam("filename");
if (StrUtil.isBlank(filename)) {
BaseAction.writeNotFound();
return;
}
if (Base64.isBase64(filename)) {
filename = Base64.decodeStr(filename);
}
doFile(filename);
}
}

View File

@@ -1,77 +0,0 @@
package ani.rss.action;
import ani.rss.dto.ImportAniDataDTO;
import ani.rss.entity.Ani;
import ani.rss.util.other.AniUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
/**
* 导入订阅
*/
@Slf4j
@Auth
@Path("/ani/import")
public class ImportAction implements BaseAction {
static final List<Ani> ANI_LIST = AniUtil.ANI_LIST;
@Override
@Synchronized("ANI_LIST")
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
ImportAniDataDTO dto = getBody(ImportAniDataDTO.class);
List<Ani> aniList = dto.getAniList();
if (aniList.isEmpty()) {
resultErrorMsg("导入列表为空");
return;
}
ImportAniDataDTO.Conflict conflict = dto.getConflict();
for (Ani ani : aniList) {
AniUtil.verify(ani);
String title = ani.getTitle();
int season = ani.getSeason();
Optional<Ani> first = AniUtil.ANI_LIST.stream()
.filter(it -> it.getTitle().equals(title) && it.getSeason() == season)
.findFirst();
if (first.isEmpty()) {
String image = ani.getImage();
String cover = AniUtil.saveJpg(image);
ani.setCover(cover)
.setId(UUID.fastUUID().toString());
ANI_LIST.add(ani);
continue;
}
if (conflict == ImportAniDataDTO.Conflict.SKIP) {
log.info("存在冲突,已跳过 {} 第{}季", title, season);
continue;
}
log.info("存在冲突,已替换 {} 第{}季", title, season);
String image = ani.getImage();
String cover = AniUtil.saveJpg(image);
ani.setCover(cover);
String[] ignoreProperties = new String[]{"id", "currentEpisodeNumber", "lastDownloadTime"};
BeanUtil.copyProperties(ani, first.get(), ignoreProperties);
}
AniUtil.sync();
resultSuccessMsg("导入成功");
}
}

View File

@@ -1,52 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Ani;
import ani.rss.entity.Item;
import ani.rss.service.DownloadService;
import ani.rss.util.other.ItemsUtil;
import ani.rss.util.other.TorrentUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* 预览订阅
*/
@Auth
@Path("/items")
public class ItemsAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
Ani ani = getBody(Ani.class);
List<Item> items = ItemsUtil.getItems(ani);
String downloadPath = DownloadService.getDownloadPath(ani);
for (Item item : items) {
item.setLocal(false);
File torrent = TorrentUtil.getTorrent(ani, item);
if (torrent.exists()) {
item.setLocal(true);
continue;
}
if (DownloadService.itemDownloaded(ani, item, false)) {
item.setLocal(true);
}
}
List<Integer> omitList = ItemsUtil.omitList(ani, items);
resultSuccess(Map.of(
"downloadPath", downloadPath,
"items", items,
"omitList", omitList
));
}
}

View File

@@ -1,37 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Log;
import ani.rss.util.basic.LogUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.http.Method;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* 日志
*/
@Slf4j
@Auth
@Path("/logs")
public class LogsAction implements BaseAction {
List<Log> LOG_LIST = LogUtil.LOG_LIST;
@Override
@Synchronized("LOG_LIST")
public void doAction(HttpServerRequest req, HttpServerResponse res) {
String method = req.getMethod();
if (Method.DELETE.name().equals(method)) {
LOG_LIST.clear();
log.info("清理日志");
resultSuccess();
return;
}
resultSuccess(LOG_LIST);
}
}

View File

@@ -1,25 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Mikan;
import ani.rss.util.other.MikanUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.IOException;
/**
* Mikan搜索
*/
@Auth
@Path("/mikan")
public class MikanAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String text = request.getParam("text");
Mikan.Season season = getBody(Mikan.Season.class);
resultSuccess(MikanUtil.list(text, season));
}
}

View File

@@ -1,68 +0,0 @@
package ani.rss.action;
import ani.rss.commons.GsonStatic;
import ani.rss.entity.Mikan;
import ani.rss.entity.TorrentsInfo;
import ani.rss.util.other.MikanUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Mikan字幕组
*/
@Auth
@Path("/mikan/group")
public class MikanGroupAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String url = request.getParam("url");
List<Mikan.Group> groups = MikanUtil.getGroups(url);
List<String> regexItemList = List.of(
"1920[Xx]1080", "3840[Xx]2160", "1080[Pp]", "4[Kk]", "720[Pp]",
"", "", "",
"cht|Cht|CHT", "chs|Chs|CHS", "hevc|Hevc|HEVC",
"10bit|10Bit|10BIT", "h265|H265", "h264|H264",
"内嵌", "内封", "外挂",
"mp4|MP4", "mkv|MKV"
);
for (Mikan.Group group : groups) {
Set<String> tags = new HashSet<>();
List<List<Mikan.RegexItem>> regexList = new ArrayList<>();
List<TorrentsInfo> items = group.getItems();
for (TorrentsInfo item : items) {
String name = item.getName();
List<Mikan.RegexItem> regexItems = new ArrayList<>();
for (String regex : regexItemList) {
if (!ReUtil.contains(regex, name)) {
continue;
}
String label = ReUtil.get(regex, name, 0);
label = label.toUpperCase();
Mikan.RegexItem regexItem = new Mikan.RegexItem(label, regex);
regexItems.add(regexItem);
tags.add(label);
}
regexItems = CollUtil.distinct(regexItems, GsonStatic::toJson, true);
regexList.add(regexItems);
}
regexList = CollUtil.distinct(regexList, GsonStatic::toJson, true);
group.setRegexList(regexList)
.setTags(tags);
}
resultSuccess(groups);
}
}

View File

@@ -1,100 +0,0 @@
package ani.rss.action;
import ani.rss.entity.PlayItem;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import com.google.gson.JsonObject;
import com.matthewn4444.ebml.EBMLReader;
import com.matthewn4444.ebml.subtitles.Subtitles;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 视频操作
*/
@Auth
@Slf4j
@Path("/playitem")
public class PlayItemAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
JsonObject jsonObject = getBody(JsonObject.class);
String type = jsonObject.get("type").getAsString();
String file = jsonObject.get("file").getAsString();
if ("getSubtitles".equalsIgnoreCase(type)) {
getSubtitles(file);
return;
}
resultErrorMsg("未知操作");
}
/**
* 获取内封字幕并返回到客户端
*
* @param file
* @throws IOException
*/
public void getSubtitles(String file) throws IOException {
Assert.notBlank(file);
if (Base64.isBase64(file)) {
file = Base64.decodeStr(file);
}
List<PlayItem.Subtitles> subtitlesList = new ArrayList<>();
String extName = FileUtil.extName(file);
if (StrUtil.isBlank(extName)) {
resultSuccess(subtitlesList);
return;
}
if (!"mkv".equals(extName)) {
resultSuccess(subtitlesList);
return;
}
Assert.isTrue(FileUtil.exist(file), "视频文件不存在");
@Cleanup
EBMLReader reader = new EBMLReader(file);
if (!reader.readHeader()) {
resultSuccess(subtitlesList);
return;
}
reader.readTracks();
reader.readCues();
for (int i = 0; i < reader.getCuesCount(); i++) {
reader.readSubtitlesInCueFrame(i);
}
List<Subtitles> subtitles = reader.getSubtitles();
for (Subtitles subtitle : subtitles) {
String name = subtitle.getName();
String presentableName = subtitle.getPresentableName();
String contents = subtitle.getContentsToVTT();
PlayItem.Subtitles sub = new PlayItem.Subtitles();
sub.setContent(contents)
.setName(name)
.setHtml(presentableName)
.setUrl("")
.setType("vtt");
subtitlesList.add(sub);
}
resultSuccess(subtitlesList);
}
}

View File

@@ -1,64 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Config;
import ani.rss.entity.ProxyTest;
import ani.rss.entity.Result;
import ani.rss.util.basic.HttpReq;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpStatus;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import java.io.IOException;
/**
* 代理
*/
@Slf4j
@Auth
@Path("/proxy")
public class ProxyAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String url = request.getParam("url");
Config config = getBody(Config.class);
url = Base64.decodeStr(url);
log.info(url);
HttpRequest httpRequest = HttpReq.get(url);
HttpReq.setProxy(httpRequest, config);
ProxyTest proxyTest = new ProxyTest();
Result<ProxyTest> result = Result.success(proxyTest);
long start = LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtil.now());
try {
httpRequest
.then(res -> {
int status = res.getStatus();
proxyTest.setStatus(status);
String title = Jsoup.parse(res.body())
.title();
result.setMessage(StrFormatter.format("测试成功 {}", title));
});
} catch (Exception e) {
result.setMessage(e.getMessage())
.setCode(HttpStatus.HTTP_INTERNAL_ERROR);
}
long end = LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtil.now());
proxyTest.setTime(end - start);
result(result);
}
}

View File

@@ -1,49 +0,0 @@
package ani.rss.action;
import ani.rss.commons.ExceptionUtils;
import ani.rss.entity.Ani;
import ani.rss.util.other.AniUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* 根据rss解析为订阅
*/
@Slf4j
@Auth
@Path("/rss")
public class RssAction implements BaseAction {
@Override
public void doAction(HttpServerRequest req, HttpServerResponse res) throws IOException {
if (!req.getMethod().equals("POST")) {
return;
}
Ani ani = getBody(Ani.class);
String url = ani.getUrl();
String type = ani.getType();
String bgmUrl = ani.getBgmUrl();
Assert.notBlank(url, "RSS地址 不能为空");
if (!ReUtil.contains("http(s*)://", url)) {
url = "https://" + url;
}
url = URLUtil.decode(url, "utf-8");
try {
Ani newAni = AniUtil.getAni(url, type, bgmUrl);
resultSuccess(newAni);
} catch (Exception e) {
String message = ExceptionUtils.getMessage(e);
log.error(message, e);
resultErrorMsg("RSS解析失败 {}", message);
}
}
}

View File

@@ -1,36 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Ani;
import ani.rss.service.ScrapeService;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* 刮削
*/
@Auth
@Slf4j
@Path("/scrape")
public class ScrapeAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
Ani ani = getBody(Ani.class);
String force = request.getParam("force");
ThreadUtil.execute(() ->
ScrapeService.scrape(ani, Boolean.parseBoolean(force))
);
String title = ani.getTitle();
resultSuccessMsg("已开始刮削 {}", title);
}
}

View File

@@ -1,48 +0,0 @@
package ani.rss.action;
import ani.rss.commons.MavenUtils;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import ani.rss.web.util.ServerUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.RuntimeUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* 关闭或重启
*/
@Slf4j
@Auth
@Path("/stop")
public class StopAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String statusStr = request.getParam("status");
int status = Integer.parseInt(statusStr);
String s = List.of("重启", "关闭").get(status);
log.info("正在{}", s);
resultSuccessMsg("正在{}", s);
ThreadUtil.execute(() -> {
ThreadUtil.sleep(3000);
File jar = MavenUtils.getJar();
String extName = FileUtil.extName(jar);
ServerUtil.stop();
if ("exe".equals(extName) && status == 0) {
log.info("正在重启 {}", jar.getName());
RuntimeUtil.exec(jar.getName());
System.exit(status);
return;
}
System.exit(status);
});
}
}

View File

@@ -1,31 +0,0 @@
package ani.rss.action;
import ani.rss.entity.NotificationConfig;
import ani.rss.notification.TelegramNotification;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.IOException;
import java.util.Map;
/**
* 电报
*/
@Auth
@Path("/telegram")
public class TelegramAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
NotificationConfig notificationConfig = getBody(NotificationConfig.class);
String method = request.getParam("method");
if ("getUpdates".equals(method)) {
Map<String, String> map = TelegramNotification.getUpdates(notificationConfig);
resultSuccess(map);
}
}
}

View File

@@ -1,29 +0,0 @@
package ani.rss.action;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import ani.rss.web.auth.fun.IpWhitelist;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.IOException;
/**
* 用于检测是否处于白名单内
*/
@Auth(false)
@Path("/test")
public class TestAction implements BaseAction {
private final IpWhitelist ipWhitelist = new IpWhitelist();
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
Boolean b = ipWhitelist.apply(request);
if (b) {
resultSuccess();
return;
}
resultError();
}
}

View File

@@ -1,50 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Ani;
import ani.rss.entity.Result;
import ani.rss.util.other.TmdbUtils;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpStatus;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import wushuo.tmdb.api.entity.Tmdb;
import java.io.IOException;
/**
* TMDB
*/
@Auth
@Path("/tmdb")
public class ThemoviedbAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String s = request.getParam("method");
if ("getThemoviedbName".equals(s)) {
Ani ani = getBody(Ani.class);
String themoviedbName = TmdbUtils.getFinalName(ani);
Result<Ani> result = new Result<Ani>()
.setCode(HttpStatus.HTTP_OK)
.setMessage("获取TMDB成功")
.setData(ani.setThemoviedbName(themoviedbName));
if (StrUtil.isBlank(themoviedbName)) {
result.setCode(HttpStatus.HTTP_INTERNAL_ERROR)
.setMessage("获取TMDB失败");
}
result(result);
return;
}
if ("getTmdbGroup".equals(s)) {
Ani ani = getBody(Ani.class);
Tmdb tmdb = ani.getTmdb();
Assert.notNull(tmdb, "tmdb is null");
Assert.notBlank(tmdb.getId(), "tmdb is null");
resultSuccess(TmdbUtils.getTmdbGroup(tmdb));
}
}
}

View File

@@ -1,67 +0,0 @@
package ani.rss.action;
import ani.rss.commons.FileUtils;
import ani.rss.entity.Ani;
import ani.rss.util.other.AniUtil;
import ani.rss.util.other.TorrentUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import ani.rss.web.util.ServerUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.Method;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
/**
* 种子管理
*/
@Slf4j
@Auth
@Path("/torrent")
public class TorrentAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
if (Method.DELETE.name().equals(request.getMethod())) {
del();
}
}
/**
* 删除缓存种子
*/
public void del() {
HttpServerRequest req = ServerUtil.REQUEST.get();
String id = req.getParam("id");
String infoHash = req.getParam("infoHash");
Optional<Ani> first = AniUtil.ANI_LIST.stream().filter(ani -> id.equals(ani.getId()))
.findFirst();
if (first.isEmpty()) {
resultErrorMsg("此订阅不存在");
return;
}
List<String> infoHashList = StrUtil.split(infoHash, ",", true, true);
Ani ani = first.get();
File torrentDir = TorrentUtil.getTorrentDir(ani);
File[] files = FileUtils.listFiles(torrentDir);
for (File file : files) {
String s = FileUtil.mainName(file);
if (infoHashList.contains(s)) {
log.info("删除种子 {}", file);
FileUtil.del(file);
}
}
resultSuccessMsg("删除完成");
}
}

View File

@@ -1,27 +0,0 @@
package ani.rss.action;
import ani.rss.entity.TorrentsInfo;
import ani.rss.util.other.TorrentUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.List;
/**
* 下载器任务列表
*/
@Slf4j
@Auth
@Path("/torrentsInfos")
public class TorrentsInfosAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
List<TorrentsInfo> torrentsInfos = TorrentUtil.getTorrentsInfos();
resultSuccess(torrentsInfos);
}
}

View File

@@ -1,25 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Config;
import ani.rss.other.Cron;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.IOException;
/**
* Trackers
*/
@Auth
@Path("/trackersUpdate")
public class TrackersUpdateAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
Config config = getBody(Config.class);
Cron.updateTrackers(config);
resultSuccessMsg("更新完成");
}
}

View File

@@ -1,35 +0,0 @@
package ani.rss.action;
import ani.rss.commons.ExceptionUtils;
import ani.rss.entity.About;
import ani.rss.util.other.UpdateUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* 更新
*/
@Slf4j
@Auth
@Path("/update")
public class UpdateAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
About about = UpdateUtil.about();
try {
UpdateUtil.update(about);
resultSuccessMsg("更新成功, 正在重启...");
} catch (Exception e) {
String message = ExceptionUtils.getMessage(e);
log.info("更新失败 {}, {}", about.getLatest(), message);
resultErrorMsg("更新失败 {}, {}", about.getLatest(), message);
}
}
}

View File

@@ -1,53 +0,0 @@
package ani.rss.action;
import ani.rss.entity.Result;
import ani.rss.util.other.ConfigUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.net.multipart.MultipartFormData;
import cn.hutool.core.net.multipart.UploadFile;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
/**
* 上传文件
*/
@Slf4j
@Auth
@Path("/upload")
public class UploadAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String type = request.getParam("type");
MultipartFormData multipart = request.getMultipart();
UploadFile file = multipart.getFile("file");
if (file.size() > 1024 * 1024 * 50) {
resultErrorMsg("文件大小超过 50M");
return;
}
byte[] fileContent = file.getFileContent();
if ("getBase64".equals(type)) {
resultSuccess(Base64.encode(fileContent));
return;
}
String s = SecureUtil.md5(new ByteArrayInputStream(fileContent));
String fileName = file.getFileName();
String saveName = s + "." + FileUtil.extName(fileName);
File configDir = ConfigUtil.getConfigDir();
FileUtil.mkdir(configDir + "/files/" + s.charAt(0));
FileUtil.writeBytes(fileContent, configDir + "/files/" + s.charAt(0) + "/" + saveName);
resultSuccess(new Result<>().setMessage("上传完成").setData(s.charAt(0) + "/" + saveName));
}
}

View File

@@ -1,6 +1,6 @@
package ani.rss.web.annotation;
package ani.rss.annotation;
import ani.rss.web.auth.enums.AuthType;
import ani.rss.auth.enums.AuthType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -11,7 +11,7 @@ import java.lang.annotation.Target;
* 鉴权
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
@Target(ElementType.METHOD)
public @interface Auth {
boolean value() default true;

View File

@@ -0,0 +1,32 @@
package ani.rss.auth;
import ani.rss.annotation.Auth;
import ani.rss.entity.Global;
import ani.rss.entity.Result;
import ani.rss.exception.ResultException;
import ani.rss.util.other.AuthUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class AuthAspect {
@Before("@annotation(auth)")
public void before(JoinPoint joinPoint, Auth auth) throws Exception {
HttpServletRequest request = Global.REQUEST.get();
if (AuthUtil.test(request, auth)) {
// 鉴权通过
return;
}
throw new ResultException(
Result.error(r ->
r.setCode(403)
.setMessage("登录已失效")
)
);
}
}

View File

@@ -1,10 +1,10 @@
package ani.rss.web.auth.enums;
package ani.rss.auth.enums;
import ani.rss.web.auth.fun.ApiKey;
import ani.rss.web.auth.fun.Form;
import ani.rss.web.auth.fun.Header;
import ani.rss.web.auth.fun.IpWhitelist;
import cn.hutool.http.server.HttpServerRequest;
import ani.rss.auth.fun.ApiKey;
import ani.rss.auth.fun.Form;
import ani.rss.auth.fun.Header;
import ani.rss.auth.fun.IpWhitelist;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.Getter;
@@ -21,5 +21,5 @@ public enum AuthType {
IP_WHITE_LIST(IpWhitelist.class);
@Getter
private final Class<? extends Function<HttpServerRequest, Boolean>> clazz;
private final Class<? extends Function<HttpServletRequest, Boolean>> clazz;
}

View File

@@ -1,24 +1,24 @@
package ani.rss.web.auth.fun;
package ani.rss.auth.fun;
import ani.rss.entity.Config;
import ani.rss.util.other.ConfigUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.server.HttpServerRequest;
import jakarta.servlet.http.HttpServletRequest;
import java.util.function.Function;
/**
* api key 鉴权
*/
public class ApiKey implements Function<HttpServerRequest, Boolean> {
public class ApiKey implements Function<HttpServletRequest, Boolean> {
@Override
public Boolean apply(HttpServerRequest request) {
public Boolean apply(HttpServletRequest request) {
Config config = ConfigUtil.CONFIG;
String apiKey = config.getApiKey();
if (StrUtil.isBlank(apiKey)) {
return false;
}
String s = StrUtil.blankToDefault(request.getParam("s"), request.getHeader("s"));
String s = StrUtil.blankToDefault(request.getParameter("s"), request.getHeader("s"));
return StrUtil.equals(apiKey, s);
}
}

View File

@@ -1,19 +1,19 @@
package ani.rss.web.auth.fun;
package ani.rss.auth.fun;
import ani.rss.entity.Login;
import ani.rss.web.util.AuthUtil;
import ani.rss.util.other.AuthUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.server.HttpServerRequest;
import jakarta.servlet.http.HttpServletRequest;
import java.util.function.Function;
/**
* 表单鉴权
*/
public class Form implements Function<HttpServerRequest, Boolean> {
public class Form implements Function<HttpServletRequest, Boolean> {
@Override
public Boolean apply(HttpServerRequest request) {
String s = request.getParam("s");
public Boolean apply(HttpServletRequest request) {
String s = request.getParameter("s");
Login login = AuthUtil.getLogin();
String auth = AuthUtil.getAuth(login);
return StrUtil.equals(auth, s);

View File

@@ -1,18 +1,18 @@
package ani.rss.web.auth.fun;
package ani.rss.auth.fun;
import ani.rss.entity.Login;
import ani.rss.web.util.AuthUtil;
import ani.rss.util.other.AuthUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.server.HttpServerRequest;
import jakarta.servlet.http.HttpServletRequest;
import java.util.function.Function;
/**
* 请求头鉴权
*/
public class Header implements Function<HttpServerRequest, Boolean> {
public class Header implements Function<HttpServletRequest, Boolean> {
@Override
public Boolean apply(HttpServerRequest request) {
public Boolean apply(HttpServletRequest request) {
String s = request.getHeader("Authorization");
if (StrUtil.isBlank(s)) {
return false;

View File

@@ -1,15 +1,15 @@
package ani.rss.web.auth.fun;
package ani.rss.auth.fun;
import ani.rss.commons.CacheUtils;
import ani.rss.entity.Config;
import ani.rss.util.basic.CidrRangeChecker;
import ani.rss.util.other.AuthUtil;
import ani.rss.util.other.ConfigUtil;
import ani.rss.web.util.AuthUtil;
import cn.hutool.core.lang.PatternPool;
import cn.hutool.core.net.Ipv4Util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.server.HttpServerRequest;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@@ -18,9 +18,9 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@Slf4j
public class IpWhitelist implements Function<HttpServerRequest, Boolean> {
public class IpWhitelist implements Function<HttpServletRequest, Boolean> {
@Override
public Boolean apply(HttpServerRequest request) {
public Boolean apply(HttpServletRequest request) {
String ip = AuthUtil.getIp();
Config config = ConfigUtil.CONFIG;
String ipWhitelistStr = config.getIpWhitelistStr();

View File

@@ -88,7 +88,7 @@ public class FileUtils {
public static String normalize(String path) {
path = path.trim();
String s = cn.hutool.core.io.FileUtil.normalize(path);
String s = FileUtil.normalize(path);
while (s.endsWith("/")) {
s = s.substring(0, s.length() - 1);
}

View File

@@ -12,7 +12,7 @@ import java.util.TimeZone;
@Slf4j
public class GsonStatic {
private static final Gson GSON = new GsonBuilder()
public static final Gson GSON = new GsonBuilder()
.disableHtmlEscaping()
.disableJdkUnsafe()
.disableInnerClassSerialization()

View File

@@ -4,10 +4,13 @@ import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.XmlUtil;
import cn.hutool.system.OsInfo;
import cn.hutool.system.SystemUtil;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.File;
import java.io.IOException;
@@ -75,8 +78,9 @@ public class MavenUtils {
}
File file = new File("pom.xml");
if (file.exists()) {
String s = FileUtil.readUtf8String(file);
version = ReUtil.get("<version>(.*?)</version>", s, 1);
Document document = XmlUtil.readXML(file);
Element element = XmlUtil.getElement(document.getDocumentElement(), "version");
version = element.getTextContent();
}
return version;
}

View File

@@ -1,4 +1,4 @@
package ani.rss.other;
package ani.rss.config;
import ani.rss.download.BaseDownload;
import ani.rss.entity.About;
@@ -10,22 +10,68 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.cron.CronUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 定时任务
*/
@Slf4j
public class Cron {
public static void updateTrackers(Config config) {
@Component
public class CronConfig {
@Scheduled(cron = "0 0 0 * * *")
public void backupConfig() {
ConfigUtil.backup();
}
@Scheduled(cron = "0 0 1 * * *")
public void autoTrackersUpdate() {
Config config = ConfigUtil.CONFIG;
Boolean autoTrackersUpdate = config.getAutoTrackersUpdate();
if (!autoTrackersUpdate) {
// 未开启自动更新 Trackers
return;
}
log.info("定时任务 开始更新 Trackers");
try {
updateTrackers(config);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
@Scheduled(cron = "0 0 6 * * *")
public void autoUpdate() {
Config config = ConfigUtil.CONFIG;
Boolean autoUpdate = config.getAutoUpdate();
if (!autoUpdate) {
// 未开启 自动更新
return;
}
log.info("定时任务 自动更新");
try {
About about = UpdateUtil.about();
Boolean update = about.getUpdate();
autoUpdate = about.getAutoUpdate();
if (!autoUpdate) {
// 禁止非跨小版本的自动更新
return;
}
if (update) {
log.info("检测到可更新版本 v{}", about.getLatest());
}
UpdateUtil.update(about);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
public void updateTrackers(Config config) {
String trackersUpdateUrls = config.getTrackersUpdateUrls();
Assert.notBlank(trackersUpdateUrls, "Trackers更新地址 为空");
@@ -75,54 +121,4 @@ public class Cron {
baseDownload.updateTrackers(trackers);
}
public static void autoUpdate(Config config) {
About about = UpdateUtil.about();
Boolean update = about.getUpdate();
Boolean autoUpdate = about.getAutoUpdate();
if (!autoUpdate) {
// 禁止非跨小版本的自动更新
return;
}
if (update) {
log.info("检测到可更新版本 v{}", about.getLatest());
}
UpdateUtil.update(about);
}
public static void start() {
Config config = ConfigUtil.CONFIG;
// 自动备份设置
CronUtil.schedule("0 0 * * *", (Runnable) ConfigUtil::backup);
CronUtil.schedule("0 1 * * *", (Runnable) () -> {
Boolean autoTrackersUpdate = config.getAutoTrackersUpdate();
if (!autoTrackersUpdate) {
// 未开启自动更新 Trackers
return;
}
log.info("定时任务 开始更新 Trackers");
try {
Cron.updateTrackers(config);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
});
CronUtil.schedule("0 6 * * *", (Runnable) () -> {
Boolean autoUpdate = config.getAutoUpdate();
if (!autoUpdate) {
// 未开启 自动更新
return;
}
log.info("定时任务 自动更新");
try {
Cron.autoUpdate(config);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
});
CronUtil.start();
}
}

View File

@@ -0,0 +1,36 @@
package ani.rss.config;
import ani.rss.entity.Result;
import ani.rss.exception.ResultException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
@Slf4j
@RestControllerAdvice
public class CustomExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public Result<Void> handleException(IllegalArgumentException e) {
return Result.error(e.getMessage());
}
@ExceptionHandler(ResultException.class)
public Result<Void> handleException(ResultException e) {
return e.getResult();
}
@ExceptionHandler({NoResourceFoundException.class, HttpRequestMethodNotSupportedException.class})
public Result<Void> handleException(NoResourceFoundException e) {
return Result.error(e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error(e.getMessage(), e);
return Result.error();
}
}

View File

@@ -0,0 +1,51 @@
package ani.rss.config;
import ani.rss.commons.ExceptionUtils;
import ani.rss.commons.MavenUtils;
import ani.rss.service.TaskService;
import ani.rss.util.other.AniUtil;
import ani.rss.util.other.ConfigUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.RuntimeUtil;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.NonNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
@Slf4j
@Component
public class Runner implements ApplicationRunner {
@Value("${server.port}")
private String port;
@Override
public void run(@NonNull ApplicationArguments args) {
try {
ConfigUtil.load();
ConfigUtil.backup();
AniUtil.load();
TaskService.start();
String version = MavenUtils.getVersion();
log.info("version {}", version);
for (String ip : NetUtil.localIpv4s()) {
InetSocketAddress inetSocketAddress = new InetSocketAddress(ip, Integer.parseInt(port));
if (NetUtil.isOpen(inetSocketAddress, 100)) {
log.info("http://{}:{}", ip, port);
}
}
} catch (Exception e) {
String message = ExceptionUtils.getMessage(e);
log.error(message, e);
System.exit(1);
}
RuntimeUtil.addShutdownHook(() -> log.info("程序退出..."));
}
}

View File

@@ -0,0 +1,39 @@
package ani.rss.config;
import ani.rss.commons.MavenUtils;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI springShopOpenAPI() {
Info info = new Info();
License license = new License()
.name("GPL-2.0")
.url("https://github.com/wushuo894/ani-rss/blob/master/LICENSE");
String version = MavenUtils.getVersion();
info.title("ANI-RSS")
.contact(new Contact())
.description("基于RSS自动追番、订阅、下载、刮削")
.version("v" + version)
.license(license);
ExternalDocumentation externalDocumentation = new ExternalDocumentation()
.description("外部文档")
.url("https://docs.wushuo.top/");
return new OpenAPI()
.info(info)
.externalDocs(externalDocumentation);
}
}

View File

@@ -0,0 +1,24 @@
package ani.rss.config;
import ani.rss.entity.Global;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class WebFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
Global.REQUEST.set((HttpServletRequest) request);
Global.RESPONSE.set((HttpServletResponse) response);
try {
filterChain.doFilter(request, response);
} finally {
Global.REQUEST.remove();
Global.RESPONSE.remove();
}
}
}

View File

@@ -0,0 +1,36 @@
package ani.rss.config;
import ani.rss.commons.GsonStatic;
import com.google.gson.Gson;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/api", c -> c.isAnnotationPresent(RestController.class));
}
@Bean
public Gson gson() {
return GsonStatic.GSON;
}
@Override
public void configureMessageConverters(HttpMessageConverters.ServerBuilder builder) {
builder.configureMessageConvertersList(converters -> {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter();
converter.setGson(gson());
converter.setDefaultCharset(StandardCharsets.UTF_8);
converters.add(0, converter);
});
}
}

View File

@@ -0,0 +1,83 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.auth.fun.IpWhitelist;
import ani.rss.commons.ExceptionUtils;
import ani.rss.commons.MavenUtils;
import ani.rss.entity.About;
import ani.rss.entity.Global;
import ani.rss.entity.Result;
import ani.rss.util.other.UpdateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.RuntimeUtil;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.util.List;
@Slf4j
@RestController
public class AboutController extends BaseController {
@Auth
@Operation(summary = "查看关于信息")
@PostMapping("/about")
public Result<About> about() {
return Result.success(UpdateUtil.about());
}
@Auth
@Operation(summary = "停止服务")
@PostMapping("/stop")
public Result<Void> stop(@RequestParam("status") Integer status) {
String s = List.of("重启", "关闭").get(status);
log.info("正在{}", s);
ThreadUtil.execute(() -> {
ThreadUtil.sleep(3000);
File jar = MavenUtils.getJar();
String extName = FileUtil.extName(jar);
if ("exe".equals(extName) && status == 0) {
log.info("正在重启 {}", jar.getName());
RuntimeUtil.exec(jar.getName());
System.exit(status);
return;
}
System.exit(status);
});
return Result.success("正在{}", s);
}
@Auth
@Operation(summary = "更新")
@PostMapping("/update")
public Result<Void> update() {
About about = UpdateUtil.about();
try {
UpdateUtil.update(about);
return Result.success("更新成功, 正在重启...");
} catch (Exception e) {
String message = ExceptionUtils.getMessage(e);
log.info("更新失败 {}, {}", about.getLatest(), message);
return Result.success("更新失败 {}, {}", about.getLatest(), message);
}
}
private final IpWhitelist ipWhitelist = new IpWhitelist();
@Operation(summary = "IP白名单测试")
@PostMapping("/testIpWhitelist")
public Result<Void> testIpWhitelist() {
HttpServletRequest request = Global.REQUEST.get();
Boolean b = ipWhitelist.apply(request);
if (b) {
return Result.success();
}
return Result.error();
}
}

View File

@@ -0,0 +1,80 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.entity.Config;
import ani.rss.entity.Result;
import ani.rss.entity.TryOut;
import ani.rss.util.basic.HttpReq;
import ani.rss.util.other.AfdianUtil;
import ani.rss.util.other.ConfigUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.http.HttpResponse;
import io.swagger.v3.oas.annotations.Hidden;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@Hidden
@RestController
public class AfdianController extends BaseController {
@Auth
@PostMapping("/verifyNo")
public Result<Void> verifyNo(@RequestBody Config config) {
String outTradeNo = config.getOutTradeNo();
Result<Void> result = AfdianUtil.verifyNo(outTradeNo);
int code = result.getCode();
if (code == 200) {
Long time = DateUtil.offsetYear(new Date(), 999).getTime();
ConfigUtil.CONFIG.setOutTradeNo(outTradeNo)
.setExpirationTime(time)
.setTryOut(false);
ConfigUtil.sync();
}
return result;
}
@Auth
@PostMapping("/tryOut")
public Result<Long> tryOut(@RequestBody Config config) {
if (AfdianUtil.verifyExpirationTime()) {
return Result.error("还在试用中!");
}
String githubToken = config.getGithubToken();
Assert.notBlank(githubToken, "GithubToken 不能为空");
Boolean ok = HttpReq.get("https://api.github.com/user/starred/wushuo894/ani-rss")
.header("Authorization", "Bearer " + githubToken)
.thenFunction(HttpResponse::isOk);
Assert.isTrue(ok, "未点击star");
TryOut tryOut = AfdianUtil.getTryOut();
Boolean enable = tryOut.getEnable();
Boolean renewal = tryOut.getRenewal();
Integer day = tryOut.getDay();
String message = tryOut.getMessage();
Assert.isTrue(enable, message);
if (config.getTryOut()) {
// 已经有过试用
Assert.isTrue(renewal, message);
}
long time = DateUtil.offsetDay(new Date(), day).getTime();
ConfigUtil.CONFIG
.setGithubToken(githubToken)
.setExpirationTime(time)
.setTryOut(true);
ConfigUtil.sync();
return Result.success(time).setMessage(message);
}
}

View File

@@ -1,7 +1,9 @@
package ani.rss.action;
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.commons.ExceptionUtils;
import ani.rss.commons.FileUtils;
import ani.rss.dto.ImportAniDataDTO;
import ani.rss.entity.*;
import ani.rss.enums.SortTypeEnum;
import ani.rss.service.AniService;
@@ -9,10 +11,6 @@ import ani.rss.service.ClearService;
import ani.rss.service.DownloadService;
import ani.rss.task.RssTask;
import ani.rss.util.other.*;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import ani.rss.web.util.ServerUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.comparator.PinyinComparator;
@@ -21,80 +19,35 @@ import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.pinyin.PinyinUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.ToLongFunction;
/**
* 订阅 增删改查
*/
@Auth
@Slf4j
@Path("/ani")
public class AniAction implements BaseAction {
@RestController
public class AniController extends BaseController {
public static final AtomicBoolean DOWNLOAD = new AtomicBoolean(false);
/**
* 手动刷新订阅
*/
private void refreshAni() {
Ani ani = getBody(Ani.class);
if (Objects.isNull(ani)) {
// 未传Body, 刷新所有订阅
RssTask.sync();
ThreadUtil.execute(() -> RssTask.download(new AtomicBoolean(true)));
resultSuccessMsg("已开始刷新RSS");
return;
}
Optional<Ani> first = AniUtil.ANI_LIST.stream()
.filter(it -> it.getId().equals(ani.getId()))
.findFirst();
if (first.isEmpty()) {
resultErrorMsg("修改失败");
return;
}
synchronized (DOWNLOAD) {
if (DOWNLOAD.get()) {
resultErrorMsg("存在未完成任务,请等待...");
return;
}
DOWNLOAD.set(true);
}
Ani downloadAni = first.get();
ThreadUtil.execute(() -> {
try {
if (TorrentUtil.login()) {
DownloadService.downloadAni(downloadAni);
}
} catch (Exception e) {
String message = ExceptionUtils.getMessage(e);
log.error(message, e);
}
DOWNLOAD.set(false);
});
resultSuccessMsg("已开始刷新RSS {}", downloadAni.getTitle());
}
/**
* 添加订阅
*/
private void post() {
Ani ani = getBody(Ani.class);
@Auth
@Operation(summary = "添加订阅")
@PostMapping("/addAni")
public Result<Void> addAni(@RequestBody Ani ani) {
ani.setTitle(ani.getTitle().trim())
.setUrl(ani.getUrl().trim());
AniUtil.verify(ani);
@@ -104,8 +57,7 @@ public class AniAction implements BaseAction {
.findFirst();
if (first.isPresent()) {
resultErrorMsg("此订阅已存在");
return;
return Result.error("此订阅已存在");
}
first = AniUtil.ANI_LIST.stream()
@@ -122,8 +74,7 @@ public class AniAction implements BaseAction {
AniUtil.ANI_LIST.remove(first.get());
log.info("自动替换 {} 第{}季", title, season);
} else {
resultErrorMsg("订阅标题重复");
return;
return Result.error("订阅标题重复");
}
}
@@ -148,15 +99,15 @@ public class AniAction implements BaseAction {
}
});
}
resultSuccessMsg("添加订阅成功");
log.info("添加订阅 {} {} {}", title, ani.getUrl(), ani.getId());
return Result.success("添加订阅成功");
}
/**
* 修改订阅
*/
private void put() {
Ani ani = getBody(Ani.class);
@Auth
@Operation(summary = "修改订阅")
@PostMapping("/setAni")
public Result<Void> setAni(@RequestBody Ani ani) {
ani.setTitle(ani.getTitle().trim())
.setUrl(ani.getUrl().trim());
AniUtil.verify(ani);
@@ -165,19 +116,17 @@ public class AniAction implements BaseAction {
.filter(it -> it.getTitle().equals(ani.getTitle()) && it.getSeason().equals(ani.getSeason()))
.findFirst();
if (first.isPresent()) {
resultErrorMsg("订阅标题重复");
return;
return Result.error("订阅标题重复");
}
first = AniUtil.ANI_LIST.stream()
.filter(it -> it.getId().equals(ani.getId()))
.findFirst();
if (first.isEmpty()) {
resultErrorMsg("修改失败");
return;
return Result.error("修改失败");
}
HttpServerRequest request = ServerUtil.REQUEST.get();
String move = request.getParam("move");
HttpServletRequest request = Global.REQUEST.get();
String move = request.getParameter("move");
if (Boolean.parseBoolean(move)) {
Ani get = ObjectUtil.clone(first.get());
ThreadUtil.execute(() -> {
@@ -237,14 +186,74 @@ public class AniAction implements BaseAction {
FileUtil.move(torrentDir, newTorrentDir.getParentFile(), true);
}
AniUtil.sync();
resultSuccessMsg("修改成功");
log.info("修改订阅 {} {} {}", ani.getTitle(), ani.getUrl(), ani.getId());
return Result.success("修改成功");
}
/**
* 返回订阅列表
*/
private void get() {
@Auth
@Operation(summary = "删除订阅")
@PostMapping("/deleteAni")
public Result<Void> deleteAni(@RequestBody List<String> ids, @RequestParam("deleteFiles") Boolean deleteFiles) {
Assert.notEmpty(ids, "未选择订阅");
List<Ani> anis = AniUtil.ANI_LIST.stream()
.filter(it -> ids.contains(it.getId()))
.toList();
if (anis.isEmpty()) {
return Result.error("删除失败");
}
for (Ani ani : anis) {
AniUtil.ANI_LIST.remove(ani);
}
AniUtil.sync();
ThreadUtil.execute(() -> {
for (Ani ani : anis) {
File torrentDir = TorrentUtil.getTorrentDir(ani);
FileUtil.del(torrentDir);
ClearService.clearParentFile(torrentDir);
log.info("删除订阅 {} {} {}", ani.getTitle(), ani.getUrl(), ani.getId());
}
if (!deleteFiles) {
// 不删除本地文件
return;
}
List<File> files = anis
.stream()
.map(DownloadService::getDownloadPath)
.map(File::new)
.toList();
Boolean login = TorrentUtil.login();
List<TorrentsInfo> torrentsInfos = new ArrayList<>();
if (login) {
torrentsInfos = TorrentUtil.getTorrentsInfos();
}
for (File file : files) {
String path = FileUtils.getAbsolutePath(file);
for (TorrentsInfo torrentsInfo : torrentsInfos) {
String downloadDir = torrentsInfo.getDownloadDir();
if (downloadDir.equals(path)) {
TorrentUtil.delete(torrentsInfo, true, true);
}
}
if (!file.exists()) {
continue;
}
ThreadUtil.sleep(3000);
log.info("删除 {}", file);
FileUtil.del(file);
ClearService.clearParentFile(file);
}
});
return Result.success("删除订阅成功");
}
@Auth
@Operation(summary = "订阅列表")
@PostMapping("/listAni")
public Result<List<Ani>> list() {
Config config = ConfigUtil.CONFIG;
SortTypeEnum sortType = config.getSortType();
@@ -297,101 +306,13 @@ public class AniAction implements BaseAction {
}).reversed());
}
resultSuccess(list);
return Result.success(list);
}
/**
* 删除订阅
*/
public void delete() {
JsonArray jsonArray = getBody(JsonArray.class);
List<String> ids = jsonArray.asList()
.stream().map(JsonElement::getAsString)
.toList();
Assert.notEmpty(ids, "未选择订阅");
List<Ani> anis = AniUtil.ANI_LIST.stream()
.filter(it -> ids.contains(it.getId()))
.toList();
if (anis.isEmpty()) {
resultErrorMsg("删除失败");
return;
}
for (Ani ani : anis) {
AniUtil.ANI_LIST.remove(ani);
}
AniUtil.sync();
resultSuccessMsg("删除订阅成功");
HttpServerRequest request = ServerUtil.REQUEST.get();
String deleteFiles = request.getParam("deleteFiles");
ThreadUtil.execute(() -> {
for (Ani ani : anis) {
File torrentDir = TorrentUtil.getTorrentDir(ani);
FileUtil.del(torrentDir);
ClearService.clearParentFile(torrentDir);
log.info("删除订阅 {} {} {}", ani.getTitle(), ani.getUrl(), ani.getId());
}
if (!Boolean.parseBoolean(deleteFiles)) {
// 不删除本地文件
return;
}
List<File> files = anis
.stream()
.map(DownloadService::getDownloadPath)
.map(File::new)
.toList();
Boolean login = TorrentUtil.login();
List<TorrentsInfo> torrentsInfos = new ArrayList<>();
if (login) {
torrentsInfos = TorrentUtil.getTorrentsInfos();
}
for (File file : files) {
String path = FileUtils.getAbsolutePath(file);
for (TorrentsInfo torrentsInfo : torrentsInfos) {
String downloadDir = torrentsInfo.getDownloadDir();
if (downloadDir.equals(path)) {
TorrentUtil.delete(torrentsInfo, true, true);
}
}
if (!file.exists()) {
continue;
}
ThreadUtil.sleep(3000);
log.info("删除 {}", file);
FileUtil.del(file);
ClearService.clearParentFile(file);
}
});
}
private void batchEnable(boolean enable) {
List<String> ids = getBody(JsonArray.class)
.asList()
.stream()
.map(JsonElement::getAsString)
.toList();
Assert.notEmpty(ids, "未选择订阅");
for (Ani ani : AniUtil.ANI_LIST) {
String id = ani.getId();
if (!ids.contains(id)) {
continue;
}
ani.setEnable(enable);
}
AniUtil.sync();
resultSuccessMsg("修改完成");
}
private void updateTotalEpisodeNumber() {
HttpServerRequest request = ServerUtil.REQUEST.get();
boolean force = Boolean.parseBoolean(request.getParam("force"));
List<String> ids = getBody(JsonArray.class)
.asList()
.stream()
.map(JsonElement::getAsString)
.toList();
@Auth
@Operation(summary = "更新总集数")
@PostMapping("/updateTotalEpisodeNumber")
public Result<Void> updateTotalEpisodeNumber(@RequestParam("force") Boolean force, @RequestBody List<String> ids) {
Assert.notEmpty(ids, "未选择订阅");
ThreadUtil.execute(() -> {
log.info("开始手动更新总集数");
@@ -416,53 +337,196 @@ public class AniAction implements BaseAction {
AniUtil.sync();
log.info("手动更新总集数完成 共更新{}条订阅", count);
});
resultSuccessMsg("已开始更新总集数");
return Result.success("已开始更新总集数");
}
@Override
public void doAction(HttpServerRequest req, HttpServerResponse res) {
String method = req.getMethod();
String type = StrUtil.blankToDefault(req.getParam("type"), "");
switch (type) {
case "refreshAni" -> {
// 刷新订阅
refreshAni();
return;
@Auth
@Operation(summary = "批量 启用/禁用 订阅")
@PostMapping("/batchEnable")
public Result<Void> batchEnable(@RequestParam("value") Boolean value, @RequestBody List<String> ids) {
Assert.notEmpty(ids, "未选择订阅");
for (Ani ani : AniUtil.ANI_LIST) {
String id = ani.getId();
if (!ids.contains(id)) {
continue;
}
case "batchEnable" -> {
// 批量 启用/禁用
boolean enable = Boolean.parseBoolean(req.getParam("value"));
batchEnable(enable);
return;
ani.setEnable(value);
}
case "updateTotalEpisodeNumber" -> {
// 更新总集数
updateTotalEpisodeNumber();
return;
AniUtil.sync();
return Result.success("修改完成");
}
@Auth
@Operation(summary = "手动刷新订阅")
@PostMapping("/refreshAll")
public Result<Void> refreshAni() {
// 未传Body, 刷新所有订阅
RssTask.sync();
ThreadUtil.execute(() -> RssTask.download(new AtomicBoolean(true)));
return Result.success("已开始刷新RSS");
}
@Auth
@Operation(summary = "手动刷新订阅")
@PostMapping("/refreshAni")
public Result<Void> refreshAni(@RequestBody Ani ani) {
Optional<Ani> first = AniUtil.ANI_LIST.stream()
.filter(it -> it.getId().equals(ani.getId()))
.findFirst();
if (first.isEmpty()) {
return Result.error("修改失败");
}
synchronized (DOWNLOAD) {
if (DOWNLOAD.get()) {
return Result.error("存在未完成任务,请等待...");
}
DOWNLOAD.set(true);
}
Ani downloadAni = first.get();
ThreadUtil.execute(() -> {
try {
if (TorrentUtil.login()) {
DownloadService.downloadAni(downloadAni);
}
} catch (Exception e) {
String message = ExceptionUtils.getMessage(e);
log.error(message, e);
}
DOWNLOAD.set(false);
});
return Result.success("已开始刷新RSS {}", downloadAni.getTitle());
}
@Auth
@Operation(summary = "将RSS转换为订阅")
@PostMapping("/rssToAni")
public Result<Ani> rssToAni(@RequestBody Ani ani) {
String url = ani.getUrl();
String type = ani.getType();
String bgmUrl = ani.getBgmUrl();
Assert.notBlank(url, "RSS地址 不能为空");
if (!ReUtil.contains("http(s*)://", url)) {
url = "https://" + url;
}
url = URLUtil.decode(url, "utf-8");
try {
Ani newAni = AniUtil.getAni(url, type, bgmUrl);
return Result.success(newAni);
} catch (Exception e) {
String message = ExceptionUtils.getMessage(e);
log.error(message, e);
return Result.error("RSS解析失败 {}", message);
}
}
switch (method) {
case "POST": {
// 添加订阅
post();
return;
@Auth
@Operation(summary = "预览订阅")
@PostMapping("/previewAni")
public Result<Map<String, Object>> previewAni(@RequestBody Ani ani) {
List<Item> items = ItemsUtil.getItems(ani);
String downloadPath = DownloadService.getDownloadPath(ani);
for (Item item : items) {
item.setLocal(false);
File torrent = TorrentUtil.getTorrent(ani, item);
if (torrent.exists()) {
item.setLocal(true);
continue;
}
case "PUT": {
// 修改订阅
put();
return;
}
case "GET": {
// 获取订阅列表
get();
return;
}
case "DELETE": {
// 删除订阅
delete();
break;
if (DownloadService.itemDownloaded(ani, item, false)) {
item.setLocal(true);
}
}
List<Integer> omitList = ItemsUtil.omitList(ani, items);
Map<String, Object> map = Map.of(
"downloadPath", downloadPath,
"items", items,
"omitList", omitList
);
return Result.success(map);
}
@Auth
@Operation(summary = "获取订阅的下载位置")
@PostMapping("/downloadPath")
public Result<Map<String, Object>> downloadPath(@RequestBody Ani ani) {
String downloadPath = DownloadService.getDownloadPath(ani);
boolean change = false;
Optional<Ani> first = AniUtil.ANI_LIST.stream()
.filter(it -> it.getId().equals(ani.getId()))
.findFirst();
if (first.isPresent()) {
Ani oldAni = ObjectUtil.clone(first.get());
// 只在名称改变时移动
oldAni.setSeason(ani.getSeason());
String oldDownloadPath = DownloadService.getDownloadPath(oldAni);
change = !downloadPath.equals(oldDownloadPath);
}
Map<String, Object> map = Map.of(
"change", change,
"downloadPath", downloadPath
);
return Result.success(map);
}
@Auth
@Operation(summary = "导入订阅")
@PostMapping("/importAni")
public Result<Void> importAni(@RequestBody ImportAniDataDTO dto) {
List<Ani> aniList = dto.getAniList();
if (aniList.isEmpty()) {
return Result.error("导入列表为空");
}
ImportAniDataDTO.Conflict conflict = dto.getConflict();
for (Ani ani : aniList) {
AniUtil.verify(ani);
String title = ani.getTitle();
int season = ani.getSeason();
Optional<Ani> first = AniUtil.ANI_LIST.stream()
.filter(it -> it.getTitle().equals(title) && it.getSeason() == season)
.findFirst();
if (first.isEmpty()) {
String image = ani.getImage();
String cover = AniUtil.saveJpg(image);
ani.setCover(cover)
.setId(UUID.fastUUID().toString());
AniUtil.ANI_LIST.add(ani);
continue;
}
if (conflict == ImportAniDataDTO.Conflict.SKIP) {
log.info("存在冲突,已跳过 {} 第{}季", title, season);
continue;
}
log.info("存在冲突,已替换 {} 第{}季", title, season);
String image = ani.getImage();
String cover = AniUtil.saveJpg(image);
ani.setCover(cover);
String[] ignoreProperties = new String[]{"id", "currentEpisodeNumber", "lastDownloadTime"};
BeanUtil.copyProperties(ani, first.get(), ignoreProperties);
}
AniUtil.sync();
return Result.success("导入成功");
}
@Auth
@Operation(summary = "刷新封面")
@PostMapping("/refreshCover")
public Result<String> refreshCover(@RequestBody Ani ani) {
String s = AniUtil.saveJpg(ani.getImage(), true);
return Result.success(r -> r.setData(s));
}
}

View File

@@ -0,0 +1,64 @@
package ani.rss.controller;
import ani.rss.entity.Global;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpStatus;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Cleanup;
import java.io.OutputStream;
public class BaseController {
/**
* 根据文件扩展名获得ContentType
*
* @param filename 文件名
* @return ContentType
*/
public static String getContentType(String filename) {
if (StrUtil.isBlank(filename)) {
return ContentType.OCTET_STREAM.getValue();
}
String extName = FileUtil.extName(filename);
if (StrUtil.isBlank(extName)) {
return ContentType.OCTET_STREAM.getValue();
}
if (extName.equalsIgnoreCase("mkv")) {
return "video/x-matroska";
}
String mimeType = FileUtil.getMimeType(filename);
if (StrUtil.isNotBlank(mimeType)) {
return mimeType;
}
return ContentType.OCTET_STREAM.getValue();
}
public static void writeNotFound() {
writeHtml(HttpStatus.HTTP_NOT_FOUND, "404 Not Found !");
}
public static void writeHtml(Integer status, String text) {
HttpServletResponse response = Global.RESPONSE.get();
String html = ResourceUtil.readUtf8Str("template.html");
html = html.replace("${text}", text);
try {
response.setStatus(status);
response.setContentType("text/html;charset=UTF-8");
@Cleanup
OutputStream outputStream = response.getOutputStream();
IoUtil.writeUtf8(outputStream, true, html);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,107 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.commons.GsonStatic;
import ani.rss.entity.*;
import ani.rss.util.basic.HttpReq;
import ani.rss.util.other.AniUtil;
import ani.rss.util.other.BgmUtil;
import ani.rss.util.other.ConfigUtil;
import cn.hutool.core.lang.Opt;
import com.google.gson.JsonObject;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import wushuo.tmdb.api.entity.Tmdb;
import java.util.List;
import java.util.Map;
@RestController
public class BgmController extends BaseController {
@Auth
@Operation(summary = "搜索BGM条目")
@PostMapping("/searchBgm")
public Result<List<BgmInfo>> searchBgm(@RequestParam("name") String name) {
List<BgmInfo> search = BgmUtil.search(name);
return Result.success(search);
}
@Auth
@Operation(summary = "将指定id的BGM番剧转换为订阅")
@PostMapping("/getAniBySubjectId")
public Result<Ani> getAniBySubjectId(@RequestParam("id") String id) {
BgmInfo bgmInfo = BgmUtil.getBgmInfo(id, true);
Ani ani = BgmUtil.toAni(bgmInfo, AniUtil.createAni());
ani
.setCustomDownloadPath(true);
return Result.success(ani);
}
@Auth
@Operation(summary = "获取BGM标题")
@PostMapping("/getBgmTitle")
public Result<String> getBgmTitle(@RequestBody Ani ani) {
Tmdb tmdb = ani.getTmdb();
BgmInfo bgmInfo = BgmUtil.getBgmInfo(ani);
String finalName = BgmUtil.getFinalName(bgmInfo, tmdb);
return Result.success(r -> r.setData(finalName));
}
@Auth
@Operation(summary = "评分")
@PostMapping("/rate")
public Result<Integer> rate(@RequestBody Ani ani) {
String subjectId = BgmUtil.getSubjectId(ani);
Integer score = Opt.ofNullable(ani.getScore())
.map(Double::intValue)
.orElse(null);
Integer rate = BgmUtil.rate(subjectId, score);
return Result.success(rate).setMessage("保存评分成功");
}
@Auth
@Operation(summary = "获取当前BGM账号信息")
@PostMapping("/meBgm")
public Result<BgmMe> meBgm() {
int expiresDays = BgmUtil.getExpiresDays();
BgmMe me = BgmUtil.me();
me.setExpiresDays(expiresDays);
return Result.success(me);
}
@Auth
@Operation(summary = "BGM授权回调")
@PostMapping("/bgm/oauth/callback")
public Result<Void> callback(@RequestParam("code") String code) {
Config config = ConfigUtil.CONFIG;
String bgmAppID = config.getBgmAppID();
String bgmAppSecret = config.getBgmAppSecret();
String bgmRedirectUri = config.getBgmRedirectUri();
Map<String, String> map = Map.of(
"grant_type", "authorization_code",
"client_id", bgmAppID,
"client_secret", bgmAppSecret,
"code", code,
"redirect_uri", bgmRedirectUri
);
HttpReq.post("https://bgm.tv/oauth/access_token")
.body(GsonStatic.toJson(map))
.then(res -> {
HttpReq.assertStatus(res);
JsonObject jsonObject = GsonStatic.fromJson(res.body(), JsonObject.class);
String accessToken = jsonObject.get("access_token").getAsString();
String refreshToken = jsonObject.get("refresh_token").getAsString();
config.setBgmToken(accessToken)
.setBgmRefreshToken(refreshToken);
});
ConfigUtil.sync();
return Result.success("授权成功, 现在你可以关闭此窗口");
}
}

View File

@@ -1,5 +1,6 @@
package ani.rss.action;
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.commons.FileUtils;
import ani.rss.download.qBittorrent;
import ani.rss.entity.*;
@@ -8,9 +9,6 @@ import ani.rss.util.basic.HttpReq;
import ani.rss.util.other.ConfigUtil;
import ani.rss.util.other.RenameUtil;
import ani.rss.util.other.TorrentUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
@@ -19,24 +17,162 @@ import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.*;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.bittorrent.TorrentFile;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 合集
*/
@Slf4j
@RestController
public class CollectionController extends BaseController {
@Auth
@Path("/collection")
public class CollectionAction implements BaseAction {
@Operation(summary = "开始下载合集")
@PostMapping("/startCollection")
public Result<Void> startCollection(@RequestBody CollectionInfo collectionInfo) throws IOException {
String torrent = collectionInfo.getTorrent();
File tempFile = FileUtil.createTempFile();
Base64.decodeToFile(torrent, tempFile);
TorrentFile torrentFile;
try {
torrentFile = new TorrentFile(tempFile);
} catch (Exception e) {
throw new RuntimeException(e);
}
Ani ani = collectionInfo.getAni();
String title = ani.getTitle();
String subgroup = ani.getSubgroup();
String downloadPath = ani.getDownloadPath();
String name = StrFormatter.format("[{}] {} 第{}季", subgroup, title, ani.getSeason());
download(name, tempFile, downloadPath, List.of("ANI-RSS合集下载", subgroup));
TorrentsInfo torrentsInfo = new TorrentsInfo()
.setHash(torrentFile.getHexHash());
Config config = ConfigUtil.CONFIG;
List<qBittorrent.FileEntity> files = new ArrayList<>();
for (int i = 0; i < 5; i++) {
ThreadUtil.sleep(500);
try {
files.addAll(qBittorrent.files(torrentsInfo, false, config));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
if (!files.isEmpty()) {
// 添加下载完成
break;
}
}
Map<String, String> reNameMap = preview(collectionInfo)
.stream()
.map(item -> {
Optional<qBittorrent.FileEntity> fileEntity = files.stream()
.filter(f -> new File(f.getName()).getName().equals(new File(item.getTitle()).getName()))
.filter(f -> f.getSize().longValue() == item.getLength())
.findFirst();
if (fileEntity.isEmpty()) {
return null;
}
String oldPath = fileEntity.get().getName();
return item.setTitle(oldPath);
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(
Item::getTitle,
Item::getReName
));
String host = config.getDownloadToolHost();
for (int i = 0; i < 30; i++) {
for (qBittorrent.FileEntity file : files) {
String oldPath = file.getName();
String newPath = reNameMap.get(oldPath);
if (!reNameMap.containsKey(oldPath)) {
if (!reNameMap.containsValue(oldPath) && file.getPriority() > 0) {
HttpReq.post(host + "/api/v2/torrents/filePrio")
.form("hash", torrentFile.getHexHash())
.form("id", file.getIndex())
.form("priority", 0)
.thenFunction(HttpResponse::isOk);
}
continue;
}
log.info("重命名 {} ==> {}", oldPath, newPath);
HttpReq.post(host + "/api/v2/torrents/renameFile")
.form("hash", torrentFile.getHexHash())
.form("oldPath", oldPath)
.form("newPath", newPath)
.thenFunction(HttpResponse::isOk);
}
files.clear();
files.addAll(qBittorrent.files(torrentsInfo, false, config));
if (CollUtil.containsAll(files.stream()
.map(qBittorrent.FileEntity::getName)
.toList(), reNameMap.values())) {
// 所有命名已完成
break;
}
// 命名有遗漏 继续
ThreadUtil.sleep(1000);
}
qBittorrent.start(torrentsInfo, config);
return Result.success("已经开始下载合集");
}
@Auth
@Operation(summary = "合集预览")
@PostMapping("/previewCollection")
public Result<List<Item>> previewCollection(@RequestBody CollectionInfo collectionInfo) {
List<Item> preview = preview(collectionInfo);
preview = CollUtil.sort(new ArrayList<>(preview), Comparator.comparingDouble(it -> {
Double episode = it.getEpisode();
return ObjectUtil.defaultIfNull(episode, 0.0);
}));
return Result.success(preview);
}
@Auth
@Operation(summary = "获取合集字幕组")
@PostMapping("/getCollectionSubgroup")
public Result<String> getCollectionSubgroup(@RequestBody CollectionInfo collectionInfo) {
List<Item> preview = preview(collectionInfo);
preview = CollUtil.sort(new ArrayList<>(preview), Comparator.comparingDouble(it -> {
Double episode = it.getEpisode();
return ObjectUtil.defaultIfNull(episode, 0.0);
}));
String subgroup = "未知字幕组";
String reg = "^\\[(.+?)]";
for (Item item : preview) {
String name = new File(item.getTitle()).getName();
if (!ReUtil.contains(reg, name)) {
continue;
}
subgroup = ReUtil.get(reg, name, 1);
break;
}
Result<String> result = Result.success();
result.setData(subgroup);
return result;
}
public static synchronized List<Item> preview(CollectionInfo collectionInfo) {
String torrent = collectionInfo.getTorrent();
File tempFile = FileUtil.createTempFile();
@@ -185,139 +321,4 @@ public class CollectionAction implements BaseAction {
.then(HttpReq::assertStatus);
}
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) {
String type = request.getParam("type");
CollectionInfo collectionInfo = getBody(CollectionInfo.class);
String torrent = collectionInfo.getTorrent();
// 预览
if (type.equals("preview") || type.equals("subgroup")) {
List<Item> preview = preview(collectionInfo);
preview = CollUtil.sort(new ArrayList<>(preview), Comparator.comparingDouble(it -> {
Double episode = it.getEpisode();
return ObjectUtil.defaultIfNull(episode, 0.0);
}));
if (type.equals("subgroup")) {
String subgroup = "未知字幕组";
String reg = "^\\[(.+?)]";
for (Item item : preview) {
String name = new File(item.getTitle()).getName();
if (!ReUtil.contains(reg, name)) {
continue;
}
subgroup = ReUtil.get(reg, name, 1);
break;
}
resultSuccess(subgroup);
return;
}
resultSuccess(preview);
}
// 开始下载
if (!type.equals("start")) {
return;
}
File tempFile = FileUtil.createTempFile();
Base64.decodeToFile(torrent, tempFile);
TorrentFile torrentFile;
try {
torrentFile = new TorrentFile(tempFile);
} catch (Exception e) {
throw new RuntimeException(e);
}
Ani ani = collectionInfo.getAni();
String title = ani.getTitle();
String subgroup = ani.getSubgroup();
String downloadPath = ani.getDownloadPath();
String name = StrFormatter.format("[{}] {} 第{}季", subgroup, title, ani.getSeason());
download(name, tempFile, downloadPath, List.of("ANI-RSS合集下载", subgroup));
TorrentsInfo torrentsInfo = new TorrentsInfo()
.setHash(torrentFile.getHexHash());
Config config = ConfigUtil.CONFIG;
List<qBittorrent.FileEntity> files = new ArrayList<>();
for (int i = 0; i < 5; i++) {
ThreadUtil.sleep(500);
try {
files.addAll(qBittorrent.files(torrentsInfo, false, config));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
if (!files.isEmpty()) {
// 添加下载完成
break;
}
}
Map<String, String> reNameMap = preview(collectionInfo)
.stream()
.map(item -> {
Optional<qBittorrent.FileEntity> fileEntity = files.stream()
.filter(f -> new File(f.getName()).getName().equals(new File(item.getTitle()).getName()))
.filter(f -> f.getSize().longValue() == item.getLength())
.findFirst();
if (fileEntity.isEmpty()) {
return null;
}
String oldPath = fileEntity.get().getName();
return item.setTitle(oldPath);
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(
Item::getTitle,
Item::getReName
));
String host = config.getDownloadToolHost();
for (int i = 0; i < 30; i++) {
for (qBittorrent.FileEntity file : files) {
String oldPath = file.getName();
String newPath = reNameMap.get(oldPath);
if (!reNameMap.containsKey(oldPath)) {
if (!reNameMap.containsValue(oldPath) && file.getPriority() > 0) {
HttpReq.post(host + "/api/v2/torrents/filePrio")
.form("hash", torrentFile.getHexHash())
.form("id", file.getIndex())
.form("priority", 0)
.thenFunction(HttpResponse::isOk);
}
continue;
}
log.info("重命名 {} ==> {}", oldPath, newPath);
HttpReq.post(host + "/api/v2/torrents/renameFile")
.form("hash", torrentFile.getHexHash())
.form("oldPath", oldPath)
.form("newPath", newPath)
.thenFunction(HttpResponse::isOk);
}
files.clear();
files.addAll(qBittorrent.files(torrentsInfo, false, config));
if (CollUtil.containsAll(files.stream()
.map(qBittorrent.FileEntity::getName)
.toList(), reNameMap.values())) {
// 所有命名已完成
break;
}
// 命名有遗漏 继续
ThreadUtil.sleep(1000);
}
qBittorrent.start(torrentsInfo, config);
resultSuccess(result ->
result.setMessage("已经开始下载合集")
);
}
}

View File

@@ -0,0 +1,278 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.commons.FileUtils;
import ani.rss.commons.MavenUtils;
import ani.rss.config.CronConfig;
import ani.rss.download.BaseDownload;
import ani.rss.entity.*;
import ani.rss.service.ClearService;
import ani.rss.service.TaskService;
import ani.rss.util.basic.HttpReq;
import ani.rss.util.other.AfdianUtil;
import ani.rss.util.other.AniUtil;
import ani.rss.util.other.ConfigUtil;
import ani.rss.util.other.TorrentUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.*;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpStatus;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequiredArgsConstructor
public class ConfigController extends BaseController {
private final CronConfig cronConfig;
/**
* 构建信息
*/
public String buildInfo() {
String buildInfo = "";
try {
buildInfo = ResourceUtil.readUtf8Str("build_info");
} catch (Exception ignored) {
}
return buildInfo;
}
@Auth
@Operation(summary = "获取设置")
@PostMapping("/config")
public Result<Config> config() {
String version = MavenUtils.getVersion();
String buildInfo = buildInfo();
Config config = ObjectUtil.clone(ConfigUtil.CONFIG);
config.getLogin().setPassword("");
config.setVersion(version)
.setBuildInfo(buildInfo)
.setVerifyExpirationTime(AfdianUtil.verifyExpirationTime());
return Result.success(config);
}
@Auth
@Operation(summary = "修改设置")
@PostMapping("/setConfig")
public Result<Void> setConfig(@RequestBody Config newConfig) {
Config config = ConfigUtil.CONFIG;
Login login = config.getLogin();
String username = login.getUsername();
String password = login.getPassword();
Integer renameSleepSeconds = config.getRenameSleepSeconds();
Integer sleep = config.getRssSleepMinutes();
String download = config.getDownloadToolType();
newConfig.setExpirationTime(null)
.setOutTradeNo(null)
.setTryOut(null);
CopyOptions copyOptions = CopyOptions
.create()
.setIgnoreNullValue(true);
BeanUtil.copyProperties(
newConfig,
config,
copyOptions
);
String loginPassword = config.getLogin().getPassword();
// 密码未发生修改
if (StrUtil.isBlank(loginPassword)) {
config.getLogin().setPassword(password);
}
String loginUsername = config.getLogin().getUsername();
if (StrUtil.isBlank(loginUsername)) {
config.getLogin().setUsername(username);
}
Boolean proxy = config.getProxy();
if (proxy) {
String proxyHost = config.getProxyHost();
Integer proxyPort = config.getProxyPort();
if (StrUtil.isBlank(proxyHost) || Objects.isNull(proxyPort)) {
return Result.error("代理参数不完整");
}
}
ConfigUtil.sync();
Integer newRenameSleepSeconds = config.getRenameSleepSeconds();
Integer newSleep = config.getRssSleepMinutes();
// 时间间隔发生改变,重启任务
if (
!Objects.equals(newSleep, sleep) ||
!Objects.equals(newRenameSleepSeconds, renameSleepSeconds)
) {
TaskService.restart();
}
// 下载工具发生改变
if (!download.equals(config.getDownloadToolType())) {
TorrentUtil.load();
}
return Result.success("修改成功");
}
@Auth
@Operation(summary = "清理缓存")
@PostMapping("/clearCache")
public Result<Void> clearCache() {
File configDir = ConfigUtil.getConfigDir();
String configDirStr = FileUtils.getAbsolutePath(configDir);
Set<String> covers = AniUtil.ANI_LIST
.stream()
.map(Ani::getCover)
.map(s -> FileUtils.getAbsolutePath(new File(configDirStr + "/files/" + s)))
.collect(Collectors.toSet());
FileUtil.mkdir(configDirStr + "/files");
FileUtil.mkdir(configDirStr + "/img");
Set<File> files = FileUtil.loopFiles(configDirStr + "/files")
.stream()
.filter(file -> {
String fileName = FileUtils.getAbsolutePath(file);
return !covers.contains(fileName);
}).collect(Collectors.toSet());
long filesSize = files.stream()
.mapToLong(File::length)
.sum();
long imgSize = FileUtil.size(new File(configDirStr + "/img"));
long sumSize = filesSize + imgSize;
if (sumSize < 1) {
return Result.success("清理完成, 共清理{}MB", 0);
}
for (File file : files) {
FileUtil.del(file);
ClearService.clearParentFile(file);
}
FileUtil.del(configDirStr + "/img");
String mb = NumberUtil.decimalFormat("0.00", sumSize / 1024.0 / 1024.0);
return Result.success("清理完成, 共清理{}MB", mb);
}
@Auth
@Operation(summary = "更新trackers")
@PostMapping("/trackersUpdate")
public Result<Void> trackersUpdate(@RequestBody Config config) {
cronConfig.updateTrackers(config);
return Result.success();
}
@Auth
@Operation(summary = "代理测试")
@PostMapping("/testProxy")
public Result<ProxyTest> testProxy(@RequestParam("url") String url, @RequestBody Config config) {
url = Base64.decodeStr(url);
log.info(url);
HttpRequest httpRequest = HttpReq.get(url);
HttpReq.setProxy(httpRequest, config);
ProxyTest proxyTest = new ProxyTest();
Result<ProxyTest> result = Result.success(proxyTest);
long start = LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtil.now());
try {
httpRequest
.then(res -> {
int status = res.getStatus();
proxyTest.setStatus(status);
String title = Jsoup.parse(res.body())
.title();
result.setMessage(StrFormatter.format("测试成功 {}", title));
});
} catch (Exception e) {
result.setMessage(e.getMessage())
.setCode(HttpStatus.HTTP_INTERNAL_ERROR);
}
long end = LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtil.now());
proxyTest.setTime(end - start);
return result;
}
@Auth
@Operation(summary = "下载器测试")
@PostMapping("/downloadLoginTest")
public Result<Void> downloadLoginTest(@RequestBody Config config) {
ConfigUtil.format(config);
String download = config.getDownloadToolType();
Class<Object> loadClass = ClassUtil.loadClass("ani.rss.download." + download);
BaseDownload baseDownload = (BaseDownload) ReflectUtil.newInstance(loadClass);
Boolean login = baseDownload.login(true, config);
if (login) {
return Result.success("登录成功");
}
return Result.error("登录失败");
}
@Operation(summary = "自定义JS")
@GetMapping("/custom.js")
public void customJs() throws IOException {
HttpServletResponse response = Global.RESPONSE.get();
response.setHeader(Header.CACHE_CONTROL.toString(), "no-store, no-cache, must-revalidate, max-age=0");
response.setHeader(Header.PRAGMA.toString(), "no-cache");
response.setHeader("Expires", "0");
String customJs = ConfigUtil.CONFIG.getCustomJs();
customJs = StrUtil.blankToDefault(customJs, "// empty js");
String contentType = "application/javascript; charset=utf-8";
response.setContentType(contentType);
@Cleanup
OutputStream outputStream = response.getOutputStream();
IoUtil.writeUtf8(outputStream, true, customJs);
}
@Operation(summary = "自定义CSS")
@GetMapping("/custom.css")
public void customCss() throws IOException {
HttpServletResponse response = Global.RESPONSE.get();
response.setHeader(Header.CACHE_CONTROL.toString(), "no-store, no-cache, must-revalidate, max-age=0");
response.setHeader(Header.PRAGMA.toString(), "no-cache");
response.setHeader("Expires", "0");
String customCss = ConfigUtil.CONFIG.getCustomCss();
customCss = StrUtil.blankToDefault(customCss, "/* empty css */");
String contentType = "text/css";
response.setContentType(contentType);
@Cleanup
OutputStream outputStream = response.getOutputStream();
IoUtil.writeUtf8(outputStream, true, customCss);
}
}

View File

@@ -1,41 +1,38 @@
package ani.rss.action;
package ani.rss.controller;
import ani.rss.commons.GsonStatic;
import ani.rss.entity.Ani;
import ani.rss.entity.Config;
import ani.rss.entity.EmbyWebHook;
import ani.rss.annotation.Auth;
import ani.rss.entity.*;
import ani.rss.enums.StringEnum;
import ani.rss.service.DownloadService;
import ani.rss.util.other.*;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import ani.rss.web.auth.enums.AuthType;
import ani.rss.web.util.AuthUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.thread.ExecutorBuilder;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.Synchronized;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import wushuo.tmdb.api.entity.Tmdb;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
/**
* WebHook
*/
@Slf4j
@Auth(type = AuthType.API_KEY)
@Path("/web_hook")
public class WebHookAction implements BaseAction {
@RestController
@RequestMapping
public class EmbyController extends BaseController {
@Auth
@Operation(summary = "获取媒体库")
@PostMapping("/getEmbyViews")
public Result<List<EmbyViews>> getEmbyViews(@RequestBody NotificationConfig notificationConfig) {
List<EmbyViews> views = EmbyUtil.getViews(notificationConfig);
return Result.success(views);
}
private static final ExecutorService EXECUTOR = ExecutorBuilder.create()
.setCorePoolSize(1)
@@ -43,25 +40,19 @@ public class WebHookAction implements BaseAction {
.setWorkQueue(new LinkedBlockingQueue<>(256))
.build();
@Override
@Synchronized("EXECUTOR")
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String body = getBody();
Assert.notBlank(body, "WebHook body is empty");
log.debug("webhook: {}", body);
@Auth
@Operation(summary = "BGM自动点格子")
@PostMapping("/embyWebHook")
public Result<Void> embyWebHook(@RequestBody EmbyWebHook embyWebHook) {
log.debug("webhook: {}", embyWebHook);
Config config = ConfigUtil.CONFIG;
String bgmToken = config.getBgmToken();
if (StrUtil.isBlank(bgmToken)) {
log.info("bgmToken 为空");
response.sendOk();
return;
return Result.success();
}
EmbyWebHook embyWebHook = GsonStatic.fromJson(body, EmbyWebHook.class);
String event = embyWebHook.getEvent();
if (List.of("system.webhooktest", "system.notificationtest").contains(event)) {
@@ -83,8 +74,7 @@ public class WebHookAction implements BaseAction {
String ip = AuthUtil.getIp();
log.info(s, ip, id, name, version);
// 测试
response.sendOk();
return;
return Result.success();
}
EmbyWebHook.Item item = embyWebHook.getItem();
@@ -92,32 +82,27 @@ public class WebHookAction implements BaseAction {
String seriesName = item.getSeriesName();
String fileName = item.getFileName();
if (!ReUtil.contains(StringEnum.SEASON_REG, fileName)) {
response.sendOk();
return;
return Result.success();
}
//
int season = Integer.parseInt(ReUtil.get(StringEnum.SEASON_REG, fileName, 1));
// 番外
if (season < 1) {
response.sendOk();
return;
return Result.success();
}
// x.5
double episode = Double.parseDouble(ReUtil.get(StringEnum.SEASON_REG, fileName, 2));
if (ItemsUtil.is5(episode)) {
response.sendOk();
return;
return Result.success();
}
response.sendOk();
int type = getType(embyWebHook);
if (type < 0) {
// 播放状态未正确获取
return;
return Result.success();
}
EXECUTOR.execute(() -> {
@@ -142,6 +127,8 @@ public class WebHookAction implements BaseAction {
BgmUtil.collections(subjectId);
BgmUtil.collectionsEpisodes(episodeId, type);
});
return Result.success();
}
/**

View File

@@ -0,0 +1,130 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.commons.ExceptionUtils;
import ani.rss.entity.Global;
import ani.rss.util.other.ConfigUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.Header;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
@Slf4j
@RestController
public class FileController extends BaseController {
@Auth
@Operation(summary = "获取文件")
@GetMapping("/file")
public void file(@RequestParam("filename") String filename) {
if (Base64.isBase64(filename)) {
filename = filename.replace(" ", "+");
filename = Base64.decodeStr(filename);
}
doFile(filename);
}
/**
* 处理文件
*
* @param filename 文件名
*/
private void doFile(String filename) {
HttpServletRequest request = Global.REQUEST.get();
HttpServletResponse response = Global.RESPONSE.get();
File file = new File(filename);
if (!file.exists()) {
File configDir = ConfigUtil.getConfigDir();
file = new File(configDir + "/files/" + filename);
if (!file.exists()) {
writeNotFound();
return;
}
}
boolean hasRange = false;
long fileLength = file.length();
long start = 0;
long end = fileLength - 1;
String contentType = getContentType(file.getName());
response.setHeader(Header.CONTENT_DISPOSITION.toString(), StrFormatter.format("inline; filename=\"{}\"", URLUtil.encode(file.getName())));
if (contentType.startsWith("video/")) {
response.setContentType(contentType);
response.setHeader("Accept-Ranges", "bytes");
String rangeHeader = request.getHeader("Range");
if (StrUtil.isNotBlank(rangeHeader) && rangeHeader.startsWith("bytes=")) {
String[] range = rangeHeader.substring(6).split("-");
if (range.length > 0) {
start = Long.parseLong(range[0]);
}
if (range.length > 1) {
end = Long.parseLong(range[1]);
} else {
long maxEnd = start + (1024 * 1024 * 10);
end = Math.min(end, maxEnd);
}
}
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength);
hasRange = true;
} else {
long maxAge = 0;
// 小于或者等于 3M 缓存
if (fileLength <= 1024 * 1024 * 3) {
// 30 天
maxAge = 86400 * 30;
}
response.setHeader(Header.CACHE_CONTROL.toString(), "private, max-age=" + maxAge);
response.setContentType(contentType);
}
try {
if (hasRange) {
long length = end - start;
response.setStatus(206);
@Cleanup
OutputStream out = response.getOutputStream();
@Cleanup
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
randomAccessFile.seek(start);
@Cleanup
FileChannel channel = randomAccessFile.getChannel();
@Cleanup
InputStream inputStream = Channels.newInputStream(channel);
IoUtil.copy(inputStream, out, 40960, length, null);
} else {
@Cleanup
InputStream inputStream = FileUtil.getInputStream(file);
@Cleanup
OutputStream outputStream = response.getOutputStream();
IoUtil.copy(inputStream, outputStream);
}
} catch (Exception e) {
String message = ExceptionUtils.getMessage(e);
log.debug(message, e);
}
}
}

View File

@@ -1,37 +1,29 @@
package ani.rss.action;
package ani.rss.controller;
import ani.rss.commons.CacheUtils;
import ani.rss.entity.Config;
import ani.rss.entity.Login;
import ani.rss.entity.Result;
import ani.rss.util.other.AuthUtil;
import ani.rss.util.other.ConfigUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import ani.rss.web.util.AuthUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import static ani.rss.web.util.AuthUtil.limitLoginAttempts;
/**
* 登录
*/
@Slf4j
@Auth(value = false)
@Path("/login")
public class LoginAction implements BaseAction {
@RestController
public class LoginController extends BaseController {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
limitLoginAttempts(false);
@Operation(summary = "登录")
@PostMapping("/login")
public Result<String> login(@RequestBody Login myLogin) {
AuthUtil.limitLoginAttempts(false);
Login myLogin = getBody(Login.class);
Config config = ConfigUtil.CONFIG;
Login login = config.getLogin();
@@ -57,25 +49,25 @@ public class LoginAction implements BaseAction {
clearLimitLoginAttempts();
log.info("登录成功 {} ip: {}", username, ip);
String s = AuthUtil.getAuth(myLogin);
resultSuccess(s);
return;
return new Result<String>()
.setCode(200)
.setMessage("登录成功")
.setData(s);
}
limitLoginAttempts(true);
AuthUtil.limitLoginAttempts(true);
log.warn("登陆失败 {} ip: {}", myUsername, ip);
ThreadUtil.sleep(RandomUtil.randomInt(500, 5000));
resultErrorMsg("用户名或密码错误");
return Result.error("用户名或密码错误");
}
/**
* 清除限制尝试次数
*/
public static void clearLimitLoginAttempts() {
private void clearLimitLoginAttempts() {
String ip = AuthUtil.getIp();
String key = "LimitLoginAttempts#" + ip;
if (CacheUtils.containsKey(key)) {
CacheUtils.remove(key);
}
}
}

View File

@@ -0,0 +1,79 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.entity.Global;
import ani.rss.entity.Log;
import ani.rss.entity.Result;
import ani.rss.util.basic.LogUtil;
import ani.rss.util.other.ConfigUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import cn.hutool.http.Header;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Slf4j
@RestController
public class LogsController extends BaseController {
List<Log> LOG_LIST = LogUtil.LOG_LIST;
@Auth
@Operation(summary = "日志")
@PostMapping("/logs")
public Result<List<Log>> list() {
return Result.success(LOG_LIST);
}
@Auth
@Operation(summary = "清理日志")
@PostMapping("/clearLogs")
public Result<Void> clearLogs() {
LOG_LIST.clear();
log.info("清理日志");
return Result.success();
}
@Auth
@Operation(summary = "下载日志")
@GetMapping("/downloadLogs")
public void downloadLogs() throws IOException {
File configDir = ConfigUtil.getConfigDir();
String logsPath = configDir + "/logs";
String filename = "logs.zip";
String contentType = getContentType(filename);
HttpServletResponse response = Global.RESPONSE.get();
response.setContentType(contentType);
response.setHeader(Header.CONTENT_DISPOSITION.toString(), StrFormatter.format("inline; filename=\"{}\"", filename));
@Cleanup
OutputStream outputStream = response.getOutputStream();
ZipUtil.zip(outputStream, StandardCharsets.UTF_8, false, name -> {
if (FileUtil.isDirectory(name)) {
return true;
}
String extName = FileUtil.extName(name);
if (StrUtil.isBlank(extName)) {
return false;
}
return extName.equals("log");
}, new File(logsPath));
}
}

View File

@@ -0,0 +1,162 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.commons.GsonStatic;
import ani.rss.entity.Global;
import ani.rss.entity.Mikan;
import ani.rss.entity.Result;
import ani.rss.entity.TorrentsInfo;
import ani.rss.util.basic.HttpReq;
import ani.rss.util.other.ConfigUtil;
import ani.rss.util.other.MikanUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpConnection;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Cleanup;
import org.springframework.web.bind.annotation.*;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
@RestController
public class MikanController extends BaseController {
@Auth
@Operation(summary = "获取Mikan番剧列表")
@PostMapping("/mikan")
public Result<Mikan> mikan(@RequestParam("text") String text, @RequestBody Mikan.Season season) {
Mikan list = MikanUtil.list(text, season);
return Result.success(list);
}
@Auth
@Operation(summary = "获取Mikan番剧的字幕组列表")
@PostMapping("/mikanGroup")
public Result<List<Mikan.Group>> mikanGroup(@RequestParam("url") String url) {
List<Mikan.Group> groups = MikanUtil.getGroups(url);
List<String> regexItemList = List.of(
"1920[Xx]1080", "3840[Xx]2160", "1080[Pp]", "4[Kk]", "720[Pp]",
"", "", "",
"cht|Cht|CHT", "chs|Chs|CHS", "hevc|Hevc|HEVC",
"10bit|10Bit|10BIT", "h265|H265", "h264|H264",
"内嵌", "内封", "外挂",
"mp4|MP4", "mkv|MKV"
);
for (Mikan.Group group : groups) {
Set<String> tags = new HashSet<>();
List<List<Mikan.RegexItem>> regexList = new ArrayList<>();
List<TorrentsInfo> items = group.getItems();
for (TorrentsInfo item : items) {
String name = item.getName();
List<Mikan.RegexItem> regexItems = new ArrayList<>();
for (String regex : regexItemList) {
if (!ReUtil.contains(regex, name)) {
continue;
}
String label = ReUtil.get(regex, name, 0);
label = label.toUpperCase();
Mikan.RegexItem regexItem = new Mikan.RegexItem(label, regex);
regexItems.add(regexItem);
tags.add(label);
}
regexItems = CollUtil.distinct(regexItems, GsonStatic::toJson, true);
regexList.add(regexItems);
}
regexList = CollUtil.distinct(regexList, GsonStatic::toJson, true);
group.setRegexList(regexList)
.setTags(tags);
}
return Result.success(groups);
}
@Auth
@Operation(summary = "获取Mikan封面")
@GetMapping("/mikanCover")
public void MikanCover(@RequestParam("img") String img) {
if (Base64.isBase64(img)) {
img = img.replace(" ", "+");
img = Base64.decodeStr(img);
}
HttpServletResponse response = Global.RESPONSE.get();
// 30 天
long maxAge = 86400 * 30;
response.setHeader(Header.CACHE_CONTROL.toString(), "private, max-age=" + maxAge);
String contentType = getContentType(URLUtil.getPath(img));
File configDir = ConfigUtil.getConfigDir();
File file = new File(URLUtil.getPath(img));
configDir = new File(configDir + "/img/" + file.getParentFile().getName());
FileUtil.mkdir(configDir);
File imgFile = new File(configDir, file.getName());
if (imgFile.exists()) {
try {
response.setContentType(contentType);
@Cleanup
InputStream inputStream = FileUtil.getInputStream(imgFile);
@Cleanup
OutputStream outputStream = response.getOutputStream();
IoUtil.copy(inputStream, outputStream);
} catch (Exception ignored) {
}
return;
}
getImg(img, is -> {
try {
response.setContentType(contentType);
FileUtil.writeFromStream(is, imgFile, true);
@Cleanup
BufferedInputStream inputStream = FileUtil.getInputStream(imgFile);
@Cleanup
ServletOutputStream outputStream = response.getOutputStream();
IoUtil.copy(inputStream, outputStream);
} catch (Exception ignored) {
}
});
}
public void getImg(String url, Consumer<InputStream> consumer) {
URI host = URLUtil.getHost(URLUtil.url(url));
HttpReq.get(url)
.then(res -> {
HttpConnection httpConnection = (HttpConnection) ReflectUtil.getFieldValue(res, "httpConnection");
URI host1 = URLUtil.getHost(httpConnection.getUrl());
if (host.toString().equals(host1.toString())) {
try {
@Cleanup
InputStream inputStream = res.bodyStream();
consumer.accept(inputStream);
} catch (Exception ignored) {
}
return;
}
String newUrl = url.replace(host.toString(), host1.toString());
getImg(newUrl, consumer);
});
}
}

View File

@@ -1,53 +1,35 @@
package ani.rss.action;
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.entity.Ani;
import ani.rss.entity.BgmInfo;
import ani.rss.entity.NotificationConfig;
import ani.rss.entity.Result;
import ani.rss.enums.NotificationStatusEnum;
import ani.rss.enums.NotificationTypeEnum;
import ani.rss.notification.BaseNotification;
import ani.rss.notification.TelegramNotification;
import ani.rss.util.other.AniUtil;
import ani.rss.util.other.BgmUtil;
import ani.rss.util.other.NotificationUtil;
import ani.rss.util.other.TmdbUtils;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import wushuo.tmdb.api.entity.Tmdb;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
/**
* 通知
*/
@RestController
public class NotificationController extends BaseController {
@Auth
@Path("/notification")
public class NotificationAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
String type = request.getParam("type");
if ("test".equals(type)) {
test();
return;
}
if ("add".equals(type)) {
add();
}
}
private void add() {
NotificationConfig notificationConfig = NotificationConfig.createNotificationConfig();
resultSuccess(notificationConfig);
}
private void test() {
NotificationConfig notificationConfig = getBody(NotificationConfig.class);
@Operation(summary = "测试通知")
@PostMapping("/testNotification")
public Result<Void> testNotification(@RequestBody NotificationConfig notificationConfig) {
NotificationTypeEnum notificationType = notificationConfig.getNotificationType();
Class<? extends BaseNotification> aClass = NotificationUtil.NOTIFICATION_MAP.get(notificationType);
BaseNotification baseNotification = ReflectUtil.newInstance(aClass);
@@ -69,10 +51,26 @@ public class NotificationAction implements BaseAction {
try {
baseNotification.test(notificationConfig, ani, "test", NotificationStatusEnum.DOWNLOAD_START);
resultSuccess();
return Result.success();
} catch (Exception e) {
resultErrorMsg(e.getMessage());
return Result.error(e.getMessage());
}
}
@Auth
@Operation(summary = "新的通知")
@PostMapping("/newNotification")
public Result<NotificationConfig> newNotification() {
NotificationConfig notificationConfig = NotificationConfig.createNotificationConfig();
return Result.success(notificationConfig);
}
@Auth
@Operation(summary = "获取TG最近消息")
@PostMapping("/getTgUpdates")
public Result<Map<String, String>> getUpdates(@RequestBody NotificationConfig notificationConfig) {
Map<String, String> map = TelegramNotification.getUpdates(notificationConfig);
return Result.success(map);
}
}

View File

@@ -1,45 +1,98 @@
package ani.rss.action;
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.commons.FileUtils;
import ani.rss.entity.Ani;
import ani.rss.entity.PlayItem;
import ani.rss.entity.Result;
import ani.rss.enums.StringEnum;
import ani.rss.service.DownloadService;
import ani.rss.util.other.AniUtil;
import ani.rss.web.action.BaseAction;
import ani.rss.web.annotation.Auth;
import ani.rss.web.annotation.Path;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import lombok.extern.slf4j.Slf4j;
import com.matthewn4444.ebml.EBMLReader;
import com.matthewn4444.ebml.subtitles.Subtitles;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Cleanup;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* 视频列表
*/
@RestController
public class PlayController extends BaseController {
@Auth
@Slf4j
@Path("/playlist")
public class PlaylistAction implements BaseAction {
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException {
Ani ani = getBody(Ani.class);
@Operation(summary = "获取内封字幕")
@PostMapping("/getSubtitles")
public Result<List<PlayItem.Subtitles>> getSubtitles(@RequestBody Map<String, String> map) throws IOException {
String file = map.get("file");
Assert.notBlank(file);
if (Base64.isBase64(file)) {
file = Base64.decodeStr(file);
}
List<PlayItem.Subtitles> subtitlesList = new ArrayList<>();
String extName = FileUtil.extName(file);
if (StrUtil.isBlank(extName)) {
return Result.success(subtitlesList);
}
if (!"mkv".equals(extName)) {
return Result.success(subtitlesList);
}
Assert.isTrue(FileUtil.exist(file), "视频文件不存在");
@Cleanup
EBMLReader reader = new EBMLReader(file);
if (!reader.readHeader()) {
return Result.success(subtitlesList);
}
reader.readTracks();
reader.readCues();
for (int i = 0; i < reader.getCuesCount(); i++) {
reader.readSubtitlesInCueFrame(i);
}
List<Subtitles> subtitles = reader.getSubtitles();
for (Subtitles subtitle : subtitles) {
String name = subtitle.getName();
String presentableName = subtitle.getPresentableName();
String contents = subtitle.getContentsToVTT();
PlayItem.Subtitles sub = new PlayItem.Subtitles();
sub.setContent(contents)
.setName(name)
.setHtml(presentableName)
.setUrl("")
.setType("vtt");
subtitlesList.add(sub);
}
return Result.success(subtitlesList);
}
@Auth
@Operation(summary = "获取视频列表")
@PostMapping("/playList")
public Result<List<PlayItem>> playList(@RequestBody Ani ani) {
String url = ani.getUrl();
Optional<Ani> first = AniUtil.ANI_LIST
.stream()
.filter(it -> url.equals(it.getUrl()))
.findFirst();
if (first.isEmpty()) {
resultError();
return;
return Result.error();
}
ani = first.get();
@@ -49,7 +102,7 @@ public class PlaylistAction implements BaseAction {
// 按照集数排序
CollUtil.sort(collect, Comparator.comparingDouble(PlayItem::getEpisode));
resultSuccess(collect);
return Result.success(collect);
}
/**

View File

@@ -0,0 +1,29 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.entity.Ani;
import ani.rss.entity.Result;
import ani.rss.service.ScrapeService;
import cn.hutool.core.thread.ThreadUtil;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ScrapeController extends BaseController {
@Auth
@Operation(summary = "刮削")
@PostMapping("/scrape")
public Result<Void> scrape(@RequestParam("force") Boolean force, @RequestBody Ani ani) {
ThreadUtil.execute(() ->
ScrapeService.scrape(ani, force)
);
String title = ani.getTitle();
return Result.success("已开始刮削 {}", title);
}
}

View File

@@ -0,0 +1,48 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.entity.Ani;
import ani.rss.entity.Result;
import ani.rss.util.other.TmdbUtils;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpStatus;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import wushuo.tmdb.api.entity.Tmdb;
import wushuo.tmdb.api.entity.TmdbGroup;
import java.util.List;
@RestController
public class ThemoviedbController extends BaseController {
@Auth
@Operation(summary = "获取TMDB标题")
@PostMapping("/getThemoviedbName")
public Result<Ani> getThemoviedbName(@RequestBody Ani ani) {
String themoviedbName = TmdbUtils.getFinalName(ani);
Result<Ani> result = new Result<Ani>()
.setCode(HttpStatus.HTTP_OK)
.setMessage("获取TMDB成功")
.setData(ani.setThemoviedbName(themoviedbName));
if (StrUtil.isBlank(themoviedbName)) {
result.setCode(HttpStatus.HTTP_INTERNAL_ERROR)
.setMessage("获取TMDB失败");
}
return result;
}
@Auth
@Operation(summary = "获取TMDB剧集组")
@PostMapping("/getThemoviedbGroup")
public Result<List<TmdbGroup>> getThemoviedbGroup(@RequestBody Ani ani) {
Tmdb tmdb = ani.getTmdb();
Assert.notNull(tmdb, "tmdb is null");
Assert.notBlank(tmdb.getId(), "tmdb is null");
List<TmdbGroup> tmdbGroup = TmdbUtils.getTmdbGroup(tmdb);
return Result.success(tmdbGroup);
}
}

View File

@@ -0,0 +1,49 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.commons.FileUtils;
import ani.rss.entity.Ani;
import ani.rss.entity.Result;
import ani.rss.util.other.AniUtil;
import ani.rss.util.other.TorrentUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.File;
import java.util.List;
import java.util.Optional;
@Slf4j
@RestController
public class TorrentController extends BaseController {
@Auth
@Operation(summary = "删除缓存种子")
@PostMapping("/deleteTorrent")
public Result<Void> del(@RequestParam("id") String id, @RequestParam("hash") String hash) {
Optional<Ani> first = AniUtil.ANI_LIST.stream()
.filter(ani -> id.equals(ani.getId()))
.findFirst();
if (first.isEmpty()) {
return Result.error("此订阅不存在");
}
List<String> hashList = StrUtil.split(hash, ",", true, true);
Ani ani = first.get();
File torrentDir = TorrentUtil.getTorrentDir(ani);
File[] files = FileUtils.listFiles(torrentDir);
for (File file : files) {
String name = FileUtil.mainName(file);
if (hashList.contains(name)) {
log.info("删除种子 {}", file);
FileUtil.del(file);
}
}
return Result.success("删除完成");
}
}

View File

@@ -0,0 +1,24 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.entity.Result;
import ani.rss.entity.TorrentsInfo;
import ani.rss.util.other.TorrentUtil;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class TorrentsInfosController extends BaseController {
@Auth
@Operation(summary = "下载列表")
@PostMapping("/torrentsInfos")
public Result<List<TorrentsInfo>> torrentsInfos() {
List<TorrentsInfo> torrentsInfos = TorrentUtil.getTorrentsInfos();
return Result.success(torrentsInfos);
}
}

View File

@@ -0,0 +1,47 @@
package ani.rss.controller;
import ani.rss.annotation.Auth;
import ani.rss.entity.Global;
import ani.rss.entity.Result;
import ani.rss.util.other.ConfigUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.crypto.SecureUtil;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
@RestController
public class UploadController extends BaseController {
@Auth
@Operation(summary = "上传文件")
@PostMapping("/upload")
public Result<Object> upload(@RequestParam("file") MultipartFile file) throws IOException {
HttpServletRequest request = Global.REQUEST.get();
String type = request.getParameter("type");
byte[] fileContent = file.getBytes();
if ("getBase64".equals(type)) {
return Result.success(r ->
r.setData(Base64.encode(fileContent))
);
}
String s = SecureUtil.md5(new ByteArrayInputStream(fileContent));
String fileName = file.getOriginalFilename();
String saveName = s + "." + FileUtil.extName(fileName);
File configDir = ConfigUtil.getConfigDir();
FileUtil.mkdir(configDir + "/files/" + s.charAt(0));
FileUtil.writeBytes(fileContent, configDir + "/files/" + s.charAt(0) + "/" + saveName);
return new Result<>()
.setMessage("上传完成")
.setData(s.charAt(0) + "/" + saveName);
}
}

View File

@@ -19,7 +19,9 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpResponse;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.*;
@@ -28,6 +30,8 @@ import java.util.*;
* Aria2
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class Aria2 implements BaseDownload {
private Config config;

View File

@@ -25,13 +25,17 @@ import cn.hutool.http.HttpResponse;
import cn.hutool.http.Method;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.*;
import java.util.stream.Stream;
@Slf4j
@Service
@RequiredArgsConstructor
public class OpenList implements BaseDownload {
private Config config;

View File

@@ -22,7 +22,9 @@ import cn.hutool.http.HttpResponse;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.ArrayList;
@@ -34,15 +36,15 @@ import java.util.Set;
* Transmission
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class Transmission implements BaseDownload {
private String host = "";
private String authorization = "";
private String sessionId = "";
private Config config;
@Override
public Boolean login(Boolean test, Config config) {
this.config = config;
String username = config.getDownloadToolUsername();
String password = config.getDownloadToolPassword();
host = config.getDownloadToolHost();

View File

@@ -24,8 +24,10 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.*;
@@ -34,7 +36,10 @@ import java.util.*;
* qBittorrent
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class qBittorrent implements BaseDownload {
private Config config;
/**

View File

@@ -1,5 +1,6 @@
package ani.rss.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -11,39 +12,47 @@ import java.util.Date;
*/
@Data
@Accessors(chain = true)
@Schema(description = "关于")
public class About implements Serializable {
/**
* 版本
*/
@Schema(description = "版本")
private String version;
/**
* 最新版本
*/
@Schema(description = "最新版本")
private String latest;
/**
* 是否需要更新
*/
@Schema(description = "是否需要更新")
private Boolean update;
/**
* 是否允许自动更新
*/
@Schema(description = "是否允许自动更新")
private Boolean autoUpdate;
/**
* 下载地址
*/
@Schema(description = "下载地址")
private String downloadUrl;
/**
* 更新内容
*/
@Schema(description = "更新内容")
private String markdownBody;
/**
* 发布时间
*/
@Schema(description = "发布时间")
private Date date;
}

View File

@@ -1,6 +1,7 @@
package ani.rss.entity;
import com.google.gson.annotations.SerializedName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import wushuo.tmdb.api.entity.Tmdb;
@@ -13,20 +14,24 @@ import java.util.List;
*/
@Data
@Accessors(chain = true)
@Schema(description = "订阅")
public class Ani implements Serializable {
/**
* id
*/
@Schema(description = "id")
private String id;
/**
* 不在页面显示
*/
@Schema(description = "不在页面显示")
private String mikanTitle;
/**
* RSS URL
*/
@Schema(description = "RSS URL")
private String url;
private Boolean exists;
@@ -34,239 +39,288 @@ public class Ani implements Serializable {
/**
* 备用rss
*/
@Schema(description = "备用rss")
private List<StandbyRss> standbyRssList;
/**
* 标题
*/
@Schema(description = "标题")
private String title;
/**
* 日语标题 来源于BGM
*/
@Schema(description = "日语标题 来源于BGM")
private String jpTitle;
/**
* 剧集偏移
*/
@Schema(description = "剧集偏移")
private Integer offset;
/**
* 年度
*/
@Schema(description = "年度")
private Integer year;
/**
*
*/
@Schema(description = "")
private Integer month;
/**
*
*/
@Schema(description = "")
private Integer date;
/**
* 星期 1表示周日2表示周一
*/
@Schema(description = "星期 1表示周日2表示周一")
private Integer week;
/**
* 季度
*/
@Schema(description = "季度")
private Integer season;
/**
* 封面本地保存位置
*/
@Schema(description = "封面本地保存位置")
private String cover;
/**
* 图片 https://
*/
@Schema(description = "图片 https://")
private String image;
/**
* 字幕组
*/
@Schema(description = "字幕组")
private String subgroup;
/**
* 匹配
*/
@Schema(description = "匹配")
private List<String> match;
/**
* 排除
*/
@Schema(description = "排除")
private List<String> exclude;
/**
* 是否启用全局排除
*/
@Schema(description = "是否启用全局排除")
private Boolean globalExclude;
/**
* 剧场版 or OVA
*/
@Schema(description = "剧场版 or OVA")
private Boolean ova;
/**
* 拼音
*/
@Schema(description = "拼音")
private String pinyin;
/**
* 拼音
*/
@Schema(description = "拼音首字母")
private String pinyinInitials;
/**
* 启用
*/
@Schema(description = "启用")
private Boolean enable;
/**
* 当前集数
*/
@Schema(description = "当前集数")
private Integer currentEpisodeNumber;
/**
* 总集数
*/
@Schema(description = "总集数")
private Integer totalEpisodeNumber;
@Schema(description = "TheMovieDB 名称")
private String themoviedbName;
@Schema(description = "类型")
private String type;
@Schema(description = "BGM 地址")
private String bgmUrl;
/**
* 自定义下载位置
*/
@Schema(description = "自定义下载位置")
private Boolean customDownloadPath;
/**
* 自定义下载位置
*/
@Schema(description = "自定义下载位置路径")
private String downloadPath;
/**
* 评分
*/
@Schema(description = "评分")
private Double score;
/**
* 自定义集数获取规则
*/
@Schema(description = "自定义集数获取规则")
private Boolean customEpisode;
/**
* 自定义集数获取规则
*/
@Schema(description = "自定义集数获取规则表达式")
private String customEpisodeStr;
/**
* 自定义集数获取规则 groupIndex
*/
@Schema(description = "自定义集数获取规则 groupIndex")
private Integer customEpisodeGroupIndex;
/**
* 遗漏检测
*/
@Schema(description = "遗漏检测")
private Boolean omit;
/**
* 只下载最新集
*/
@Schema(description = "只下载最新集")
private Boolean downloadNew;
/**
* 不进行下载的集
*/
@Schema(description = "不进行下载的集")
private List<Double> notDownload;
/**
* tmdb 相关信息
*/
@Schema(description = "TMDB 相关信息")
private Tmdb tmdb;
/**
* 自动上传
*/
@Schema(description = "自动上传")
private Boolean upload;
/**
* 摸鱼
*/
@Schema(description = "摸鱼")
private Boolean procrastinating;
/**
* 自定义重命名模版
*/
@Schema(description = "自定义重命名模版开关")
private Boolean customRenameTemplateEnable;
/**
* 自定义重命名模版
*/
@Schema(description = "自定义重命名模版")
private String customRenameTemplate;
/**
* 自定义优先保留开关
*/
@Schema(description = "自定义优先保留开关")
private Boolean customPriorityKeywordsEnable;
/**
* 自定义优先保留关键词列表
*/
@Schema(description = "自定义优先保留关键词列表")
private List<String> customPriorityKeywords;
/**
* 上次下载完成时间
*/
@Schema(description = "上次下载完成时间")
private Long lastDownloadTime;
/**
* 自定义上传
*/
@SerializedName(value = "customUploadEnable", alternate = "customAlistPath")
@Schema(description = "自定义上传开关")
private Boolean customUploadEnable;
/**
* 自定义上传
*/
@SerializedName(value = "customUploadPathTarget", alternate = "alistPath")
@Schema(description = "自定义上传目标路径")
private String customUploadPathTarget;
/**
* 消息通知
*/
@Schema(description = "消息通知")
private Boolean message;
/**
* 完结迁移
*/
@Schema(description = "完结迁移")
private Boolean completed;
/**
* 自定义完结迁移
*/
@Schema(description = "自定义完结迁移开关")
private Boolean customCompleted;
/**
* 自定义完结迁移
*/
@Schema(description = "自定义完结迁移路径模版")
private String customCompletedPathTemplate;
/**
* 自定义标签开关
*/
@Schema(description = "自定义标签开关")
private Boolean customTagsEnable;
/**
* 单个订阅自定义标签
*/
@Schema(description = "单个订阅自定义标签")
private List<String> customTags;

View File

@@ -1,6 +1,7 @@
package ani.rss.entity;
import com.google.gson.annotations.SerializedName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -14,6 +15,7 @@ import java.util.Map;
*/
@Data
@Accessors(chain = true)
@Schema(description = "Bgm番剧信息")
public class BgmInfo implements Serializable {
private String id;
@@ -22,37 +24,44 @@ public class BgmInfo implements Serializable {
/**
* 名称
*/
@Schema(description = "名称")
private String name;
/**
* 中文名称
*/
@SerializedName(value = "nameCn", alternate = "name_cn")
@Schema(description = "中文名称")
private String nameCn;
/**
* 集数
*/
@Schema(description = "集数")
private Integer eps;
/**
* 时间
*/
@Schema(description = "时间")
private Date date;
/**
* 图片
*/
@Schema(description = "图片")
private Images images;
/**
* 季度
*/
@Schema(description = "季度")
private Integer season;
/**
* 平台 OVA/剧场版
*/
@Schema(description = "平台 OVA/剧场版")
private String platform;
private List<Tag> tags;
@@ -64,6 +73,7 @@ public class BgmInfo implements Serializable {
*/
@Data
@Accessors(chain = true)
@Schema(description = "封面图片")
public static class Images implements Serializable {
private String small;
private String grid;
@@ -77,12 +87,16 @@ public class BgmInfo implements Serializable {
*/
@Data
@Accessors(chain = true)
@Schema(description = "标签")
public static class Tag implements Serializable {
@Schema(description = "标签名")
private String name;
@Schema(description = "计数")
private String count;
@SerializedName(value = "totalCont", alternate = "total_cont")
@Schema(description = "总计数")
private String totalCont;
}
@@ -91,25 +105,30 @@ public class BgmInfo implements Serializable {
*/
@Data
@Accessors(chain = true)
@Schema(description = "评分")
public static class Rating implements Serializable {
/**
* 级别
*/
@Schema(description = "级别")
private Integer rank;
/**
* 评分
*/
@Schema(description = "评分")
private Double score;
/**
* 评分数
*/
@Schema(description = "评分数")
private Integer total;
/**
* 各阶段评分数
*/
@Schema(description = "各阶段评分数")
private Map<String, Integer> count;
}
}

View File

@@ -0,0 +1,54 @@
package ani.rss.entity;
import com.google.gson.annotations.SerializedName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@Accessors(chain = true)
@Schema(description = "BGM 用户信息")
public class BgmMe implements Serializable {
@Schema(description = "头像")
private Avatar avatar;
@Schema(description = "用户ID")
private Integer id;
@Schema(description = "签名")
private String sign;
@Schema(description = "主页地址")
private String url;
@Schema(description = "用户名")
private String username;
@Schema(description = "昵称")
private String nickname;
@SerializedName(value = "userGroup", alternate = "user_group")
@Schema(description = "用户组")
private String userGroup;
@SerializedName(value = "regTime", alternate = "reg_time")
@Schema(description = "注册时间")
private Date regTime;
@Schema(description = "邮箱")
private String email;
@SerializedName(value = "timeOffset", alternate = "time_offset")
@Schema(description = "时区偏移")
private Integer timeOffset;
@SerializedName(value = "expiresDays", alternate = "expires_days")
@Schema(description = "过期天数")
private Integer expiresDays;
@Data
@Accessors(chain = true)
@Schema(description = "头像")
public static class Avatar implements Serializable {
@Schema(description = "large")
private String large;
@Schema(description = "medium")
private String medium;
@Schema(description = "small")
private String small;
}
}

View File

@@ -1,5 +1,6 @@
package ani.rss.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -10,19 +11,23 @@ import java.io.Serializable;
*/
@Data
@Accessors(chain = true)
@Schema(description = "合集")
public class CollectionInfo implements Serializable {
/**
* 种子文件 base64
*/
@Schema(description = "种子文件 base64")
private String torrent;
/**
* 订阅
*/
@Schema(description = "订阅")
private Ani ani;
/**
* bgm
*/
@Schema(description = "bgm")
private BgmInfo bgmInfo;
}

View File

@@ -2,6 +2,7 @@ package ani.rss.entity;
import ani.rss.enums.BgmTokenTypeEnum;
import ani.rss.enums.SortTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -13,305 +14,366 @@ import java.util.List;
*/
@Data
@Accessors(chain = true)
@Schema(description = "设置")
public class Config implements Serializable {
/**
* Mikan Host
*/
@Schema(description = "Mikan Host")
private String mikanHost;
/**
* tmdbApi
*/
@Schema(description = "TMDB API")
private String tmdbApi;
/**
* tmdbApiKey
*/
@Schema(description = "TMDB API Key")
private String tmdbApiKey;
/**
* 仅获取动漫
*/
@Schema(description = "仅获取动漫")
private Boolean tmdbAnime;
/**
* 下载工具
*/
@Schema(description = "下载工具类型")
private String downloadToolType;
/**
* 下载重试次数
*/
@Schema(description = "下载重试次数")
private Integer downloadRetry;
/**
* 下载工具 地址
*/
@Schema(description = "下载工具地址")
private String downloadToolHost;
/**
* 下载工具 用户名
*/
@Schema(description = "下载工具用户名")
private String downloadToolUsername;
/**
* 下载工具 密码
*/
@Schema(description = "下载工具密码")
private String downloadToolPassword;
/**
* qb下载时使用qb自身的保存路径配置(未下载完成的使用临时目录复制种子文件)
*/
@Schema(description = "使用 qb 自身保存路径")
private Boolean qbUseDownloadPath;
/**
* 分享率
*/
@Schema(description = "分享率")
private Integer ratioLimit;
/**
* 总做种时长
*/
@Schema(description = "总做种时长")
private Integer seedingTimeLimit;
/**
* 非活跃时长
*/
@Schema(description = "非活跃时长")
private Integer inactiveSeedingTimeLimit;
/**
* 下载路径
*/
@Schema(description = "下载路径模版")
private String downloadPathTemplate;
/**
* 剧场版下载路径
*/
@Schema(description = "剧场版下载路径模版")
private String ovaDownloadPathTemplate;
/**
* 自定义标签
*/
@Schema(description = "自定义标签")
private List<String> customTags;
/*
* 优先保留开关
*/
@Schema(description = "优先保留开关")
private Boolean priorityKeywordsEnable;
/**
* 优先保留关键词列表
*/
@Schema(description = "优先保留关键词列表")
private List<String> priorityKeywords;
/**
* 延迟下载
*/
@Schema(description = "延迟下载(分钟)")
private Integer delayedDownload;
/**
* 显示评分
*/
@Schema(description = "显示评分")
private Boolean scoreShow;
/**
* RSS 间隔(分钟)
*/
@Schema(description = "RSS 间隔(分钟)")
private Integer rssSleepMinutes;
/**
* 重命名间隔()
*/
@Schema(description = "重命名间隔(秒)")
private Integer renameSleepSeconds;
/**
* 自动重命名
*/
@Schema(description = "自动重命名")
private Boolean rename;
/**
* rss开关
*/
@Schema(description = "RSS 开关")
private Boolean rss;
/**
* rss 超时时间
*/
@Schema(description = "RSS 超时时间(秒)")
private Integer rssTimeout;
/**
* 文件已下载自动跳过
*/
@Schema(description = "文件已下载自动跳过")
private Boolean fileExist;
/**
* 等待做种完毕
*/
@Schema(description = "等待做种完毕")
private Boolean awaitStalledUP;
/**
* 自动删除已完成任务
*/
@Schema(description = "自动删除已完成任务")
private Boolean delete;
/**
* 仅在主RSS更新后删除备用RSS
*/
@Schema(description = "主RSS更新后删除备用RSS")
private Boolean deleteStandbyRSSOnly;
/**
* 自动推断剧集偏移
*/
@Schema(description = "自动推断剧集偏移")
private Boolean offset;
/**
* 获取标题时带上年份
*/
@Schema(description = "获取标题时带上年份")
private Boolean titleYear;
/**
* 自动禁用已完结番剧的订阅
*/
@Schema(description = "自动禁用已完结番剧订阅")
private Boolean autoDisabled;
/**
* 自动跳过 x.5 集数
*/
@Schema(description = "自动跳过 x.5 集数")
private Boolean skip5;
/**
* 备用RSS
*/
@Schema(description = "备用RSS")
private Boolean standbyRss;
/**
* 多字幕组共存模式
*/
@Schema(description = "多字幕组共存模式")
private Boolean coexist;
/**
* 最大日志条数
*/
@Schema(description = "最大日志条数")
private Integer logsMax;
/**
* DEBUG
*/
@Schema(description = "DEBUG")
private Boolean debug;
/**
* 仅启用主rss摸鱼检测
*/
@Schema(description = "仅启用主RSS摸鱼检测")
private Boolean procrastinatingMasterOnly;
/**
* 代理是否开启
*/
@Schema(description = "代理是否开启")
private Boolean proxy;
/**
* 代理host
*/
@Schema(description = "代理 host")
private String proxyHost;
/**
* 代理端口
*/
@Schema(description = "代理端口")
private Integer proxyPort;
/**
* 代理用户名
*/
@Schema(description = "代理用户名")
private String proxyUsername;
/**
* 代理密码
*/
@Schema(description = "代理密码")
private String proxyPassword;
/**
* 同时下载数量限制
*/
@Schema(description = "同时下载数量限制")
private Integer downloadCount;
/**
* 登录信息
*/
@Schema(description = "登录信息")
private Login login;
/**
* 禁止多端登录
*/
@Schema(description = "禁止多端登录")
private Boolean multiLoginForbidden;
/**
* 登录有效时间/小时
*/
@Schema(description = "登录有效时间(小时)")
private Integer loginEffectiveHours;
/**
* 全局排除
*/
@Schema(description = "全局排除")
private List<String> exclude;
/**
* 默认导入全局排除
*/
@Schema(description = "默认导入全局排除")
private Boolean importExclude;
/**
* 默认启用全局排除
*/
@Schema(description = "默认启用全局排除")
private Boolean enabledExclude;
/**
* BGM日语标题
*/
@Schema(description = "BGM日语标题")
private Boolean bgmJpName;
/**
* tmdb
*/
@Schema(description = "启用 TMDB")
private Boolean tmdb;
/**
* 获取标题时带有tmdbId
*/
@Schema(description = "标题带 TMDB ID")
private Boolean tmdbId;
/**
* 剧集标题是否支持plex命名方式
*/
@Schema(description = "Plex 命名方式")
private Boolean tmdbIdPlexMode;
/**
* tmdb 语言
*/
@Schema(description = "TMDB 语言")
private String tmdbLanguage;
/**
* 获取罗马音
*/
@Schema(description = "获取罗马音")
private Boolean tmdbRomaji;
/**
* 开启ip白名单
*/
@Schema(description = "开启 IP 白名单")
private Boolean ipWhitelist;
/**
* ip白名单
*/
@Schema(description = "IP 白名单")
private String ipWhitelistStr;
/**
* 显示已下载视频列表
*/
@Schema(description = "显示已下载视频列表")
private Boolean showPlaylist;
/**
* 检测遗漏集数
*/
@Schema(description = "检测遗漏集数")
private Boolean omit;
/**
@@ -319,295 +381,354 @@ public class Config implements Serializable {
* <p>
* INPUT or AUTO
*/
@Schema(description = "BGM Token 类型")
private BgmTokenTypeEnum bgmTokenType;
/**
* bgmToken
*/
@Schema(description = "BGM Token")
private String bgmToken;
/**
* bgmAppID
*/
@Schema(description = "BGM App ID")
private String bgmAppID;
/**
* bgmAppID
*/
@Schema(description = "BGM App Secret")
private String bgmAppSecret;
/**
* bgmRefreshToken
*/
@Schema(description = "BGM Refresh Token")
private String bgmRefreshToken;
/**
* bgmRedirectUri
*/
@Schema(description = "BGM Redirect URI")
private String bgmRedirectUri;
/**
* api key
*/
@Schema(description = "API Key")
private String apiKey;
/**
* 按星期展示
*/
@Schema(description = "按星期展示")
private Boolean weekShow;
/**
* 只下载最新集
*/
@Schema(description = "只下载最新集")
private Boolean downloadNew;
/**
* 仅允许内网ip访问
*/
@Schema(description = "仅允许内网 IP 访问")
private Boolean innerIP;
/**
* 重命名模版
*/
@Schema(description = "重命名模版")
private String renameTemplate;
/**
* 重命名时剔除 年份 (2024)
*/
@Schema(description = "重命名剔除年份")
private Boolean renameDelYear;
/**
* 重命名时剔除 tmdbId [tmdbid=242143]
*/
@Schema(description = "重命名剔除 TMDB ID")
private Boolean renameDelTmdbId;
/**
* 校验登录IP
*/
@Schema(description = "校验登录 IP")
private Boolean verifyLoginIp;
/**
* 自动更新 trackers
*/
@Schema(description = "自动更新 trackers")
private Boolean autoTrackersUpdate;
/**
* Trackers更新地址
*/
@Schema(description = "Trackers 更新地址")
private String trackersUpdateUrls;
/**
* 消息模版
*/
@Schema(description = "消息模版")
private String notificationTemplate;
/**
* 自动更新
*/
@Schema(description = "自动更新")
private Boolean autoUpdate;
/**
* 版本
*/
@Schema(description = "版本")
private String version;
/**
* 获取BGM封面图片质量
*/
@Schema(description = "BGM 封面图片质量")
private String bgmImage;
/**
* 自定义CSS
*/
@Schema(description = "自定义 CSS")
private String customCss;
/**
* 自定义JS
*/
@Schema(description = "自定义 JS")
private String customJs;
/**
* 自定义集数获取规则
*/
@Schema(description = "自定义集数获取规则")
private Boolean customEpisode;
/**
* 自定义集数获取规则
*/
@Schema(description = "自定义集数获取规则表达式")
private String customEpisodeStr;
/**
* 自定义集数获取规则 groupIndex
*/
@Schema(description = "自定义集数获取规则 groupIndex")
private Integer customEpisodeGroupIndex;
/**
* OpenList driver
*/
@Schema(description = "OpenList Driver")
private String provider;
/**
* 添加行订阅是是否开启自动上传
*/
@Schema(description = "新增订阅自动上传")
private Boolean upload;
/**
* 上传速度限制
*/
@Schema(description = "上传速度限制")
private Long upLimit;
/**
* 下载速度限制
*/
@Schema(description = "下载速度限制")
private Long dlLimit;
/**
* 捐赠过期时间
*/
@Schema(description = "捐赠过期时间")
private Long expirationTime;
/**
* 爱发电订单号
*/
@Schema(description = "爱发电订单号")
private String outTradeNo;
/**
* 捐赠或试用是否过期
*/
@Schema(description = "捐赠或试用是否过期")
private Boolean verifyExpirationTime;
/**
* 试用
*/
@Schema(description = "试用")
private Boolean tryOut;
/**
* 摸鱼
*/
@Schema(description = "摸鱼")
private Boolean procrastinating;
/**
* 摸鱼天数
*/
@Schema(description = "摸鱼天数")
private Integer procrastinatingDay;
/**
* github 加速
*/
@Schema(description = "GitHub 加速")
private String github;
/**
* 自定义github加速
*/
@Schema(description = "自定义 GitHub 加速")
private Boolean customGithub;
/**
* 自定义github加速网址
*/
@Schema(description = "自定义 GitHub 加速地址")
private String customGithubUrl;
/**
* github Token
*/
@Schema(description = "GitHub Token")
private String githubToken;
/**
* 开启 OpenList 列表刷新
*/
@Schema(description = "开启 OpenList 列表刷新")
private Boolean alistRefresh;
/**
* OpenList 刷新延迟
*/
@Schema(description = "OpenList 刷新延迟")
private Long alistRefreshDelayed;
/**
* 自动更新总集数信息
*/
@Schema(description = "自动更新总集数信息")
private Boolean updateTotalEpisodeNumber;
/**
* 强制更新总集数信息
*/
@Schema(description = "强制更新总集数信息")
private Boolean forceUpdateTotalEpisodeNumber;
/**
* OpenList 离线超时 分钟
*/
@Schema(description = "OpenList 离线超时(分钟)")
private Integer alistDownloadTimeout;
/**
* OpenList 下载重试次数
*/
@Schema(description = "OpenList 下载重试次数")
private Long alistDownloadRetryNumber;
/**
* 设置备份
*/
@Schema(description = "设置备份")
private Boolean configBackup;
/**
* 备份天数
*/
@Schema(description = "备份天数")
private Integer configBackupDay;
/**
* 展示最后更新时间
*/
@Schema(description = "展示最后更新时间")
private Boolean showLastDownloadTime;
/**
* 番剧完结迁移
*/
@Schema(description = "番剧完结迁移")
private Boolean completed;
/**
* 番剧完结迁移位置
*/
@Schema(description = "番剧完结迁移位置")
private String completedPathTemplate;
/**
* 通知
*/
@Schema(description = "通知配置列表")
private List<NotificationConfig> notificationConfigList;
/**
* 添加订阅时自动复制主rss至备用rss
*/
@Schema(description = "添加订阅时复制主RSS至备用")
private Boolean copyMasterToStandby;
/**
* 排序方式
*/
@Schema(description = "排序方式")
private SortTypeEnum sortType;
/**
* 代理列表
*/
@Schema(description = "代理列表")
private String proxyList;
/**
* 刮削开关
*/
@Schema(description = "刮削开关")
private Boolean scrape;
/**
* 重名的订阅将允许被替换
*/
@Schema(description = "重名订阅允许替换")
private Boolean replace;
/**
* 最大文件名长度 不包含后缀 : .mkv .mp4
*/
@Schema(description = "最大文件名长度(不含后缀)")
private Integer maxFileNameLength;
/**
* 限制尝试次数
*/
@Schema(description = "限制尝试次数")
private Boolean limitLoginAttempts;
/**
* 构建信息
*/
@Schema(description = "构建信息")
private String buildInfo;
}

View File

@@ -2,8 +2,10 @@ package ani.rss.entity;
import ani.rss.util.other.ConfigUtil;
import cn.hutool.core.util.StrUtil;
import io.swagger.v3.oas.annotations.media.Schema;
import wushuo.tmdb.api.entity.TmdbConfig;
@Schema(description = "自定义 TMDB 配置")
public class CustomTmdbConfig extends TmdbConfig {
public final static Config CONFIG = ConfigUtil.CONFIG;

View File

@@ -1,5 +1,6 @@
package ani.rss.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -10,7 +11,10 @@ import java.io.Serializable;
*/
@Data
@Accessors(chain = true)
@Schema(description = "Emby 媒体库")
public class EmbyViews implements Serializable {
@Schema(description = "id")
private String id;
@Schema(description = "名称")
private String name;
}

View File

@@ -1,6 +1,7 @@
package ani.rss.entity;
import com.google.gson.annotations.SerializedName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -11,33 +12,43 @@ import java.io.Serializable;
*/
@Data
@Accessors(chain = true)
@Schema(description = "EmbyWebHook")
public class EmbyWebHook implements Serializable {
@SerializedName(value = "title", alternate = "Title")
@Schema(description = "标题")
private String title;
@SerializedName(value = "description", alternate = "Description")
@Schema(description = "描述")
private String description;
@SerializedName(value = "date", alternate = "Date")
@Schema(description = "日期")
private String date;
@SerializedName(value = "event", alternate = "Event")
@Schema(description = "事件")
private String event;
@SerializedName(value = "severity", alternate = "Severity")
@Schema(description = "严重级别")
private String severity;
@SerializedName(value = "user", alternate = "User")
@Schema(description = "用户信息")
private User user;
@SerializedName(value = "server", alternate = "Server")
@Schema(description = "服务器信息")
private Server server;
@SerializedName(value = "item", alternate = "Item")
@Schema(description = "项目信息")
private Item item;
@SerializedName(value = "playbackInfo", alternate = "PlaybackInfo")
@Schema(description = "播放信息")
private PlaybackInfo playbackInfo;
/**
@@ -45,23 +56,27 @@ public class EmbyWebHook implements Serializable {
*/
@Data
@Accessors(chain = true)
@Schema(description = "项目信息")
public static class Item implements Serializable {
/**
* 文件路径
*/
@SerializedName(value = "path", alternate = "Path")
@Schema(description = "文件路径")
private String path;
/**
* 剧集名
*/
@SerializedName(value = "seriesName", alternate = "SeriesName")
@Schema(description = "剧集名")
private String seriesName;
/**
* 文件名
*/
@SerializedName(value = "fileName", alternate = "FileName")
@Schema(description = "文件名")
private String fileName;
}
@@ -70,17 +85,20 @@ public class EmbyWebHook implements Serializable {
*/
@Data
@Accessors(chain = true)
@Schema(description = "用户信息")
public static class User implements Serializable {
/**
* 用户 Id
*/
@SerializedName(value = "id", alternate = "Id")
@Schema(description = "用户 Id")
private String id;
/**
* 用户名称
*/
@SerializedName(value = "name", alternate = "Name")
@Schema(description = "用户名称")
private String name;
}
@@ -89,23 +107,27 @@ public class EmbyWebHook implements Serializable {
*/
@Data
@Accessors(chain = true)
@Schema(description = "服务器信息")
public static class Server implements Serializable {
/**
* 服务器 Id
*/
@SerializedName(value = "id", alternate = "Id")
@Schema(description = "服务器 Id")
private String id;
/**
* 服务器名称
*/
@SerializedName(value = "name", alternate = "Name")
@Schema(description = "服务器名称")
private String name;
/**
* 服务器版本号
*/
@SerializedName(value = "version", alternate = "Version")
@Schema(description = "服务器版本号")
private String version;
}
@@ -114,11 +136,13 @@ public class EmbyWebHook implements Serializable {
*/
@Data
@Accessors(chain = true)
@Schema(description = "播放信息")
public static class PlaybackInfo implements Serializable {
/**
* 是否播放完成
*/
@SerializedName(value = "playedToCompletion", alternate = "PlayedToCompletion")
@Schema(description = "是否播放完成")
private Boolean playedToCompletion;
}

View File

@@ -0,0 +1,21 @@
package ani.rss.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Data
@Accessors(chain = true)
@Schema(description = "全局变量")
public class Global implements Serializable {
public static List<String> ARGS = new ArrayList<>();
public static final ThreadLocal<HttpServletRequest> REQUEST = new ThreadLocal<>();
public static final ThreadLocal<HttpServletResponse> RESPONSE = new ThreadLocal<>();
}

View File

@@ -1,5 +1,6 @@
package ani.rss.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -11,59 +12,71 @@ import java.util.Date;
*/
@Data
@Accessors(chain = true)
@Schema(description = "下载项")
public class Item implements Serializable {
/**
* 标题
*/
@Schema(description = "标题")
private String title;
/**
* 重命名
*/
@Schema(description = "重命名")
private String reName;
/**
* 种子
*/
@Schema(description = "种子")
private String torrent;
/**
* infoHash
*/
@Schema(description = "infoHash")
private String infoHash;
/**
* 集数
*/
@Schema(description = "集数")
private Double episode;
/**
* 大小
*/
@Schema(description = "大小")
private String size;
/**
* 大小
*/
@Schema(description = "大小")
private Long length;
/**
* 本地已存在
*/
@Schema(description = "本地已存在")
private Boolean local;
/**
* rss
*/
@Schema(description = "主 rss")
private Boolean master;
/**
* 字幕组
*/
@Schema(description = "字幕组")
private String subgroup;
/**
* 发布时间
*/
@Schema(description = "发布时间")
private Date pubDate;
}

View File

@@ -1,5 +1,6 @@
package ani.rss.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -10,25 +11,30 @@ import java.io.Serializable;
*/
@Data
@Accessors(chain = true)
@Schema(description = "日志")
public class Log implements Serializable {
/**
* 日志信息
*/
@Schema(description = "日志信息")
private String message;
/**
* 日志级别
*/
@Schema(description = "日志级别")
private String level;
/**
* 类路径
*/
@Schema(description = "类路径")
private String loggerName;
/**
* 线程名
*/
@Schema(description = "线程名")
private String threadName;
}

View File

@@ -1,5 +1,6 @@
package ani.rss.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -10,21 +11,26 @@ import java.io.Serializable;
*/
@Data
@Accessors(chain = true)
@Schema(description = "登录")
public class Login implements Serializable {
/**
* 用户名
*/
@Schema(description = "用户名")
private String username;
/**
* 密码
*/
@Schema(description = "密码")
private String password;
/**
* ip
*/
@Schema(description = "ip")
private String ip;
/**
* key
*/
@Schema(description = "key")
private String key;
}

Some files were not shown because too many files have changed in this diff Show More