镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-20 13:47:47 +08:00
新增 episodeExtract 规则类型,支持从文件名提取季集号并按模板生成新文件名。提取失败的文件会在预览中显示错误信息并自动跳过执行,避免误操作。
- 支持模板变量:{prefix} {season} {episode} {ext}
- 提供 helperPre/helperPost 锚点辅助提取
- 支持集数偏移和自定义前导零位数
- 更新规则工厂、类型定义和国际化配置
- 添加单元测试和设计文档
201 行
4.7 KiB
TypeScript
201 行
4.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { ReplaceRule } from '../../src/rules/replace';
|
|
import { RegexRule } from '../../src/rules/regex';
|
|
import { NumberingRule } from '../../src/rules/numbering';
|
|
import { EpisodeExtractRule } from '../../src/rules/episode-extract';
|
|
import { RuleFactory } from '../../src/rules/rule-factory';
|
|
|
|
describe('ReplaceRule', () => {
|
|
it('should replace text in file name', () => {
|
|
const rule = new ReplaceRule({
|
|
search: 'old',
|
|
replace: 'new',
|
|
caseSensitive: false,
|
|
global: false,
|
|
});
|
|
|
|
const result = rule.execute('old_file.txt', 0, 1);
|
|
expect(result).toBe('new_file.txt');
|
|
});
|
|
|
|
it('should replace all occurrences when global is true', () => {
|
|
const rule = new ReplaceRule({
|
|
search: 'a',
|
|
replace: 'b',
|
|
caseSensitive: false,
|
|
global: true,
|
|
});
|
|
|
|
const result = rule.execute('aaa.txt', 0, 1);
|
|
expect(result).toBe('bbb.txt');
|
|
});
|
|
|
|
it('should be case sensitive', () => {
|
|
const rule = new ReplaceRule({
|
|
search: 'OLD',
|
|
replace: 'new',
|
|
caseSensitive: true,
|
|
global: false,
|
|
});
|
|
|
|
const result = rule.execute('old_file.txt', 0, 1);
|
|
expect(result).toBe('old_file.txt');
|
|
});
|
|
});
|
|
|
|
describe('NumberingRule', () => {
|
|
it('should add prefix numbering', () => {
|
|
const rule = new NumberingRule({
|
|
startNumber: 1,
|
|
digits: 3,
|
|
position: 'prefix',
|
|
format: '{num}',
|
|
separator: '_',
|
|
});
|
|
|
|
const result = rule.execute('file.txt', 0, 10);
|
|
expect(result).toBe('001_file.txt');
|
|
});
|
|
|
|
it('should add suffix numbering', () => {
|
|
const rule = new NumberingRule({
|
|
startNumber: 1,
|
|
digits: 2,
|
|
position: 'suffix',
|
|
format: 'E{num}',
|
|
separator: '_',
|
|
});
|
|
|
|
const result = rule.execute('video.mp4', 4, 10);
|
|
expect(result).toBe('video_E05.mp4');
|
|
});
|
|
|
|
it('should handle custom format', () => {
|
|
const rule = new NumberingRule({
|
|
startNumber: 1,
|
|
digits: 2,
|
|
position: 'prefix',
|
|
format: '第{num}集',
|
|
separator: '-',
|
|
});
|
|
|
|
const result = rule.execute('剧集.mp4', 0, 10);
|
|
expect(result).toBe('第01集-剧集.mp4');
|
|
});
|
|
});
|
|
|
|
describe('RegexRule', () => {
|
|
it('should replace using regex pattern', () => {
|
|
const rule = new RegexRule({
|
|
pattern: '^old_',
|
|
replace: 'new_',
|
|
caseSensitive: false,
|
|
global: false,
|
|
});
|
|
|
|
const result = rule.execute('old_file.txt', 0, 1);
|
|
expect(result).toBe('new_file.txt');
|
|
});
|
|
|
|
it('should support capture groups', () => {
|
|
const rule = new RegexRule({
|
|
pattern: '^(\\d+)-(.*)$',
|
|
replace: '$2-$1',
|
|
caseSensitive: false,
|
|
global: false,
|
|
});
|
|
|
|
const result = rule.execute('001-hello.txt', 0, 1);
|
|
expect(result).toBe('hello-001.txt');
|
|
});
|
|
|
|
it('should ignore g/i in custom flags and respect toggles', () => {
|
|
const rule = new RegexRule({
|
|
pattern: 'a',
|
|
replace: 'b',
|
|
caseSensitive: false,
|
|
global: false,
|
|
flags: 'g', // should NOT force global replace
|
|
});
|
|
|
|
const result = rule.execute('aaa.txt', 0, 1);
|
|
expect(result).toBe('baa.txt');
|
|
});
|
|
|
|
it('should allow updating extension when includeExtension is true', () => {
|
|
const rule = new RegexRule({
|
|
pattern: '\\.jpeg$',
|
|
replace: '.jpg',
|
|
caseSensitive: false,
|
|
global: false,
|
|
includeExtension: true,
|
|
});
|
|
|
|
const result = rule.execute('photo.jpeg', 0, 1);
|
|
expect(result).toBe('photo.jpg');
|
|
});
|
|
});
|
|
|
|
describe('RuleFactory', () => {
|
|
it('should create replace rule', () => {
|
|
const rule = RuleFactory.create({
|
|
type: 'replace',
|
|
params: {
|
|
search: 'old',
|
|
replace: 'new',
|
|
},
|
|
});
|
|
|
|
expect(rule).toBeInstanceOf(ReplaceRule);
|
|
});
|
|
|
|
it('should create regex rule', () => {
|
|
const rule = RuleFactory.create({
|
|
type: 'regex',
|
|
params: {
|
|
pattern: 'a+',
|
|
replace: 'b',
|
|
},
|
|
});
|
|
|
|
expect(rule).toBeInstanceOf(RegexRule);
|
|
});
|
|
|
|
it('should create numbering rule', () => {
|
|
const rule = RuleFactory.create({
|
|
type: 'numbering',
|
|
params: {
|
|
startNumber: 1,
|
|
digits: 3,
|
|
position: 'prefix',
|
|
format: '{num}',
|
|
separator: '_',
|
|
},
|
|
});
|
|
|
|
expect(rule).toBeInstanceOf(NumberingRule);
|
|
});
|
|
|
|
it('should create episode extract rule', () => {
|
|
const rule = RuleFactory.create({
|
|
type: 'episodeExtract',
|
|
params: {
|
|
template: '{prefix}.S{season}E{episode}{ext}',
|
|
prefix: 'Series',
|
|
season: 1,
|
|
},
|
|
});
|
|
|
|
expect(rule).toBeInstanceOf(EpisodeExtractRule);
|
|
});
|
|
|
|
it('should throw error for unknown rule type', () => {
|
|
expect(() =>
|
|
RuleFactory.create({
|
|
type: 'unknown' as any,
|
|
params: {},
|
|
})
|
|
).toThrow();
|
|
});
|
|
});
|