mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-03 14:16:03 +08:00
feat(4.0-P4): 内置插件 rename 提供 pn / advance / template 重命名方式
- pn / advance 从 core 移入内置插件 rename(默认启用),输出与 3.x 逐字节一致; 宿主只保留 none,插件停用时按 none 处理。 - template:Jinja2 沙箱模板(StrictUndefined + pad 过滤器)渲染文件名主体, 字幕自动追加语言与扩展名。保存配置时编译并试渲染示例,不合法返回 422; 运行时渲染失败、结果为空或含路径分隔符时抛出 RenameSkipped,文件保留原名并 通知,绝不退回 pn。 - 设置页重命名方式下拉框并入插件提供的方式;通知中心支持 rename_skipped。 - 测试环境默认登记内置 rename 插件,与生产默认启用一致。
This commit is contained in:
@@ -1,42 +1,14 @@
|
||||
"""重命名策略:由媒体文件信息生成目标文件名。
|
||||
"""宿主自带的重命名方式 ``none``:保留原路径。
|
||||
|
||||
注意:这里的 title/bangumi_name 来自已存在于磁盘上的文件/文件夹名(单个路径
|
||||
分量,不可能含分隔符),不做保留字符清洗——追加清洗会让既有做种库(如含 ":"
|
||||
的标题)在升级后被整库批量重命名 (#721 评审)。
|
||||
|
||||
电影/剧场版:Title (Year).ext,不使用 SxxExx 编号。bangumi_name 由调用方传入,
|
||||
与 gen_save_path 的文件夹命名保持一致 (Title (Year))。
|
||||
``pn`` / ``advance`` / ``template`` 由内置插件 ``rename`` 提供
|
||||
(module/plugins/builtin/rename/)。
|
||||
"""
|
||||
|
||||
from ab_sdk.rename import RenameInput, pad
|
||||
from ab_sdk.rename import RenameInput
|
||||
|
||||
NO_RENAME = "none"
|
||||
|
||||
|
||||
class NoRename:
|
||||
"""``none``:保留原路径。"""
|
||||
|
||||
def target_name(self, f: RenameInput) -> str:
|
||||
return f.media_path
|
||||
|
||||
|
||||
def standard_name(base: str, f: RenameInput) -> str:
|
||||
"""``{base} SxxEyy[.语言].ext``;电影为 ``{base}[.语言].ext``。"""
|
||||
language = f".{f.language}" if f.kind == "subtitle" else ""
|
||||
if f.episode_type == "movie":
|
||||
return f"{base}{language}{f.suffix}"
|
||||
return f"{base} S{pad(f.season)}E{pad(f.episode)}{language}{f.suffix}"
|
||||
|
||||
|
||||
class PnRename:
|
||||
"""``pn``:以文件名解析出的标题命名。"""
|
||||
|
||||
def target_name(self, f: RenameInput) -> str:
|
||||
return standard_name(f.title, f)
|
||||
|
||||
|
||||
class AdvanceRename:
|
||||
"""``advance``:以番剧文件夹名命名。"""
|
||||
|
||||
def target_name(self, f: RenameInput) -> str:
|
||||
return standard_name(f.bangumi_name, f)
|
||||
|
||||
@@ -181,19 +181,13 @@ def _register_core(registry: ExtensionRegistry) -> None:
|
||||
ProviderEntry(CORE, provider_id, metadata(provider_id)),
|
||||
)
|
||||
|
||||
# organize:重命名方式 none、按扩展名分类、版本冲突策略
|
||||
# organize:重命名方式 none(pn / advance / template 由内置插件 rename 提供)、
|
||||
# 按扩展名分类、版本冲突策略
|
||||
from module.downloader.path import SuffixMediaFiles
|
||||
from module.manager.rename_strategies import (
|
||||
NO_RENAME,
|
||||
AdvanceRename,
|
||||
NoRename,
|
||||
PnRename,
|
||||
)
|
||||
from module.manager.rename_strategies import NO_RENAME, NoRename
|
||||
from module.manager.revision_policy import CoreConflictPolicy
|
||||
|
||||
_core(registry, points.RENAME_STRATEGY, NO_RENAME, NoRename())
|
||||
_core(registry, points.RENAME_STRATEGY, "pn", PnRename())
|
||||
_core(registry, points.RENAME_STRATEGY, "advance", AdvanceRename())
|
||||
_core(registry, points.MEDIA_FILES, CORE_ID, SuffixMediaFiles())
|
||||
_core(registry, points.CONFLICT_POLICY, CORE_ID, CoreConflictPolicy())
|
||||
|
||||
|
||||
8
backend/src/module/plugins/builtin/rename/plugin.toml
Normal file
8
backend/src/module/plugins/builtin/rename/plugin.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[plugin]
|
||||
id = "rename"
|
||||
name = "重命名"
|
||||
version = "1.0.0"
|
||||
sdk = ">=0.4,<1"
|
||||
entry = "rename_plugin:RenamePlugin"
|
||||
description = "提供重命名方式 pn、advance 与自定义模板 template。停用后所有番剧按 none 处理(保留原文件名)"
|
||||
authors = ["AutoBangumi"]
|
||||
140
backend/src/module/plugins/builtin/rename/rename_plugin.py
Normal file
140
backend/src/module/plugins/builtin/rename/rename_plugin.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""内置插件:重命名方式 ``pn``、``advance`` 与 ``template``(``rename_strategy``)。
|
||||
|
||||
``pn`` / ``advance`` 的输出与 3.x 逐字节一致。``template`` 用 Jinja2 沙箱模板
|
||||
渲染文件名主体(不含扩展名),字幕自动追加 ``.语言``,最后追加扩展名。
|
||||
|
||||
注意:title / bangumi_name 来自已存在于磁盘上的文件 / 文件夹名(单个路径分量),
|
||||
不做保留字符清洗——追加清洗会让既有做种库(如含 ":" 的标题)在升级后被整库
|
||||
批量重命名 (#721 评审)。电影 / 剧场版为 ``Title (Year).ext``,不使用 SxxExx。
|
||||
"""
|
||||
|
||||
from jinja2 import StrictUndefined, Template, TemplateError
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from ab_sdk import Plugin, PluginContext, points, provider
|
||||
from ab_sdk.rename import RenameInput, RenameSkipped, RenameStrategy, pad
|
||||
|
||||
DEFAULT_TEMPLATE = "{{ title }} S{{ season|pad(2) }}E{{ episode|pad(2) }}"
|
||||
|
||||
# 保存配置时用它试渲染一次,渲染不出合法文件名的模板直接拒绝
|
||||
SAMPLE = RenameInput(
|
||||
kind="media",
|
||||
media_path="[Group] Sample - 01 [1080p].mkv",
|
||||
title="Sample",
|
||||
bangumi_name="Sample (2024)",
|
||||
season=1,
|
||||
episode=1,
|
||||
suffix=".mkv",
|
||||
group="Group",
|
||||
)
|
||||
|
||||
# StrictUndefined:写错变量名在保存时就报错,而不是渲染出空字符串
|
||||
_env = SandboxedEnvironment(undefined=StrictUndefined, autoescape=False)
|
||||
_env.filters["pad"] = pad
|
||||
|
||||
|
||||
def _language(f: RenameInput) -> str:
|
||||
return f".{f.language}" if f.kind == "subtitle" else ""
|
||||
|
||||
|
||||
def standard_name(base: str, f: RenameInput) -> str:
|
||||
"""``{base} SxxEyy[.语言].ext``;电影为 ``{base}[.语言].ext``。"""
|
||||
if f.episode_type == "movie":
|
||||
return f"{base}{_language(f)}{f.suffix}"
|
||||
return f"{base} S{pad(f.season)}E{pad(f.episode)}{_language(f)}{f.suffix}"
|
||||
|
||||
|
||||
class PnRename:
|
||||
"""``pn``:以文件名解析出的标题命名。"""
|
||||
|
||||
def target_name(self, f: RenameInput) -> str:
|
||||
return standard_name(f.title, f)
|
||||
|
||||
|
||||
class AdvanceRename:
|
||||
"""``advance``:以番剧文件夹名命名。"""
|
||||
|
||||
def target_name(self, f: RenameInput) -> str:
|
||||
return standard_name(f.bangumi_name, f)
|
||||
|
||||
|
||||
class TemplateRename:
|
||||
"""``template``:渲染失败或文件名不安全时抛出 RenameSkipped,宿主保留原名
|
||||
并通知用户。绝不退回到 pn。"""
|
||||
|
||||
def __init__(self, template: Template) -> None:
|
||||
self._template = template
|
||||
|
||||
def target_name(self, f: RenameInput) -> str:
|
||||
try:
|
||||
stem = self._template.render(
|
||||
title=f.title,
|
||||
bangumi_name=f.bangumi_name,
|
||||
season=f.season,
|
||||
episode=f.episode,
|
||||
episode_type=f.episode_type,
|
||||
group=f.group or "",
|
||||
kind=f.kind,
|
||||
language=f.language,
|
||||
).strip()
|
||||
except Exception as e:
|
||||
raise RenameSkipped(f"模板渲染失败:{e}") from None
|
||||
if not stem or not stem.strip("."):
|
||||
raise RenameSkipped("模板渲染结果为空")
|
||||
if any(c in "/\\" or ord(c) < 32 for c in stem):
|
||||
raise RenameSkipped(f"模板渲染结果含路径分隔符或控制字符:{stem!r}")
|
||||
return f"{stem}{_language(f)}{f.suffix}"
|
||||
|
||||
|
||||
def compile_template(source: str) -> TemplateRename:
|
||||
"""编译并用示例文件试渲染;不合法时抛出 ValueError(保存配置时返回 422)。"""
|
||||
try:
|
||||
strategy = TemplateRename(_env.from_string(source))
|
||||
except TemplateError as e:
|
||||
raise ValueError(f"模板语法错误:{e}") from None
|
||||
try:
|
||||
strategy.target_name(SAMPLE)
|
||||
except RenameSkipped as e:
|
||||
raise ValueError(str(e)) from None
|
||||
return strategy
|
||||
|
||||
|
||||
class Options(BaseModel):
|
||||
template: str = Field(
|
||||
DEFAULT_TEMPLATE,
|
||||
title="文件名模板",
|
||||
description=(
|
||||
"重命名方式选择 template 时使用的 Jinja2 模板,渲染文件名主体(不含"
|
||||
"扩展名,字幕会自动追加语言)。可用变量:title、bangumi_name、season、"
|
||||
"episode、episode_type(episode / movie / special)、group、kind"
|
||||
"(media / subtitle)、language;过滤器 pad(n) 把数字补零到 n 位。"
|
||||
"渲染失败或结果含路径分隔符的文件保留原名,并发送通知。"
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("template")
|
||||
@classmethod
|
||||
def _check_template(cls, value: str) -> str:
|
||||
compile_template(value)
|
||||
return value
|
||||
|
||||
|
||||
class RenamePlugin(Plugin[Options]):
|
||||
config_model = Options
|
||||
|
||||
def __init__(self, ctx: PluginContext) -> None:
|
||||
super().__init__(ctx)
|
||||
self._template = compile_template(self.config.template)
|
||||
|
||||
@provider(points.RENAME_STRATEGY, id="pn")
|
||||
def pn_strategy(self) -> RenameStrategy:
|
||||
return PnRename()
|
||||
|
||||
@provider(points.RENAME_STRATEGY, id="advance")
|
||||
def advance_strategy(self) -> RenameStrategy:
|
||||
return AdvanceRename()
|
||||
|
||||
@provider(points.RENAME_STRATEGY, id="template")
|
||||
def template_strategy(self) -> RenameStrategy:
|
||||
return self._template
|
||||
@@ -40,6 +40,42 @@ def _reset_downloader_client_cache():
|
||||
_reset_client_cache()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Built-in rename plugin
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def _rename_plugin_cls():
|
||||
from module.plugin.loader import BUILTIN_ROOT, discover
|
||||
|
||||
candidates, _ = discover(
|
||||
local_root=BUILTIN_ROOT / "__missing__", entry_point_group="ab-test-none"
|
||||
)
|
||||
return {c.manifest.id: c for c in candidates}["rename"].load()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _builtin_rename_plugin(_rename_plugin_cls, tmp_path):
|
||||
"""内置插件 rename 默认启用:与生产一致,单元测试里 pn / advance /
|
||||
template 也已登记在进程级注册表中。"""
|
||||
from ab_sdk.hooks import PROVIDER_ATTR
|
||||
from ab_sdk.testing import create_plugin
|
||||
from module.plugin import host
|
||||
from module.plugin.registry import ProviderEntry
|
||||
|
||||
plugin, _ = create_plugin(_rename_plugin_cls, plugin_id="rename", data_dir=tmp_path)
|
||||
registry = host.get_registry()
|
||||
for name, member in vars(_rename_plugin_cls).items():
|
||||
spec = getattr(member, PROVIDER_ATTR, None)
|
||||
if spec is not None:
|
||||
registry.add_provider(
|
||||
spec.point, ProviderEntry("rename", spec.id, getattr(plugin, name))
|
||||
)
|
||||
yield
|
||||
registry.remove_plugin("rename")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Database Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -5,6 +5,7 @@ from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from ab_sdk import points
|
||||
from ab_sdk.events import (
|
||||
@@ -20,13 +21,15 @@ from ab_sdk.rename import (
|
||||
Revision,
|
||||
RevisionTask,
|
||||
)
|
||||
from ab_sdk.testing import RecordingBus
|
||||
from ab_sdk.testing import RecordingBus, create_plugin
|
||||
from module.downloader import DownloadClient
|
||||
from module.manager import renamer as renamer_module
|
||||
from module.manager.renamer import Renamer
|
||||
from module.manager.revision_policy import CoreConflictPolicy
|
||||
from module.models import EpisodeFile, SubtitleFile
|
||||
from module.plugin import host
|
||||
from module.models.config import Plugins
|
||||
from module.plugin import PluginManager, host
|
||||
from module.plugin.loader import BUILTIN_ROOT, discover
|
||||
from module.plugin.registry import ProviderEntry
|
||||
from module.plugin.runner import CircuitBreaker, HookRunner
|
||||
|
||||
@@ -253,3 +256,123 @@ def test_core_conflict_policy_decide_matches_3x_reasons(
|
||||
ConflictRequest("t.mkv", incoming, owners, configured, strict)
|
||||
)
|
||||
assert (decision.action, decision.reason) == (action, reason)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- built-in plugin
|
||||
|
||||
|
||||
def load_rename_plugin():
|
||||
candidates, errors = discover(
|
||||
local_root=BUILTIN_ROOT / "__missing__", entry_point_group="ab-test-none"
|
||||
)
|
||||
assert errors == []
|
||||
return {c.manifest.id: c for c in candidates}["rename"]
|
||||
|
||||
|
||||
def template_strategy(tmp_path, template: str):
|
||||
plugin, _ = create_plugin(
|
||||
load_rename_plugin().load(), {"template": template}, data_dir=tmp_path
|
||||
)
|
||||
return plugin.template_strategy()
|
||||
|
||||
|
||||
def rename_input(**overrides) -> RenameInput:
|
||||
fields = {
|
||||
"kind": "media",
|
||||
"media_path": NAME,
|
||||
"title": "Anime",
|
||||
"bangumi_name": "Anime (2024)",
|
||||
"season": 1,
|
||||
"episode": 1,
|
||||
"suffix": ".mkv",
|
||||
"group": "Sub",
|
||||
}
|
||||
return RenameInput(**{**fields, **overrides})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("template", "overrides", "expected"),
|
||||
[
|
||||
(None, {}, "Anime S01E01.mkv"),
|
||||
(None, {"episode": 12.5}, "Anime S01E12.5.mkv"),
|
||||
(
|
||||
"{{ title }} - S{{ season|pad(2) }}E{{ episode|pad(3) }}",
|
||||
{"kind": "subtitle", "language": "zh-tw", "suffix": ".ass"},
|
||||
"Anime - S01E001.zh-tw.ass",
|
||||
),
|
||||
(
|
||||
"{% if episode_type == 'movie' %}{{ bangumi_name }}"
|
||||
"{% else %}[{{ group }}] {{ title }} {{ episode }}{% endif %}",
|
||||
{"episode_type": "movie"},
|
||||
"Anime (2024).mkv",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_template_strategy_renders_stem_and_appends_suffix(
|
||||
tmp_path, template, overrides, expected
|
||||
):
|
||||
plugin, _ = create_plugin(
|
||||
load_rename_plugin().load(),
|
||||
{} if template is None else {"template": template},
|
||||
data_dir=tmp_path,
|
||||
)
|
||||
strategy = plugin.template_strategy()
|
||||
assert strategy.target_name(rename_input(**overrides)) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"template",
|
||||
[
|
||||
"{{ title ", # 语法错误
|
||||
"{{ titel }}", # 未定义变量
|
||||
"{{ title.__class__ }}", # 沙箱拒绝
|
||||
"{{ group }}/{{ title }}", # 路径分隔符
|
||||
"{{ '' }}", # 空文件名
|
||||
"..",
|
||||
],
|
||||
)
|
||||
def test_template_options_invalid_template_rejected(template):
|
||||
options_model = load_rename_plugin().load().config_model
|
||||
with pytest.raises(ValidationError):
|
||||
options_model.model_validate({"template": template})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("template", "overrides"),
|
||||
[
|
||||
("{{ group }}", {"group": None}), # 运行时渲染为空
|
||||
("{{ title }}", {"title": "a/b"}), # 运行时出现分隔符
|
||||
("{{ (episode / season)|int }}", {"season": 0}), # 运行时渲染异常
|
||||
],
|
||||
)
|
||||
def test_template_strategy_bad_runtime_name_raises_skipped(
|
||||
tmp_path, template, overrides
|
||||
):
|
||||
strategy = template_strategy(tmp_path, template)
|
||||
with pytest.raises(RenameSkipped):
|
||||
strategy.target_name(rename_input(**overrides))
|
||||
|
||||
|
||||
async def test_rename_plugin_loaded_by_manager_enabled_by_default(plugins, tmp_path):
|
||||
manager = PluginManager(
|
||||
SimpleNamespace(plugins=Plugins()),
|
||||
registry=plugins.registry,
|
||||
discover_fn=lambda: ([load_rename_plugin()], []),
|
||||
data_root=tmp_path,
|
||||
)
|
||||
await manager.start()
|
||||
try:
|
||||
status = {s.id: s for s in manager.statuses()}["rename"]
|
||||
assert status.state == "active"
|
||||
strategies = plugins.registry.providers(points.RENAME_STRATEGY)
|
||||
assert {pid: e.plugin_id for pid, e in strategies.items()} == {
|
||||
"none": "core",
|
||||
"pn": "rename",
|
||||
"advance": "rename",
|
||||
"template": "rename",
|
||||
}
|
||||
with pytest.raises(ValidationError):
|
||||
manager.validate_options("rename", {"template": "{{ nope }}"})
|
||||
finally:
|
||||
await manager.stop()
|
||||
assert set(plugins.registry.providers(points.RENAME_STRATEGY)) == {"none"}
|
||||
|
||||
@@ -18,6 +18,7 @@ const KIND_ROUTES: Record<string, string> = {
|
||||
offset_review: '/bangumi',
|
||||
download_failure: '/bangumi',
|
||||
rename_conflict: '/downloader',
|
||||
rename_skipped: '/downloader',
|
||||
};
|
||||
|
||||
const SEVERITY_ICONS = {
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { defineComponent, nextTick } from 'vue';
|
||||
import { defineComponent, nextTick, ref } from 'vue';
|
||||
import ConfigManage from '../config-manage.vue';
|
||||
|
||||
vi.mock('@/hooks/usePluginProviders', () => ({
|
||||
usePluginProviders: () =>
|
||||
ref({
|
||||
downloader: [],
|
||||
notifier: [],
|
||||
search_site: [],
|
||||
metadata_provider: [],
|
||||
rename_strategy: ['advance', 'pn', 'template'],
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/hooks/useMyI18n', () => ({
|
||||
useMyI18n: () => ({ t: (key: string) => key }),
|
||||
}));
|
||||
@@ -80,4 +91,29 @@ describe('config-manage', () => {
|
||||
};
|
||||
expect(store.__manageState.revision_conflict_policy).toBe('replace');
|
||||
});
|
||||
|
||||
it('should append plugin rename strategies when the rename plugin provides them', () => {
|
||||
const wrapper = mount(ConfigManage, {
|
||||
global: {
|
||||
stubs: {
|
||||
'ab-fold-panel': { template: '<section><slot /></section>' },
|
||||
'ab-setting': AbSettingStub,
|
||||
},
|
||||
},
|
||||
});
|
||||
const method = wrapper
|
||||
.findAllComponents(AbSettingStub)
|
||||
.find(
|
||||
(setting) =>
|
||||
(setting.props('label') as () => string)() ===
|
||||
'config.manage_set.method'
|
||||
);
|
||||
|
||||
expect(method?.props('prop')?.items).toEqual([
|
||||
'pn',
|
||||
'advance',
|
||||
'none',
|
||||
'template',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,12 +5,21 @@ import type {
|
||||
RevisionConflictPolicy,
|
||||
} from '#/config';
|
||||
import type { SelectItem, SettingItem } from '#/components';
|
||||
import { usePluginProviders } from '@/hooks/usePluginProviders';
|
||||
|
||||
const { t } = useMyI18n();
|
||||
const { getSettingGroup } = useConfigStore();
|
||||
|
||||
const manage = getSettingGroup('bangumi_manage');
|
||||
const renameMethod: RenameMethod = ['pn', 'advance', 'none'];
|
||||
const builtinMethods: RenameMethod = ['pn', 'advance', 'none'];
|
||||
const pluginProviders = usePluginProviders();
|
||||
// pn / advance 也由内置插件 rename 提供;插件新增的方式(如 template)追加在后
|
||||
const renameMethod = computed(() => [
|
||||
...builtinMethods,
|
||||
...pluginProviders.value.rename_strategy.filter(
|
||||
(id) => !(builtinMethods as readonly string[]).includes(id)
|
||||
),
|
||||
]);
|
||||
const revisionConflictPolicies: RevisionConflictPolicy = ['hold', 'replace'];
|
||||
|
||||
const revisionConflictOptions = computed<SelectItem[]>(() => [
|
||||
@@ -37,7 +46,7 @@ const items = computed<SettingItem<BangumiManage>[]>(() => [
|
||||
label: () => t('config.manage_set.method'),
|
||||
type: 'select',
|
||||
prop: {
|
||||
items: renameMethod,
|
||||
items: renameMethod.value,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@ const empty = {
|
||||
notifier: [],
|
||||
search_site: [],
|
||||
metadata_provider: [],
|
||||
rename_strategy: [],
|
||||
};
|
||||
|
||||
describe('usePluginProviders', () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ const providers = ref<PluginProviders>({
|
||||
notifier: [],
|
||||
search_site: [],
|
||||
metadata_provider: [],
|
||||
rename_strategy: [],
|
||||
});
|
||||
let loaded = false;
|
||||
|
||||
@@ -20,7 +21,7 @@ export async function refreshPluginProviders() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件提供的下载器 / 通知渠道 / 搜索站点 / 元数据源 id,供设置页下拉框合并候选。
|
||||
* 插件提供的下载器 / 通知渠道 / 搜索站点 / 元数据源 / 重命名方式 id,供设置页下拉框合并候选。
|
||||
* 首次使用时请求;请求失败(如未登录)时保持空列表,不影响内置选项。
|
||||
*/
|
||||
export function usePluginProviders() {
|
||||
|
||||
@@ -755,6 +755,10 @@
|
||||
"rename_conflict": {
|
||||
"title": "Media rename conflict",
|
||||
"body": "{torrent_name} → {target_path}: {reason}"
|
||||
},
|
||||
"rename_skipped": {
|
||||
"title": "Files kept their names",
|
||||
"body": "{torrent_name} ({strategy}): {reason}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,6 +755,10 @@
|
||||
"rename_conflict": {
|
||||
"title": "媒体文件重命名冲突",
|
||||
"body": "{torrent_name} → {target_path}:{reason}"
|
||||
},
|
||||
"rename_skipped": {
|
||||
"title": "文件未重命名",
|
||||
"body": "{torrent_name}({strategy}):{reason}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ const KNOWN_KINDS = [
|
||||
'llm_auth_failure',
|
||||
'llm_plugin_install_failed',
|
||||
'rename_conflict',
|
||||
'rename_skipped',
|
||||
] as const;
|
||||
|
||||
export const useNotificationStore = defineStore('notification', () => {
|
||||
|
||||
@@ -62,7 +62,8 @@ export interface RssParser {
|
||||
export interface BangumiManage {
|
||||
enable: boolean;
|
||||
eps_complete: boolean;
|
||||
rename_method: TupleToUnion<RenameMethod>;
|
||||
/** 内置方式,或插件提供的重命名方式 id(如 template) */
|
||||
rename_method: TupleToUnion<RenameMethod> | (string & {});
|
||||
revision_conflict_policy: TupleToUnion<RevisionConflictPolicy>;
|
||||
group_tag: boolean;
|
||||
remove_bad_torrent: boolean;
|
||||
|
||||
@@ -58,4 +58,5 @@ export interface PluginProviders {
|
||||
notifier: string[];
|
||||
search_site: string[];
|
||||
metadata_provider: string[];
|
||||
rename_strategy: string[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user