Skip to content

Commit

Permalink
merge upstream
Browse files Browse the repository at this point in the history
  • Loading branch information
misaka0508 committed Sep 24, 2024
2 parents e96ce7f + ca17022 commit ea2164c
Showing 23 changed files with 408 additions and 192 deletions.
1 change: 1 addition & 0 deletions .github/workflows/dotnet-desktop.yml
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ on:

env:
DIST_DIR: /tmp/builds
ENABLE_NATIVE_LIBS: true

jobs:
build:
75 changes: 63 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -43,21 +43,24 @@ SubRenamer 专注于字幕文件改名,简单易用。

- **自动匹配**:自动识别算法,一键匹配
- **拖拽导入**:拖拽快速导入文件及文件夹
- **多语言筛选**:自动检测并筛选指定语言的字幕
- **多种匹配规则**:对于复杂的文件名格式,支持手动匹配
- **多语言匹配**:支持视频字幕多语言匹配(一对多映射)
- **多语言筛选**:导入前自动检测并筛选指定语言的字幕
- **多匹配规则**:对于复杂的文件名格式,支持手动匹配
- **手动匹配编辑器**:自定义规则,支持简单通配符
- **正则表达式编辑器**:包含正则表达式匹配测试工具
- **匹配微调**:支持对匹配结果进行微调
- **改名命令**:右键快速复制 Linux 改名命令到剪贴板
- **字幕备份**:改名前自动备份字幕文件
- **后缀名**:通过后缀名自动区分视频和字幕,支持自定义
- **追加后缀**:支持在文件扩展名前添加自定义后缀
- **文件识别**:通过文件扩展名自动区分视频和字幕,支持自定义
- **快捷键**:支持快捷键操作,提高效率
- **夜间模式**:支持夜间模式,跟随系统切换
- **窗口置顶**:支持窗口置顶,方便操作
- **跨平台**:支持 Windows、macOS、Linux
- **体积小**:仅 15MB 左右

> [!IMPORTANT]\
> 重制说明:SubRenamer 第一版于 2019 年发布,当时使用 WinForm 进行开发,仅支持 Windows 平台。2024 年 SubRenamer 完成重制发布 v2.0 版本,采用全新技术栈 AvaloniaUI + .NET 8 开发,支持跨平台,能够在 Windows、macOS、Linux 上原生运行(不是 Electron.js)。
> 重制说明:SubRenamer 第一版于 2019 年发布,当时使用 WinForm 进行开发,仅支持 Windows 平台。2024 年 SubRenamer 完成重制发布 v2.0 版本,采用全新技术栈 AvaloniaUI + .NET 8 开发,支持跨平台,可以在 Windows、macOS、Linux 上原生运行(不是 Electron.js)。
<img width="800" src="https://github.com/qwqcode/SubRenamer/assets/22412567/9b620a47-61cb-418a-b6d3-3dd2e0140f69">

@@ -73,9 +76,9 @@ SubRenamer 专注于字幕文件改名,简单易用。
|-|-|
| <img width="600" src="https://github.com/qwqcode/SubRenamer/assets/22412567/fa46d20a-3c95-440f-90a1-f50df192c876"> | <img width="512" src="https://github.com/qwqcode/SubRenamer/assets/22412567/59e1b56f-14d9-4414-adcc-7f259b138a35"> |

| 右键菜单 | 快捷键支持 | 字幕备份 |
| 右键菜单 | 快捷键支持 | 设置界面 |
|-|-|-|
| <img width="224" src="https://github.com/qwqcode/SubRenamer/assets/22412567/e890b761-149f-4902-90ea-6f7ff7b91699"> | <img width="224" src="https://github.com/qwqcode/SubRenamer/assets/22412567/b06126e1-4541-442e-b76f-5de792c7db81"> | <img width="412" src="https://github.com/qwqcode/SubRenamer/assets/22412567/dbb0305a-9d1a-4d85-9e9a-7c7a45a82e25"> |
| <img width="224" src="https://github.com/qwqcode/SubRenamer/assets/22412567/e890b761-149f-4902-90ea-6f7ff7b91699"> | <img width="224" src="https://github.com/qwqcode/SubRenamer/assets/22412567/b06126e1-4541-442e-b76f-5de792c7db81"> | <img width="412" src="https://github.com/user-attachments/assets/84d5c217-1bf1-4d0d-b137-899189b44553"> |

**拖拽导入文件**

@@ -97,7 +100,9 @@ SubRenamer 专注于字幕文件改名,简单易用。

为实施自动匹配,需导入至少两个文件名格式一致的视频文件和两个字幕文件。

> 相关代码可见:[SubRenamer/Matcher](https://github.com/qwqcode/SubRenamer/tree/main/SubRenamer/Matcher)
- 算法代码:[SubRenamer/Matcher](https://github.com/qwqcode/SubRenamer/tree/main/SubRenamer/Matcher)(入口函数位于 [Matcher.cs](https://github.com/qwqcode/SubRenamer/blob/main/SubRenamer/Matcher/Matcher.cs) 文件内)
- 单元测试代码:[SubRenamer.Tests](https://github.com/qwqcode/SubRenamer/tree/main/SubRenamer.Tests)
- 测试用例数据:[TopLevelTests.json](https://github.com/qwqcode/SubRenamer/blob/main/SubRenamer.Tests/MatcherTests/TopLevelTests.json)**其中包含了自动匹配算法的示例数据**

### 手动匹配模式

@@ -107,7 +112,7 @@ SubRenamer 专注于字幕文件改名,简单易用。

<details>

<summary>请听 ABCDE 的故事</summary>
<summary>请听 ABCDE 的故事(🌫️</summary>

> (缩减版) 小A下载了一部新更的生肉番,又从字幕网站下载到了一套字幕文件,生肉番的 视频文件名 常常和 字幕文件名 不一致,看番时需要手动选定字幕,下次打开又得重新选定。小A拥有了 **SubRenamer**,从此改名交给他来做,终于可以安安心心看番啦。
@@ -156,22 +161,68 @@ AVALONIA_SCREEN_SCALE_FACTORS="eDP-1=2;" ./SubRenamer

## 编译说明

建议使用 Rider 或 Visual Studio 2022 打开项目。
建议使用 JetBrains Rider 或 Visual Studio 2022 打开项目。

### Prerequisites

Windows
**Windows**

- Visual Studio 2022, including .NET 8 & Desktop development with C++ workload.
- Alternatively, you can install JetBrains Rider to build the project. (Recommended).

**Fedora (36+)**

```bash
Visual Studio 2022, including .NET 8 & Desktop development with C++ workload.
sudo dnf group install "C Development Tools and Libraries" "Development Tools"

sudo dnf install dotnet-sdk-8.0 libicu-devel cmake zlib-devel -y
```

Ubuntu (20.04+)
**Ubuntu (20.04+)**

```bash
sudo apt-get install dotnet-sdk-8.0 libicu-dev cmake zlib1g-dev -y
```

**macOS (12+)**

```bash
# Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Install xcode command line tools
xcode-select --install

# Install dependencies
brew install dotnet-sdk8 icu4c cmake zlib
```

****

### 单元测试

```bash
dotnet test SubRenamer.Tests --verbosity normal
```

单元测试代码位于 [SubRenamer.Tests](https://github.com/qwqcode/SubRenamer/tree/main/SubRenamer.Tests) 目录内,推荐使用 Rider 内置的可视化工具执行测试和查看测试结果。

<img width="1432" src="https://github.com/user-attachments/assets/4e922f6b-08f0-4e72-9d8d-90db8358e46c">

**测试数据**

[TopLevelTests.json](https://github.com/qwqcode/SubRenamer/blob/main/SubRenamer.Tests/MatcherTests/TopLevelTests.json) 文件存放了测试用例数据,包含各种各样的字幕和视频文件名列表用于测试匹配算法,欢迎提交 PR 添加更多测试用例,修改文件后执行单元测试命令即可查看测试结果。

每次代码提交将通过 GitHub Actions 自动执行单元测试,确保代码质量。

### 构建单文件

在 Win 平台,为了构建出单个包含静态链接依赖库的 exe 文件(无额外的动态链接 dll 依赖库文件),需要手动把 [这几个 dll 文件](https://github.com/qwqcode/qwqcode/releases/tag/dotnet-lib) 下载放到 `native` 目录内。然后添加环境变量 `ENABLE_NATIVE_LIBS=true` 再执行编译。

- https://github.com/qwqcode/SubRenamer/blob/main/.github/workflows/dotnet-desktop.yml
- https://github.com/AvaloniaUI/Avalonia/issues/9503
- https://github.com/qwqcode/SubRenamer/blob/main/SubRenamer/SubRenamer.csproj

### Publish with NativeAOT

```bash
20 changes: 20 additions & 0 deletions SubRenamer.Tests/MatcherTests/MergeSameKeysItemsTests.cs
Original file line number Diff line number Diff line change
@@ -138,4 +138,24 @@ public void EmptyInput()
{
Assert.That(MergeSameKeysItems(new List<MatchItem>()), Is.Empty);
}

[Test]
public void KeepVideoIfNoSubtitles()
{
var input = new List<MatchItem>
{
new("k1", "v1.mp4", ""),
new("k1", "", "s1.srt"),
new("k2", "v2.mp4", ""),
new("k3", "v3.mp4", ""),
};
var output = new List<MatchItem>
{
new("k1", "v1.mp4", "s1.srt"),
new("k2", "v2.mp4", ""),
new("k3", "v3.mp4", ""),
};

Assert.That(MergeSameKeysItems(input), Is.EqualTo(output));
}
}
2 changes: 1 addition & 1 deletion SubRenamer.Tests/MatcherTests/TopLevelTests.cs
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ public void TestCasesFromJson(string name, List<MatchItem> input, List<MatchItem
{
var actual = Execute(input);

var jsonOpts = new JsonSerializerOptions { WriteIndented = true };
var jsonOpts = new JsonSerializerOptions { WriteIndented = true, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
TestContext.Progress.WriteLine("{1}\n\n \ud83c\udf1f Matcher Test Case: {0}\n\n{1}", name, new string('=', 50));
TestContext.Progress.WriteLine("{2}\n {0}\n{2}\n{1}", "Input", JsonSerializer.Serialize(input, jsonOpts),
new string('-', 50));
93 changes: 58 additions & 35 deletions SubRenamer.Tests/MatcherTests/TopLevelTests.json
Original file line number Diff line number Diff line change
@@ -4,55 +4,37 @@
"Input": [
{"Key": "", "Video": "abc.S02E01.123.mkv", "Subtitle": ""},
{"Key": "", "Video": "abc.S02E02.abc.mkv", "Subtitle": ""},
{"Key": "", "Video": "abc.S02E03.ccc.mkv", "Subtitle": ""},
{"Key": "", "Video": "", "Subtitle": "[SubGroup] def.S02E01.xyz.ass"},
{"Key": "", "Video": "", "Subtitle": "[SubGroup] def.S02E02.abc.ass"}
{"Key": "", "Video": "", "Subtitle": "[SubGroup] def.S02E02.abc.ass"},
{"Key": "", "Video": "", "Subtitle": "[SubGroup] def.S02E04.kkk.ass"}
],
"Output": [
{"Key": "1", "Video": "abc.S02E01.123.mkv", "Subtitle": "[SubGroup] def.S02E01.xyz.ass"},
{"Key": "2", "Video": "abc.S02E02.abc.mkv", "Subtitle": "[SubGroup] def.S02E02.abc.ass"}
{"Key": "2", "Video": "abc.S02E02.abc.mkv", "Subtitle": "[SubGroup] def.S02E02.abc.ass"},
{"Key": "3", "Video": "abc.S02E03.ccc.mkv", "Subtitle": ""},
{"Key": "4", "Video": "", "Subtitle": "[SubGroup] def.S02E04.kkk.ass"}
]
},
{
"Name": "SquareBrackets",
"Input": [
{"Key": "", "Video": "abc [01].mkv", "Subtitle": ""},
{"Key": "", "Video": "abc [02].mkv", "Subtitle": ""},
{"Key": "", "Video": "", "Subtitle": "[SubGroup] def [01].ass"},
{"Key": "", "Video": "", "Subtitle": "[SubGroup] def [02].ass"}
],
"Output": [
{"Key": "1", "Video": "abc [01].mkv", "Subtitle": "[SubGroup] def [01].ass"},
{"Key": "2", "Video": "abc [02].mkv", "Subtitle": "[SubGroup] def [02].ass"}
]
},
{
"Name": "Chinese",
"Input": [
{"Key": "", "Video": "中文 第一集.mkv", "Subtitle": ""},
{"Key": "", "Video": "中文 第二集.mkv", "Subtitle": ""},
{"Key": "", "Video": "", "Subtitle": "【字幕】中文 第一集.srt"},
{"Key": "", "Video": "", "Subtitle": "【字幕】中文 第二集.srt"}
],
"Output": [
{"Key": "", "Video": "中文 第一集.mkv", "Subtitle": "【字幕】中文 第一集.srt"},
{"Key": "", "Video": "中文 第二集.mkv", "Subtitle": "【字幕】中文 第二集.srt"}
]
},
{
"Name": "Breaking Bad",
"Name": "Breaking Bad (with episode titles)",
"Input": [
{"Key": "", "Video": "Breaking.Bad.S03E04.Green.Light.2160p.Netflix.WEB-DL.DDP.5.1.H.265.mkv", "Subtitle": ""},
{"Key": "", "Video": "Breaking.Bad.S03E12.Half.Measures.2160p.Netflix.WEB-DL.DDP.5.1.H.265.mkv", "Subtitle": ""},
{"Key": "", "Video": "Breaking.Bad.S03E100.Test.Test.2160p.Netflix.WEB-DL.DDP.5.1.H.265.mkv", "Subtitle": ""},
{"Key": "", "Video": "", "Subtitle": "breaking.bad.s03e12.720p.hdtv.x264-ctu.en.srt"},
{"Key": "", "Video": "", "Subtitle": "Breaking.Bad.S03E04.720p.HDTV.x264-CTU.en.srt"}
{"Key": "", "Video": "", "Subtitle": "Breaking.Bad.S03E04.720p.HDTV.x264-CTU.en.srt"},
{"Key": "", "Video": "", "Subtitle": "Breaking.Bad.S03E123.720p.HDTV.x264-CTU.en.srt"}
],
"Output": [
{"Key": "4", "Video": "Breaking.Bad.S03E04.Green.Light.2160p.Netflix.WEB-DL.DDP.5.1.H.265.mkv", "Subtitle": "Breaking.Bad.S03E04.720p.HDTV.x264-CTU.en.srt"},
{"Key": "12", "Video": "Breaking.Bad.S03E12.Half.Measures.2160p.Netflix.WEB-DL.DDP.5.1.H.265.mkv", "Subtitle": "breaking.bad.s03e12.720p.hdtv.x264-ctu.en.srt"}
{"Key": "12", "Video": "Breaking.Bad.S03E12.Half.Measures.2160p.Netflix.WEB-DL.DDP.5.1.H.265.mkv", "Subtitle": "breaking.bad.s03e12.720p.hdtv.x264-ctu.en.srt"},
{"Key": "100", "Video": "Breaking.Bad.S03E100.Test.Test.2160p.Netflix.WEB-DL.DDP.5.1.H.265.mkv", "Subtitle": ""},
{"Key": "123", "Video": "", "Subtitle": "Breaking.Bad.S03E123.720p.HDTV.x264-CTU.en.srt"}
]
},
{
"Name": "Pantheon",
"Name": "Pantheon (with episode titles)",
"Input": [
{"Key": "", "Video": "Pantheon.S02E04.Olivia.and.Farhad.1080p.AMZN.WEB-DL.DD5.1.H.264-testWEB.mkv", "Subtitle": ""},
{"Key": "", "Video": "Pantheon.S02E05.Yair.1080p.AMZN.WEB-DL.DD5.1.H.264-testWEB.mkv", "Subtitle": ""},
@@ -68,7 +50,7 @@
]
},
{
"Name": "鬼滅之刃",
"Name": "鬼滅之刃 (with complex characters)",
"Input": [
{"Key": "", "Video": "[Up to 21°C] 鬼滅之刃 柱訓練篇 - 02 (Baha 1920x1080 AVC AAC MP4) [784C8989].mp4", "Subtitle": ""},
{"Key": "", "Video": "[Up to 21°C] 鬼滅之刃 柱訓練篇 - 04 (Baha 1920x1080 AVC AAC MP4) [41B42367].mp4", "Subtitle": ""},
@@ -81,7 +63,7 @@
]
},
{
"Name": "BlackDoor",
"Name": "BlackDoor (with parentheses)",
"Input": [
{"Key": "", "Video": "Black Mirror (2011)(1080p)(Webdl)(VP9)(14 lang-AAC- 2.0) (S01) PHDTeam.mkv", "Subtitle": ""},
{"Key": "", "Video": "Black Mirror (2011)(1080p)(Webdl)(VP9)(14 lang-AAC- 2.0) (S11) PHDTeam.mkv", "Subtitle": ""},
@@ -94,20 +76,61 @@
]
},
{
"Name": "Haikyuu!! (sc & tc)",
"Name": "Haikyuu!! (with multiple languages, one-to-many)",
"Input": [
{"Key": "", "Video": "[Kamigami] Haikyuu!! S2 - 09 [1920x1080 HEVC AAC Sub(Chs,Cht,Jap)].mkv", "Subtitle": ""},
{"Key": "", "Video": "[Kamigami] Haikyuu!! S2 - 10 [1920x1080 HEVC AAC Sub(Chs,Cht,Jap)].mkv", "Subtitle": ""},
{"Key": "", "Video": "", "Subtitle": "[YYDM-11FANS][Haikyuu!!][09][BDRIP][720P][X264-10bit_AAC][40A7E056].en.ass"},
{"Key": "", "Video": "", "Subtitle": "[YYDM-11FANS][Haikyuu!!][09][BDRIP][720P][X264-10bit_AAC][40A7E056].sc.ass"},
{"Key": "", "Video": "", "Subtitle": "[YYDM-11FANS][Haikyuu!!][09][BDRIP][720P][X264-10bit_AAC][40A7E056].tc.ass"},
{"Key": "", "Video": "", "Subtitle": "[YYDM-11FANS][Haikyuu!!][10][BDRIP][720P][X264-10bit_AAC][6FDEFD72].sc.ass"},
{"Key": "", "Video": "", "Subtitle": "[YYDM-11FANS][Haikyuu!!][10][BDRIP][720P][X264-10bit_AAC][6FDEFD72].tc.ass"}
],
"Output": [
{"Key": "9", "Video": "[Kamigami] Haikyuu!! S2 - 09 [1920x1080 HEVC AAC Sub(Chs,Cht,Jap)].mkv", "Subtitle": "[YYDM-11FANS][Haikyuu!!][09][BDRIP][720P][X264-10bit_AAC][40A7E056].en.ass"},
{"Key": "9", "Video": "[Kamigami] Haikyuu!! S2 - 09 [1920x1080 HEVC AAC Sub(Chs,Cht,Jap)].mkv", "Subtitle": "[YYDM-11FANS][Haikyuu!!][09][BDRIP][720P][X264-10bit_AAC][40A7E056].sc.ass"},
{"Key": "9", "Video": "[Kamigami] Haikyuu!! S2 - 09 [1920x1080 HEVC AAC Sub(Chs,Cht,Jap)].mkv", "Subtitle": "[YYDM-11FANS][Haikyuu!!][09][BDRIP][720P][X264-10bit_AAC][40A7E056].tc.ass"},
{"Key": "10", "Video": "[Kamigami] Haikyuu!! S2 - 10 [1920x1080 HEVC AAC Sub(Chs,Cht,Jap)].mkv", "Subtitle": "[YYDM-11FANS][Haikyuu!!][10][BDRIP][720P][X264-10bit_AAC][6FDEFD72].sc.ass"},
{"Key": "10", "Video": "[Kamigami] Haikyuu!! S2 - 10 [1920x1080 HEVC AAC Sub(Chs,Cht,Jap)].mkv", "Subtitle": "[YYDM-11FANS][Haikyuu!!][10][BDRIP][720P][X264-10bit_AAC][6FDEFD72].tc.ass"}
]
},
{
"Name": "SquareBrackets",
"Input": [
{"Key": "", "Video": "abc [01].mkv", "Subtitle": ""},
{"Key": "", "Video": "abc [02].mkv", "Subtitle": ""},
{"Key": "", "Video": "", "Subtitle": "[SubGroup] def [01].ass"},
{"Key": "", "Video": "", "Subtitle": "[SubGroup] def [02].ass"}
],
"Output": [
{"Key": "1", "Video": "abc [01].mkv", "Subtitle": "[SubGroup] def [01].ass"},
{"Key": "2", "Video": "abc [02].mkv", "Subtitle": "[SubGroup] def [02].ass"}
]
},
{
"Name": "Chinese Characters",
"Input": [
{"Key": "", "Video": "中文 第一集.mkv", "Subtitle": ""},
{"Key": "", "Video": "中文 第二集.mkv", "Subtitle": ""},
{"Key": "", "Video": "", "Subtitle": "【字幕】中文 第一集.srt"},
{"Key": "", "Video": "", "Subtitle": "【字幕】中文 第二集.srt"}
],
"Output": [
{"Key": "", "Video": "中文 第一集.mkv", "Subtitle": "【字幕】中文 第一集.srt"},
{"Key": "", "Video": "中文 第二集.mkv", "Subtitle": "【字幕】中文 第二集.srt"}
]
},
{
"Name": "WhiteSpaceEnd",
"Input": [
{"Key": "", "Video": "视频 1 xyz.mov", "Subtitle": ""},
{"Key": "", "Video": "视频 77 test xyz.mov", "Subtitle": ""},
{"Key": "", "Video": "", "Subtitle": "字幕 1xyz.srt"},
{"Key": "", "Video": "", "Subtitle": "字幕 77test xyz.srt"}
],
"Output": [
{"Key": "1", "Video": "视频 1 xyz.mov", "Subtitle": "字幕 1xyz.srt"},
{"Key": "77", "Video": "视频 77 test xyz.mov", "Subtitle": "字幕 77test xyz.srt"}
]
}
]
11 changes: 7 additions & 4 deletions SubRenamer.Tests/SubRenamer.Tests.csproj
Original file line number Diff line number Diff line change
@@ -11,10 +11,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="NUnit" Version="4.1.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
</ItemGroup>

1 change: 1 addition & 0 deletions SubRenamer/Config.cs
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ public partial class Config
public bool UpdateCheck { get; set; } = true;
public bool KeepLangExt { get; set; } = false;
public string CustomLangExt { get; set; } = "";
public bool FileConflictFilter { get; set; } = true;
public string VideoExtAppend { get; set; } = "";
public string SubtitleExtAppend { get; set; } = "";

2 changes: 1 addition & 1 deletion SubRenamer/Helper/MatchItemHelper.cs
Original file line number Diff line number Diff line change
@@ -9,6 +9,6 @@ public static void UpdateMatchItemStatus(MatchItem item)
if (item.Key != "" && item.Subtitle != "" && item.Video != "") item.Status = "已匹配";
else if (item.Key != "" && item.Subtitle != "") item.Status = "缺视频";
else if (item.Key != "" && item.Video != "") item.Status = "缺字幕";
else if (item.Key == "") item.Status = "";
else if (item.Key == "") item.Status = "未匹配";
}
}
Loading

0 comments on commit ea2164c

Please sign in to comment.