mirror of
https://github.com/truewhile/MeBox.git
synced 2026-11-12 19:34:24 +08:00
feat: 字幕轨名称可读化
外挂字幕原来直接拿文件名的语言后缀当名称(chs、chs[BilibiliID_danmu]), 内嵌字幕没有 title 标签时也是裸语言码(chi)。改成统一映射成中文名: chi/zh -> 中文,chs -> 简体中文,jpn -> 日本語,并把弹幕转存的 [BilibiliID_danmu] 文件标注为「弹幕」,避免和对白字幕混淆。 认不出来的标记保持原样(含 und),Emby 客户端的「字幕 (ASS)」兜底不变。
This commit is contained in:
@@ -182,6 +182,77 @@ func (s *SubtitleService) discoverExternalUncached(ctx context.Context, mediaID
|
||||
return discoverExternalSubtitleTracks(m), nil
|
||||
}
|
||||
|
||||
// subtitleLanguageNames 把文件名后缀/容器里的语言标记折成可读名称。
|
||||
//
|
||||
// 外挂字幕只能从文件名拿到语言(`movie.chs.ass` → `chs`),直接展示给用户
|
||||
// 就是「chs」这种看不懂的值;内嵌轨没有 title 标签时同理。认不出来的标记
|
||||
// 原样保留,交给调用方(例如 Emby 的「字幕 (ASS)」兜底)处理。
|
||||
var subtitleLanguageNames = map[string]string{
|
||||
"zh": "中文",
|
||||
"chi": "中文",
|
||||
"zho": "中文",
|
||||
"cn": "中文",
|
||||
|
||||
"chs": "简体中文",
|
||||
"sc": "简体中文",
|
||||
"zh-cn": "简体中文",
|
||||
"zh-hans": "简体中文",
|
||||
"zh-sg": "简体中文",
|
||||
|
||||
"cht": "繁体中文",
|
||||
"tc": "繁体中文",
|
||||
"zh-tw": "繁体中文",
|
||||
"zh-hk": "繁体中文",
|
||||
"zh-mo": "繁体中文",
|
||||
"zh-hant": "繁体中文",
|
||||
|
||||
"jpn": "日本語",
|
||||
"ja": "日本語",
|
||||
"jp": "日本語",
|
||||
|
||||
"eng": "English",
|
||||
"en": "English",
|
||||
|
||||
"kor": "한국어",
|
||||
"ko": "한국어",
|
||||
|
||||
"yue": "粤语",
|
||||
}
|
||||
|
||||
// subtitleLanguageName 返回语言标记的可读名称,认不出来时返回空串。
|
||||
func subtitleLanguageName(raw string) string {
|
||||
tag := strings.ToLower(strings.TrimSpace(strings.ReplaceAll(raw, "_", "-")))
|
||||
if tag == "" {
|
||||
return ""
|
||||
}
|
||||
if name, ok := subtitleLanguageNames[tag]; ok {
|
||||
return name
|
||||
}
|
||||
// `chs[bilibiliid_danmu]`、`chs.ass` 这类带附加信息的标记先取前缀。
|
||||
head := tag
|
||||
if i := strings.IndexAny(head, "[.@"); i > 0 {
|
||||
head = strings.TrimSpace(head[:i])
|
||||
}
|
||||
return subtitleLanguageNames[head]
|
||||
}
|
||||
|
||||
// subtitleDisplayLabel 把语言标记换成可读名称;认不出来时保留原值(含 und)。
|
||||
func subtitleDisplayLabel(raw string) string {
|
||||
if name := subtitleLanguageName(raw); name != "" {
|
||||
return name
|
||||
}
|
||||
return strings.TrimSpace(raw)
|
||||
}
|
||||
|
||||
// isDanmakuSubtitle 报告字幕文件是否是弹幕转存(`xxx[BilibiliID_danmu].ass`)。
|
||||
// 这类轨道是飘过的弹幕而不是对白字幕,名称里要标出来,避免和对白字幕混淆。
|
||||
func isDanmakuSubtitle(name string) bool {
|
||||
lower := strings.ToLower(name)
|
||||
return strings.Contains(lower, "danmu") ||
|
||||
strings.Contains(lower, "danmaku") ||
|
||||
strings.Contains(name, "弹幕")
|
||||
}
|
||||
|
||||
func discoverExternalSubtitleTracks(m *model.Media) []SubtitleTrack {
|
||||
dir := filepath.Dir(m.Path)
|
||||
bases := mediaSidecarBaseVariants(m.Path)
|
||||
@@ -228,9 +299,13 @@ func discoverExternalSubtitleTracks(m *model.Media) []SubtitleTrack {
|
||||
matchedBase = bases[0]
|
||||
}
|
||||
lang := detectLang(fullName, matchedBase)
|
||||
label := subtitleDisplayLabel(lang)
|
||||
if isDanmakuSubtitle(e.Name()) {
|
||||
label += " · 弹幕"
|
||||
}
|
||||
tracks = append(tracks, SubtitleTrack{
|
||||
Lang: lang,
|
||||
Label: lang,
|
||||
Label: label,
|
||||
Path: filepath.Join(c, e.Name()),
|
||||
Codec: codec,
|
||||
Source: "external",
|
||||
@@ -319,7 +394,11 @@ func subtitleTracksFromProbe(probe embeddedSubtitleProbe) []SubtitleTrack {
|
||||
}
|
||||
label := strings.TrimSpace(stream.Tags.Title)
|
||||
if label == "" {
|
||||
label = lang
|
||||
// 没有 title 标签时用可读语言名(chi → 中文)代替裸语言码。
|
||||
label = subtitleDisplayLabel(lang)
|
||||
}
|
||||
if label == "" {
|
||||
label = "字幕"
|
||||
}
|
||||
if stream.Disposition.Forced != 0 {
|
||||
label += "(强制)"
|
||||
|
||||
@@ -202,3 +202,103 @@ func TestSubtitleServeRawWritesSourceBytes(t *testing.T) {
|
||||
t.Fatal("ServeRaw accepted a non-subtitle file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtitleLanguageNameMapsCommonTags(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"chs": "简体中文",
|
||||
"CHS": "简体中文",
|
||||
"zh-Hans": "简体中文",
|
||||
"cht": "繁体中文",
|
||||
"chi": "中文",
|
||||
"zh": "中文",
|
||||
"jpn": "日本語",
|
||||
"eng": "English",
|
||||
"kor": "한국어",
|
||||
"chs[bilibiliid_danmu]": "简体中文",
|
||||
"und": "",
|
||||
"": "",
|
||||
}
|
||||
for raw, want := range cases {
|
||||
if got := subtitleLanguageName(raw); got != want {
|
||||
t.Errorf("subtitleLanguageName(%q) = %q, want %q", raw, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtitleDisplayLabelKeepsUnknownTags(t *testing.T) {
|
||||
// 认不出来的标记原样返回:Emby 的 DisplayTitle 依赖 `und` 走「字幕 (ASS)」兜底。
|
||||
if got := subtitleDisplayLabel("und"); got != "und" {
|
||||
t.Fatalf("subtitleDisplayLabel(und) = %q, want und", got)
|
||||
}
|
||||
if got := subtitleDisplayLabel("weird"); got != "weird" {
|
||||
t.Fatalf("subtitleDisplayLabel(weird) = %q, want weird", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDanmakuSubtitle(t *testing.T) {
|
||||
if !isDanmakuSubtitle("S01E01.chs[BilibiliID_danmu].ass") {
|
||||
t.Fatal("danmaku sidecar was not detected")
|
||||
}
|
||||
if !isDanmakuSubtitle("弹幕.ass") {
|
||||
t.Fatal("Chinese danmaku name was not detected")
|
||||
}
|
||||
if isDanmakuSubtitle("S01E01.chs.ass") {
|
||||
t.Fatal("normal subtitle was misdetected as danmaku")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverExternalSubtitleLabelsAreReadable(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
videoPath := filepath.Join(dir, "S01E01.mkv")
|
||||
files := map[string]string{
|
||||
"S01E01.chs.ass": "[Script Info]\n",
|
||||
"S01E01.chs[BilibiliID_danmu].ass": "[Script Info]\n",
|
||||
}
|
||||
for name, body := range files {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
m := &model.Media{Path: videoPath}
|
||||
tracks := discoverExternalSubtitleTracks(m)
|
||||
if len(tracks) != 2 {
|
||||
t.Fatalf("len(tracks) = %d, want 2: %#v", len(tracks), tracks)
|
||||
}
|
||||
labels := map[string]string{}
|
||||
for _, track := range tracks {
|
||||
labels[filepath.Base(track.Path)] = track.Label
|
||||
}
|
||||
if got := labels["S01E01.chs.ass"]; got != "简体中文" {
|
||||
t.Fatalf("chs.ass label = %q, want 简体中文", got)
|
||||
}
|
||||
if got := labels["S01E01.chs[BilibiliID_danmu].ass"]; got != "简体中文 · 弹幕" {
|
||||
t.Fatalf("danmaku label = %q, want 简体中文 · 弹幕", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmbeddedSubtitleProbeUsesReadableLanguage(t *testing.T) {
|
||||
var probe embeddedSubtitleProbe
|
||||
raw := []byte(`{"streams":[
|
||||
{"index":2,"codec_name":"hdmv_pgs_subtitle","tags":{"language":"chi"},"disposition":{"default":1}},
|
||||
{"index":3,"codec_name":"subrip","tags":{"language":"jpn"}},
|
||||
{"index":4,"codec_name":"subrip","tags":{"language":"chi","title":"简日双语"}}
|
||||
]}`)
|
||||
if err := json.Unmarshal(raw, &probe); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tracks := subtitleTracksFromProbe(probe)
|
||||
if len(tracks) != 3 {
|
||||
t.Fatalf("len(tracks) = %d, want 3", len(tracks))
|
||||
}
|
||||
if got := tracks[0].Label; got != "中文(默认)(内嵌·图片)" {
|
||||
t.Fatalf("bitmap label = %q", got)
|
||||
}
|
||||
if got := tracks[1].Label; got != "日本語(内嵌)" {
|
||||
t.Fatalf("text label = %q", got)
|
||||
}
|
||||
// 容器自带 title 时优先用 title。
|
||||
if got := tracks[2].Label; got != "简日双语(内嵌)" {
|
||||
t.Fatalf("titled label = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user