feat: 将OpenList上传移至通知, 现在支持洗版上传了

This commit is contained in:
wushuo
2026-02-08 13:55:26 +08:00
parent ec6aefa4ee
commit 74331f0f6b
22 changed files with 419 additions and 449 deletions

View File

@@ -3,10 +3,7 @@ package ani.rss.download;
import ani.rss.commons.ExceptionUtils;
import ani.rss.commons.FileUtils;
import ani.rss.commons.GsonStatic;
import ani.rss.entity.Ani;
import ani.rss.entity.Config;
import ani.rss.entity.Item;
import ani.rss.entity.TorrentsInfo;
import ani.rss.entity.*;
import ani.rss.enums.NotificationStatusEnum;
import ani.rss.enums.StringEnum;
import ani.rss.util.basic.HttpReq;
@@ -28,12 +25,9 @@ import cn.hutool.http.HttpResponse;
import cn.hutool.http.Method;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Stream;
@@ -384,7 +378,7 @@ public class OpenList implements BaseDownload {
List<OpenListFileInfo> openListFileInfos = ls(path);
List<OpenListFileInfo> list = openListFileInfos.stream()
.flatMap(openListFileInfo -> {
if (openListFileInfo.getIs_dir()) {
if (openListFileInfo.getIsDir()) {
return findFiles(path + "/" + openListFileInfo.getName()).stream();
}
return Stream.of(openListFileInfo);
@@ -396,17 +390,6 @@ public class OpenList implements BaseDownload {
}));
}
@Data
@Accessors(chain = true)
public static class OpenListFileInfo implements Serializable {
private String name;
private Long size;
private Boolean is_dir;
private Date modified;
private Date created;
private String path;
}
/**
* post api
*

View File

@@ -164,11 +164,6 @@ public class Config implements Serializable {
*/
private Boolean deleteStandbyRSSOnly;
/**
* 删除本地文件
*/
private Boolean deleteFiles;
/**
* 自动推断剧集偏移
*/
@@ -416,41 +411,6 @@ public class Config implements Serializable {
*/
private Boolean autoUpdate;
/**
* OpenList
*/
private String alistHost;
/**
* OpenList 令牌
*/
private String alistToken;
/**
* 上传位置
*/
private String alistPath;
/**
* 剧场版上传位置
*/
private String alistOvaPath;
/**
* 启用 OpenList
*/
private Boolean alist;
/**
* OpenList 上传失败时的重试次数
*/
private Integer alistRetry;
/**
* OpenList 添加上传任务
*/
private Boolean alistTask;
/**
* 版本
*/

View File

@@ -0,0 +1,283 @@
package ani.rss.notification;
import ani.rss.commons.FileUtils;
import ani.rss.commons.GsonStatic;
import ani.rss.entity.Ani;
import ani.rss.entity.NotificationConfig;
import ani.rss.entity.OpenListFileInfo;
import ani.rss.enums.NotificationStatusEnum;
import ani.rss.enums.StringEnum;
import ani.rss.service.DownloadService;
import ani.rss.util.basic.HttpReq;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpConfig;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
public class OpenListUploadNotification implements BaseNotification {
/**
* 上传配置
*/
private final HttpConfig httpConfig = new HttpConfig()
.setBlockSize(1024 * 1024 * 50);
private NotificationConfig notificationConfig;
@Override
public Boolean send(NotificationConfig notificationConfig, Ani ani, String text, NotificationStatusEnum notificationStatusEnum) {
Assert.isTrue(NotificationStatusEnum.DOWNLOAD_END == notificationStatusEnum, "OpenListUpload 仅支持下载完成通知");
ani = ObjectUtil.clone(ani);
this.notificationConfig = notificationConfig;
String openListUploadPath = notificationConfig.getOpenListUploadPath();
String openListUploadOvaPath = notificationConfig.getOpenListUploadOvaPath();
Boolean deleteOldEpisode = notificationConfig.getOpenListUploadDeleteOldEpisode();
// 本地位置
String localPath = DownloadService.getDownloadPath(ani);
// 新的位置; 设置自定义下载位置同时启用, 用以获取新的位置
Boolean ova = ani.getOva();
if (ova) {
ani.setDownloadPath(openListUploadOvaPath);
} else {
ani.setDownloadPath(openListUploadPath);
}
ani.setCustomDownloadPath(true);
String target = DownloadService.getDownloadPath(ani);
target = ReUtil.replaceAll(target, "^[A-z]:", "");
if (ova) {
uploadOva(localPath, target);
return true;
}
if (deleteOldEpisode) {
deleteOldEpisode(localPath, target);
}
upload(localPath, target);
return true;
}
public void uploadOva(String localFilePath, String cloudFilePath) {
List<File> files = FileUtils.listFileList(localFilePath);
for (File file : files) {
if (file.isDirectory()) {
// 文件夹 跳过
continue;
}
String name = file.getName();
if (!FileUtils.isSubtitleFormat(name) && !FileUtils.isVideoFormat(name)) {
// 非视频与字幕 跳过
continue;
}
// 开始上传
uploadFile(file.getAbsolutePath(), cloudFilePath);
}
}
public void deleteOldEpisode(String localFilePath, String cloudFilePath) {
Set<String> episodeSet = FileUtils.listFileList(localFilePath)
.stream()
.filter(FileUtil::isFile)
.map(File::getName)
.filter(name -> FileUtils.isVideoFormat(name) || FileUtils.isSubtitleFormat(name))
.filter(name -> ReUtil.contains(StringEnum.SEASON_REG, name))
.map(name -> ReUtil.get(StringEnum.SEASON_REG, name, 0))
.map(String::toUpperCase)
.collect(Collectors.toSet());
List<OpenListFileInfo> fileInfos = fileList(cloudFilePath);
for (OpenListFileInfo fileInfo : fileInfos) {
String name = fileInfo.getName();
if (!FileUtils.isVideoFormat(name) && !FileUtils.isSubtitleFormat(name)) {
// 非视频与字幕文件
continue;
}
if (!ReUtil.contains(StringEnum.SEASON_REG, name)) {
// 确保命名
continue;
}
String episode = ReUtil.get(StringEnum.SEASON_REG, name, 0);
episode = episode.toUpperCase();
if (!episodeSet.contains(episode)) {
// 新位置没有旧的同集文件, 不需要删除
continue;
}
log.info("因洗版需要删除: {}", name);
// 删除 洗版
postApi("fs/remove")
.body(GsonStatic.toJson(Map.of(
"dir", cloudFilePath,
"names", List.of(name)
))).then(HttpResponse::isOk);
}
}
private void upload(String localFilePath, String cloudFilePath) {
Boolean openListUploadDeleteLocalFile = notificationConfig.getOpenListUploadDeleteLocalFile();
for (File file : FileUtils.listFileList(localFilePath)) {
if (file.isDirectory()) {
// 过滤掉文件夹
continue;
}
String name = file.getName();
if (!FileUtils.isVideoFormat(name) && !FileUtils.isSubtitleFormat(name)) {
// 非视频与字幕文件
continue;
}
if (!ReUtil.contains(StringEnum.SEASON_REG, name)) {
// 确保命名
continue;
}
log.info("文件上传: {} => {}", file, cloudFilePath);
uploadFile(file.getAbsolutePath(), cloudFilePath);
if (openListUploadDeleteLocalFile) {
log.info("删除本地文件 {}", file);
FileUtil.del(file);
}
}
}
/**
* 上传文件
*
* @param localFilePath 本地文件位置
* @param cloudFilePath 云端文件位置
*/
private void uploadFile(String localFilePath, String cloudFilePath) {
Assert.isTrue(FileUtil.exist(localFilePath), "文件不存在 {}", localFilePath);
if (FileUtil.isDirectory(localFilePath)) {
List<File> files = FileUtils.listFileList(localFilePath);
for (File file : files) {
uploadFile(file.getAbsolutePath(), cloudFilePath);
}
return;
}
String openListUploadHost = notificationConfig.getOpenListUploadHost();
String openListUploadApiKey = notificationConfig.getOpenListUploadApiKey();
Boolean openListUploadTask = notificationConfig.getOpenListUploadTask();
String url = StrUtil.format("{}/api/fs/put", openListUploadHost);
String filename = FileUtil.getName(localFilePath);
HttpReq
.put(url)
.timeout(1000 * 60 * 2)
.setConfig(httpConfig)
.header(Header.AUTHORIZATION, openListUploadApiKey)
.header("As-Task", Boolean.toString(openListUploadTask))
.header("File-Path", URLUtil.encode(cloudFilePath))
.contentType("application/octet-stream")
.body(ResourceUtil.getResourceObj(localFilePath))
.then(res -> {
Assert.isTrue(res.isOk(), "上传失败 {} 状态码:{}", localFilePath, res.getStatus());
JsonObject jsonObject = GsonStatic.fromJson(res.body(), JsonObject.class);
int code = jsonObject.get("code").getAsInt();
log.info(jsonObject.toString());
Assert.isTrue(code == 200, "上传失败 {} 状态码:{}", localFilePath, code);
String text = StrFormatter.format("OpenList 上传完成 {}", filename);
if (openListUploadTask) {
text = StrFormatter.format("已向 OpenList 添加上传任务 {}", filename);
}
log.info(text);
});
}
/**
* 文件列表
*
* @param path 目录
* @return 文件列表
*/
public List<OpenListFileInfo> fileList(String path) {
try {
return postApi("fs/list")
.body(GsonStatic.toJson(Map.of(
"path", path,
"page", 1,
"per_page", 256,
"refresh", true
)))
.thenFunction(res -> {
JsonObject jsonObject = GsonStatic.fromJson(res.body(), JsonObject.class);
int code = jsonObject.get("code").getAsInt();
if (code != 200) {
return List.of();
}
JsonElement data = jsonObject.get("data");
if (Objects.isNull(data) || data.isJsonNull()) {
return List.of();
}
JsonElement content = data.getAsJsonObject()
.get("content");
if (Objects.isNull(content) || content.isJsonNull()) {
return List.of();
}
List<OpenListFileInfo> infos = GsonStatic.fromJsonList(content.getAsJsonArray(), OpenListFileInfo.class);
for (OpenListFileInfo info : infos) {
info.setPath(path);
}
return ListUtil.sort(new ArrayList<>(infos), Comparator.comparing(fileInfo -> {
Long size = fileInfo.getSize();
return Long.MAX_VALUE - ObjectUtil.defaultIfNull(size, 0L);
}));
});
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return List.of();
}
/**
* post api
*
* @param action 操作
* @return HttpReq
*/
public synchronized HttpRequest postApi(String action) {
ThreadUtil.sleep(2000);
String openListUploadHost = notificationConfig.getOpenListUploadHost();
String openListUploadApiKey = notificationConfig.getOpenListUploadApiKey();
return HttpReq.post(openListUploadHost + "/api/" + action)
.header(Header.AUTHORIZATION, openListUploadApiKey);
}
}

View File

@@ -480,17 +480,6 @@ public class DownloadService {
// 刮削
ScrapeService.scrape(ani, false);
}
try {
OpenListUtil.upload(torrentsInfo, ani);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
try {
OpenListUtil.refresh(ani);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
String text = StrFormatter.format("{} 下载完成", name);
if (tags.contains(TorrentsTags.BACK_RSS.getValue())) {
text = StrFormatter.format("(备用RSS) {}", text);

View File

@@ -820,19 +820,16 @@ public class BgmUtil {
// 下载位置
String downloadPath = DownloadService.getDownloadPath(ani);
String alistPath = config.getAlistPath();
String completedPathTemplate = config.getCompletedPathTemplate();
if (ova) {
// 剧场版默认不开启摸鱼检测
ani.setProcrastinating(false);
alistPath = config.getAlistOvaPath();
}
return ani
// tmdb 标题
.setThemoviedbName(themoviedbName)
.setAlistPath(alistPath)
.setDownloadPath(downloadPath)
.setCustomCompletedPathTemplate(completedPathTemplate);
}

View File

@@ -122,7 +122,6 @@ public class ConfigUtil {
.setAwaitStalledUP(true)
.setDelete(false)
.setDeleteStandbyRSSOnly(false)
.setDeleteFiles(false)
.setOffset(false)
.setTitleYear(false)
.setAutoDisabled(false)
@@ -187,13 +186,6 @@ public class ConfigUtil {
.setAutoTrackersUpdate(false)
.setTrackersUpdateUrls("https://cf.trackerslist.com/best.txt")
.setAutoUpdate(false)
.setAlist(false)
.setAlistRetry(5)
.setAlistTask(true)
.setAlistPath("/115/Media/番剧/${title}/Season ${season}")
.setAlistOvaPath("/115/Media/剧场版/${title}")
.setAlistHost("")
.setAlistToken("")
.setVersion("")
.setBgmImage("large")
.setCustomCss("")
@@ -451,7 +443,6 @@ public class ConfigUtil {
public static void formatUrl(Config config) {
List<Func1<Config, String>> func1List = List.of(
Config::getDownloadToolHost,
Config::getAlistHost,
Config::getMikanHost,
Config::getTmdbApi,
Config::getCustomGithubUrl
@@ -476,9 +467,7 @@ public class ConfigUtil {
List<Func1<Config, String>> func1List = List.of(
Config::getDownloadPathTemplate,
Config::getOvaDownloadPathTemplate,
Config::getCompletedPathTemplate,
Config::getAlistPath,
Config::getAlistOvaPath
Config::getCompletedPathTemplate
);
DynaBean dynaBean = DynaBean.create(config);

View File

@@ -35,7 +35,8 @@ public class NotificationUtil {
NotificationTypeEnum.TELEGRAM, TelegramNotification.class,
NotificationTypeEnum.WEB_HOOK, WebHookNotification.class,
NotificationTypeEnum.SHELL, ShellNotification.class,
NotificationTypeEnum.FILE_MOVE, FileMoveNotification.class
NotificationTypeEnum.FILE_MOVE, FileMoveNotification.class,
NotificationTypeEnum.OPEN_LIST_UPLOAD, OpenListUploadNotification.class
);
/**

View File

@@ -1,237 +0,0 @@
package ani.rss.util.other;
import ani.rss.commons.FileUtils;
import ani.rss.commons.GsonStatic;
import ani.rss.entity.Ani;
import ani.rss.entity.Config;
import ani.rss.entity.TorrentsInfo;
import ani.rss.enums.NotificationStatusEnum;
import ani.rss.enums.TorrentsTags;
import ani.rss.service.DownloadService;
import ani.rss.util.basic.HttpReq;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.thread.ExecutorBuilder;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpConfig;
import com.google.gson.JsonObject;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* OpenList
*/
@Slf4j
public class OpenListUtil {
private static final ExecutorService EXECUTOR = ExecutorBuilder.create()
.setCorePoolSize(1)
.setMaxPoolSize(1)
.setWorkQueue(new LinkedBlockingQueue<>(256))
.build();
/**
* 将下载完成的任务上传至 OpenList
*
* @param torrentsInfo 任务
*/
public static void upload(TorrentsInfo torrentsInfo, Ani ani) {
Boolean upload = ani.getUpload();
// 禁止自动上传
if (!upload) {
return;
}
Config config = ConfigUtil.CONFIG;
Boolean alist = config.getAlist();
if (!alist) {
return;
}
String alistHost = config.getAlistHost();
String alistToken = config.getAlistToken();
Integer alistRetry = config.getAlistRetry();
verify();
List<String> tags = torrentsInfo.getTags();
if (tags.contains(TorrentsTags.OPEN_LIST.getValue())) {
return;
}
if (tags.contains(TorrentsTags.UPLOAD_COMPLETED.getValue())) {
return;
}
TorrentUtil.addTags(torrentsInfo, TorrentsTags.OPEN_LIST.getValue());
String downloadDir = FileUtils.getAbsolutePath(torrentsInfo.getDownloadDir());
List<String> files = torrentsInfo.getFiles().get();
String filePath = getPath(ani);
for (String fileName : files) {
String finalFilePath = filePath + "/" + fileName;
String file = downloadDir + "/" + fileName;
if (!FileUtil.exist(file)) {
log.error("文件不存在 {}", file);
return;
}
EXECUTOR.execute(() -> {
log.info("上传 {} ==> {}", file, finalFilePath);
Boolean alistTask = config.getAlistTask();
for (int i = 0; i < alistRetry; i++) {
try {
String url = alistHost;
// 使用流式上传
url += "/api/fs/put";
// 50M 上传
HttpConfig httpConfig = new HttpConfig()
.setBlockSize(1024 * 1024 * 50);
HttpReq
.put(url)
.timeout(1000 * 60 * 2)
.setConfig(httpConfig)
.header(Header.AUTHORIZATION, alistToken)
.header("As-Task", Boolean.toString(alistTask))
.header("File-Path", URLUtil.encode(finalFilePath))
.contentType("application/octet-stream")
.body(ResourceUtil.getResourceObj(file))
.then(res -> {
Assert.isTrue(res.isOk(), "上传失败 {} 状态码:{}", fileName, res.getStatus());
JsonObject jsonObject = GsonStatic.fromJson(res.body(), JsonObject.class);
int code = jsonObject.get("code").getAsInt();
log.info(jsonObject.toString());
Assert.isTrue(code == 200, "上传失败 {} 状态码:{}", fileName, code);
String text = StrFormatter.format("OpenList 上传完成 {}", fileName);
if (alistTask) {
text = StrFormatter.format("已向 OpenList 添加上传任务 {}", fileName);
}
log.info(text);
NotificationUtil.send(config, ani, text, NotificationStatusEnum.OPEN_LIST_UPLOAD);
});
TorrentUtil.addTags(torrentsInfo, TorrentsTags.UPLOAD_COMPLETED.getValue());
return;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
NotificationUtil.send(config, ani, "OpenList 上传失败 " + fileName, NotificationStatusEnum.ERROR);
});
}
}
/**
* 刷新 OpenList 路径
*/
public static void refresh(Ani ani) {
Config config = ConfigUtil.CONFIG;
Boolean refresh = config.getAlistRefresh();
if (!refresh) {
return;
}
String alistHost = config.getAlistHost();
String alistToken = config.getAlistToken();
Integer alistRetry = config.getAlistRetry();
verify();
String finalPath = getPath(ani);
EXECUTOR.execute(() -> {
Long getAlistRefreshDelay = config.getAlistRefreshDelayed();
if (getAlistRefreshDelay > 0) {
ThreadUtil.sleep(getAlistRefreshDelay, TimeUnit.SECONDS);
}
log.info("刷新 OpenList 路径: {}", finalPath);
for (int i = 0; i < alistRetry; i++) {
try {
HttpReq
.post(alistHost + "/api/fs/mkdir")
.header(Header.AUTHORIZATION, alistToken)
.body(GsonStatic.toJson(Map.of("path", finalPath)))
.then(HttpReq::assertStatus);
String url = alistHost;
url += "/api/fs/list";
Map<String, Object> resolved = Map.of(
"path", finalPath,
"refresh", true
);
HttpReq.post(url)
.timeout(1000 * 20)
.header(Header.AUTHORIZATION, alistToken)
.body(GsonStatic.toJson(resolved))
.then(res -> {
Assert.isTrue(res.isOk(), "刷新失败 路径: {} 状态码: {}", finalPath, res.getStatus());
JsonObject jsonObject = GsonStatic.fromJson(res.body(), JsonObject.class);
int code = jsonObject.get("code").getAsInt();
Assert.isTrue(code == 200, "刷新失败 路径: {} 状态码: {}", finalPath, code);
log.info("已成功刷新 OpenList 路径: {}", finalPath);
});
break;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
ThreadUtil.sleep(3000);
});
}
/**
* 校验配置
*/
public static void verify() {
Config config = ConfigUtil.CONFIG;
String alistHost = config.getAlistHost();
String alistPath = FileUtils.getAbsolutePath(config.getAlistPath());
String alistToken = config.getAlistToken();
Assert.notBlank(alistHost, "alistHost 未配置");
Assert.notBlank(alistPath, "alistPath 未配置");
Assert.notBlank(alistToken, "alistToken 未配置");
}
/**
* 获取上传位置
*
* @param ani
* @return
*/
private static String getPath(Ani ani) {
ani = ObjectUtil.clone(ani)
// 因为临时修改下载位置模版以获取对应下载位置, 要关闭自定义下载位置
.setCustomDownloadPath(false);
Config config = ObjectUtil.clone(ConfigUtil.CONFIG);
config.setDownloadPathTemplate(config.getAlistPath())
.setOvaDownloadPathTemplate(config.getAlistOvaPath());
Boolean customAlistPath = ani.getCustomAlistPath();
String alistPath = ani.getAlistPath();
if (customAlistPath) {
config.setDownloadPathTemplate(alistPath)
.setOvaDownloadPathTemplate(alistPath);
}
String path = DownloadService.getDownloadPath(ani, config);
path = ReUtil.replaceAll(path, "^[A-z]:", "");
return path;
}
}

View File

@@ -240,17 +240,7 @@ public class TorrentUtil {
* @param torrentsInfo
*/
public static synchronized Boolean delete(TorrentsInfo torrentsInfo) {
Config config = ConfigUtil.CONFIG;
Boolean deleteFiles = config.getDeleteFiles();
Boolean alist = config.getAlist();
if (!deleteFiles || !alist) {
return delete(torrentsInfo, false, false);
}
// 开启 OpenList 上传 后删除源文件的行为需要等待 OpenList 上传完成
if (torrentsInfo.getTags().contains(TorrentsTags.UPLOAD_COMPLETED.getValue())) {
return delete(torrentsInfo, false, true);
}
return false;
return delete(torrentsInfo, false, false);
}
/**

View File

@@ -183,6 +183,41 @@ public class NotificationConfig implements Serializable {
*/
private Boolean fileMoveDeleteOldEpisode;
/**
* OpenList Host
*/
private String openListUploadHost;
/**
* OpenList ApiKey
*/
private String openListUploadApiKey;
/**
* OpenList 上传位置
*/
private String openListUploadPath;
/**
* OpenList OVA/剧场版 上传位置
*/
private String openListUploadOvaPath;
/**
* 添加为上传任务
*/
private Boolean openListUploadTask;
/**
* 上传完成后删除本地文件
*/
private Boolean openListUploadDeleteLocalFile;
/**
* 删除同及文件
*/
private Boolean openListUploadDeleteOldEpisode;
/**
* 通知 状态
*/
@@ -250,14 +285,26 @@ public class NotificationConfig implements Serializable {
.setEmbyRefreshViewIds(new ArrayList<>())
.setEmbyDelayed(0L);
// SHELL
notificationConfig.setShell("")
.setAliveLimit(10);
// FileMove
notificationConfig
.setFileMoveTarget("/CD2/115/Media/番剧/${title}/Season ${season}")
.setFileMoveOvaTarget("/CD2/115/Media/剧场版/${title}")
.setFileMoveDeleteOldEpisode(false);
// OpenList
notificationConfig
.setOpenListUploadHost("http://127.0.0.1:5244")
.setOpenListUploadApiKey("")
.setOpenListUploadPath("/115/Media/番剧/${title}/Season ${season}")
.setOpenListUploadOvaPath("/115/Media/剧场版/${title}")
.setOpenListUploadTask(false)
.setOpenListUploadDeleteLocalFile(true)
.setOpenListUploadDeleteOldEpisode(false);
return notificationConfig;
}

View File

@@ -0,0 +1,21 @@
package ani.rss.entity;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@Accessors(chain = true)
public class OpenListFileInfo implements Serializable {
private String name;
private Long size;
@SerializedName(value = "isDir",alternate = "is_dir")
private Boolean isDir;
private Date modified;
private Date created;
private String path;
}

View File

@@ -20,10 +20,6 @@ public enum NotificationStatusEnum {
* 错误
*/
ERROR("", "发生错误"),
/**
* OpenList 上传通知
*/
OPEN_LIST_UPLOAD("🙌", "OpenList 上传通知"),
/**
* 订阅完结
*/

View File

@@ -11,5 +11,6 @@ public enum NotificationTypeEnum {
TELEGRAM,
WEB_HOOK,
SHELL,
FILE_MOVE
FILE_MOVE,
OPEN_LIST_UPLOAD
}

View File

@@ -9,9 +9,7 @@ public enum TorrentsTags {
ANI_RSS("ani-rss"),
RENAME("RENAME"),
BACK_RSS("备用RSS"),
DOWNLOAD_COMPLETE("下载完成"),
OPEN_LIST("OpenList"),
UPLOAD_COMPLETED("上传完成");
DOWNLOAD_COMPLETE("下载完成");
private final String value;
}

View File

@@ -143,15 +143,6 @@
<el-text class="mx-1" size="small">
<strong>主RSS</strong> <span class="download-danger-text">不会自动删除</span>仅在其更新后删除对应备用RSS的任务与文件
</el-text>
<br>
<el-checkbox v-model:model-value="props.config.deleteFiles"
:disabled="!props.config.delete">
<span class="download-danger-text">删除本地文件</span>
</el-checkbox>
<br>
<el-text class="mx-1" size="small">
删除本地文件, 仅在同时开启了 <strong>OpenList 上传</strong> 并上传成功后删除
</el-text>
</div>
</el-form-item>
<el-form-item label="失败重试次数">
@@ -199,9 +190,6 @@
<el-collapse-item name="qBittorrent" title="qBittorrent 设置">
<QBittorrent v-if="activeName.indexOf('qBittorrent') > -1" :config="props.config"/>
</el-collapse-item>
<el-collapse-item name="OpenList" title="OpenList 设置">
<OpenList v-if="activeName.indexOf('OpenList') > -1" :config="props.config"/>
</el-collapse-item>
</el-collapse>
</el-form>
</template>
@@ -212,7 +200,6 @@ import api from "@/js/api.js";
import {ElMessage, ElText} from "element-plus";
import {Key, User} from "@element-plus/icons-vue";
import QBittorrent from "@/config/download/qBittorrent.vue";
import OpenList from "@/config/download/OpenList.vue";
import PrioKeys from "@/config/PrioKeys.vue";
import CustomTags from "@/config/CustomTags.vue";

View File

@@ -128,7 +128,7 @@ let props = defineProps(['config'])
<style>
/* 通知组件通用样式 - 非scoped以便子组件使用 */
.notification-input-width {
width: 160px;
width: 160px !important;
}
.notification-flex-between {

View File

@@ -21,7 +21,6 @@
<el-checkbox label="下载完成" value="DOWNLOAD_END"/>
<el-checkbox label="缺集" value="OMIT"/>
<el-checkbox label="错误" value="ERROR"/>
<el-checkbox label="OpenList 上传通知" value="OPEN_LIST_UPLOAD"/>
<el-checkbox label="订阅完结" value="COMPLETED"/>
<el-checkbox label="摸鱼检测" value="PROCRASTINATING"/>
</el-checkbox-group>
@@ -34,9 +33,13 @@
<SystemNotification v-model:notification-config="notificationConfig" v-model:config="props.config"/>
<ShellNotification v-model:notification-config="notificationConfig" v-model:config="props.config"/>
<FileMoveNotification v-model:notification-config="notificationConfig" v-model:config="props.config"/>
<OpenListNotification v-model:config="props.config" v-model:notification-config="notificationConfig"/>
<el-form-item label="顺序">
<div>
<el-input-number v-model="notificationConfig['sort']"/>
<el-input-number
v-model="notificationConfig['sort']"
class="notification-input-width"
/>
</div>
</el-form-item>
<el-form-item label="重试">
@@ -44,12 +47,8 @@
:min="0"
:max="100"
v-model="notificationConfig['retry']"
class="notification-config-retry-input"
>
<template #suffix>
<span></span>
</template>
</el-input-number>
class="notification-input-width"
/>
</el-form-item>
<el-form-item label="开启">
<el-switch v-model="notificationConfig['enable']"/>
@@ -78,6 +77,7 @@ import {notificationTypeList} from "@/js/notification-type.js";
import {ElMessage} from "element-plus";
import api from "@/js/api.js";
import FileMoveNotification from "@/config/notification/FileMoveNotification.vue";
import OpenListNotification from "@/config/notification/OpenListNotification.vue";
let notificationConfig = ref({
@@ -154,10 +154,6 @@ defineExpose({
padding: 15px;
}
.notification-config-retry-input {
width: 160px;
}
.notification-config-footer {
justify-content: space-between;
width: 100%;

View File

@@ -1,61 +0,0 @@
<template>
<el-form-item label="Host">
<el-input v-model:model-value="props.config['alistHost']" placeholder="http://127.0.0.1:5244"/>
</el-form-item>
<el-form-item label="Token">
<el-input v-model:model-value="props.config['alistToken']" placeholder="openlist-xxxxxx"/>
</el-form-item>
<el-form-item label="上传位置">
<el-input v-model:model-value="props.config['alistPath']" placeholder="/"/>
</el-form-item>
<el-form-item label="剧场版上传位置">
<el-input v-model:model-value="props.config['alistOvaPath']" placeholder="/"/>
</el-form-item>
<el-form-item label="失败重试次数">
<el-input-number v-model:model-value="props.config['alistRetry']" :max="100" :min="1"/>
</el-form-item>
<el-form-item label="上传开关">
<div class="openlist-container">
<div>
<el-switch v-model="props.config['alist']"/>
</div>
<div>
<el-checkbox v-model="props.config['alistTask']" :disabled="!props.config['alist']" label="添加为任务"/>
</div>
<div>
<el-text class="mx-1" size="small">
自动将下载完成的文件上传至 OpenList
</el-text>
</div>
</div>
</el-form-item>
<el-form-item label="刷新开关">
<div class="openlist-container">
<div>
<el-switch v-model:model-value="props.config['alistRefresh']"/>
</div>
<div>
<el-text class="mx-1" size="small">
刷新 OpenList 上传路径的文件列表
</el-text>
</div>
</div>
</el-form-item>
<el-form-item label="刷新延迟">
<el-input-number v-model="props.config['alistRefreshDelayed']" :min="0">
<template #suffix>
<span></span>
</template>
</el-input-number>
</el-form-item>
</template>
<script setup>
let props = defineProps(['config'])
</script>
<style scoped>
.openlist-container {
width: 100%;
}
</style>

View File

@@ -0,0 +1,36 @@
<template>
<template v-if="notificationConfig['notificationType'] === 'OPEN_LIST_UPLOAD'">
<el-form-item label="Host">
<el-input v-model="notificationConfig['openListUploadHost']" placeholder="http://127.0.0.1:5244"/>
</el-form-item>
<el-form-item label="ApiKey">
<el-input v-model="notificationConfig['openListUploadApiKey']" placeholder="openlist-xxxxxx"/>
</el-form-item>
<el-form-item label="上传位置">
<el-input
v-model="notificationConfig['openListUploadPath']"
placeholder="/115/Media/番剧/${title}/Season ${season}"/>
</el-form-item>
<el-form-item label="上传位置 (剧场版)">
<el-input
v-model="notificationConfig['openListUploadOvaPath']"
placeholder="/115/Media/剧场版/${title}"/>
</el-form-item>
<el-form-item label="其他">
<div>
<el-checkbox v-model="notificationConfig['openListUploadDeleteLocalFile']" label="上传完成后删除本地文件"/>
<el-checkbox v-model="notificationConfig['openListUploadTask']" label="添加为上传任务"/>
<br/>
<el-checkbox v-model="notificationConfig['openListUploadDeleteOldEpisode']" label="删除旧的同集文件"/>
<br/>
<el-text class="mx-1" size="small">
自动时将目标位置存在的同集文件删除, 可达到洗版的效果
</el-text>
</div>
</el-form-item>
</template>
</template>
<script setup>
let props = defineProps(['notificationConfig', 'config'])
</script>

View File

@@ -1,27 +1,29 @@
<template>
<template v-if="notificationConfig['notificationType'] === 'TELEGRAM'">
<el-form-item label="通知模版">
<el-input v-model:model-value="props.notificationConfig['notificationTemplate']" :autosize="{ minRows: 2}"
<el-input v-model="notificationConfig['notificationTemplate']" :autosize="{ minRows: 2}"
placeholder="${notification}" type="textarea"/>
</el-form-item>
<el-form-item label="Api Host">
<el-input v-model:model-value="props.notificationConfig['telegramApiHost']"
<el-input v-model="notificationConfig['telegramApiHost']"
placeholder="https://api.telegram.org"/>
</el-form-item>
<el-form-item label="Token">
<el-input v-model:model-value="props.notificationConfig['telegramBotToken']"
<el-input v-model="notificationConfig['telegramBotToken']"
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"/>
</el-form-item>
<el-form-item label="ChatId">
<div>
<div class="auto-flex notification-flex-between">
<div class="notification-margin-top-right">
<el-input v-model:model-value="props.notificationConfig['telegramChatId']"
<el-input v-model="notificationConfig['telegramChatId']"
placeholder="123456789"/>
</div>
<div class="flex notification-margin-top-center">
<div>
<el-select v-model:model-value="chatId" @change="chatIdChange" class="notification-input-width">
<el-select v-model="chatId"
class="notification-input-width"
@change="chatIdChange">
<el-option v-for="item in Object.keys(chatIdMap)"
:key="item"
:label="item"
@@ -36,15 +38,15 @@
</div>
</el-form-item>
<el-form-item label="话题ID">
<el-input-number v-model="props.notificationConfig['telegramTopicId']"
<el-input-number v-model="notificationConfig['telegramTopicId']"
:min="-1" class="notification-input-width"/>
</el-form-item>
<el-form-item label="图片">
<el-switch v-model:model-value="props.notificationConfig['telegramImage']"/>
<el-switch v-model="notificationConfig['telegramImage']"/>
</el-form-item>
<el-form-item label="格式">
<div class="notification-input-width">
<el-select v-model:model-value="props.notificationConfig['telegramFormat']" placeholder="None">
<el-select v-model="notificationConfig['telegramFormat']" placeholder="None">
<el-option label="None" value=""/>
<el-option label="Markdown" value="Markdown"/>
<el-option label="HTML" value="HTML"/>

View File

@@ -147,18 +147,6 @@
</div>
</div>
</el-form-item>
<el-form-item label="自定义上传">
<div style="width: 100%;">
<div>
<el-switch v-model="props.ani.customAlistPath"/>
</div>
<div>
<el-input type="textarea" style="width: 100%" :disabled="!props.ani.customAlistPath"
:autosize="{ minRows: 2}"
v-model:model-value="props.ani.alistPath"/>
</div>
</div>
</el-form-item>
<el-form-item label="自定义完结迁移">
<div style="width: 100%;">
<div>

View File

@@ -30,6 +30,10 @@ export let notificationTypeList = [
{
name: 'FILE_MOVE',
label: '文件移动'
},
{
name: 'OPEN_LIST_UPLOAD',
label: 'OpenList上传'
}
]