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
|
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 {
|
func discoverExternalSubtitleTracks(m *model.Media) []SubtitleTrack {
|
||||||
dir := filepath.Dir(m.Path)
|
dir := filepath.Dir(m.Path)
|
||||||
bases := mediaSidecarBaseVariants(m.Path)
|
bases := mediaSidecarBaseVariants(m.Path)
|
||||||
@@ -228,9 +299,13 @@ func discoverExternalSubtitleTracks(m *model.Media) []SubtitleTrack {
|
|||||||
matchedBase = bases[0]
|
matchedBase = bases[0]
|
||||||
}
|
}
|
||||||
lang := detectLang(fullName, matchedBase)
|
lang := detectLang(fullName, matchedBase)
|
||||||
|
label := subtitleDisplayLabel(lang)
|
||||||
|
if isDanmakuSubtitle(e.Name()) {
|
||||||
|
label += " · 弹幕"
|
||||||
|
}
|
||||||
tracks = append(tracks, SubtitleTrack{
|
tracks = append(tracks, SubtitleTrack{
|
||||||
Lang: lang,
|
Lang: lang,
|
||||||
Label: lang,
|
Label: label,
|
||||||
Path: filepath.Join(c, e.Name()),
|
Path: filepath.Join(c, e.Name()),
|
||||||
Codec: codec,
|
Codec: codec,
|
||||||
Source: "external",
|
Source: "external",
|
||||||
@@ -319,7 +394,11 @@ func subtitleTracksFromProbe(probe embeddedSubtitleProbe) []SubtitleTrack {
|
|||||||
}
|
}
|
||||||
label := strings.TrimSpace(stream.Tags.Title)
|
label := strings.TrimSpace(stream.Tags.Title)
|
||||||
if label == "" {
|
if label == "" {
|
||||||
label = lang
|
// 没有 title 标签时用可读语言名(chi → 中文)代替裸语言码。
|
||||||
|
label = subtitleDisplayLabel(lang)
|
||||||
|
}
|
||||||
|
if label == "" {
|
||||||
|
label = "字幕"
|
||||||
}
|
}
|
||||||
if stream.Disposition.Forced != 0 {
|
if stream.Disposition.Forced != 0 {
|
||||||
label += "(强制)"
|
label += "(强制)"
|
||||||
|
|||||||
@@ -202,3 +202,103 @@ func TestSubtitleServeRawWritesSourceBytes(t *testing.T) {
|
|||||||
t.Fatal("ServeRaw accepted a non-subtitle file")
|
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