Browse Source

时间轴修复功能,正在对接 emby api

Signed-off-by: allan716 <[email protected]>
allan716 4 years ago
parent
commit
d76baaa185

+ 3 - 0
.gitignore

@@ -44,3 +44,6 @@
 /internal/pkg/sub_timeline_fixer/bar.html
 /internal/pkg/sub_timeline_fixer/*.srt
 /internal/pkg/sub_timeline_fixer/*.ass
+/internal/logic/sub_timeline_fixer/config.yaml
+/internal/pkg/emby_api/config.yaml
+

+ 97 - 4
internal/logic/emby_helper/embyhelper.go

@@ -1,8 +1,9 @@
 package emby_helper
 
 import (
+	"fmt"
 	"github.com/allanpk716/ChineseSubFinder/internal/common"
-	embyHelper "github.com/allanpk716/ChineseSubFinder/internal/pkg/emby_helper"
+	embyHelper "github.com/allanpk716/ChineseSubFinder/internal/pkg/emby_api"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
 	"github.com/allanpk716/ChineseSubFinder/internal/types"
 	"github.com/allanpk716/ChineseSubFinder/internal/types/emby"
@@ -25,7 +26,7 @@ type EmbyHelper struct {
 
 func NewEmbyHelper(embyConfig emby.EmbyConfig) *EmbyHelper {
 	em := EmbyHelper{EmbyConfig: embyConfig}
-	em.embyApi = embyHelper.NewEmbyHelper(embyConfig)
+	em.embyApi = embyHelper.NewEmbyApi(embyConfig)
 	em.threads = 6
 	em.timeOut = 60 * time.Second
 	return &em
@@ -233,7 +234,7 @@ func (em *EmbyHelper) filterNoChineseSubVideoList(videoList []emby.EmbyMixInfo)
 			needDlSub3Month = true
 		}
 		// 这个影片只要有一个符合字幕要求的,就可以跳过
-		// 外置字幕
+		// 外置中文字幕
 		haveExternalChineseSub := false
 		for _, stream := range info.VideoInfo.MediaStreams {
 			// 首先找到外置的字幕文件
@@ -247,7 +248,7 @@ func (em *EmbyHelper) filterNoChineseSubVideoList(videoList []emby.EmbyMixInfo)
 				}
 			}
 		}
-		// 内置字幕
+		// 内置中文字幕
 		haveInsideChineseSub := false
 		for _, stream := range info.VideoInfo.MediaStreams {
 			if stream.IsExternal == false && (stream.Language == "chi" || stream.Language == "cht" || stream.Language == "chs") {
@@ -282,6 +283,98 @@ func (em *EmbyHelper) filterNoChineseSubVideoList(videoList []emby.EmbyMixInfo)
 	return noSubVideoList, nil
 }
 
+// GetInternalEngSubAndExChineseEnglishSub 获取对应 videoId 的内置英文字幕,外置中(简体、繁体)英字幕
+func (em *EmbyHelper) GetInternalEngSubAndExChineseEnglishSub(videoId string) (bool, []emby.SubInfo, []emby.SubInfo, error) {
+
+	// 先刷新以下这个资源,避免找到的字幕不存在了
+	err := em.embyApi.UpdateVideoSubList(videoId)
+	if err != nil {
+		return false, nil, nil, err
+	}
+	// 获取这个资源的信息
+	videoInfo, err := em.embyApi.GetItemVideoInfo(videoId)
+	if err != nil {
+		return false, nil, nil, err
+	}
+	// 获取 MediaSources ID,这里强制使用第一个视频源(因为 emby 运行有多个版本的视频指向到一个视频ID上,比如一个 web 一个 蓝光)
+	mediaSourcesId := videoInfo.MediaSources[0].Id
+	// 视频文件名称带后缀名
+	videoFileName := filepath.Base(videoInfo.Path)
+	videoFileNameWithOutExt := strings.ReplaceAll(videoFileName, path.Ext(videoFileName), "")
+	// TODO 后续会新增一个功能,从视频中提取音频文件,然后识别转为字符,再进行与字幕的匹配
+	// 获取是否有内置的英文字幕,如果没有则无需继续往下
+	haveInsideEngSub := false
+	InsideEngSubIndex := 0
+	for _, stream := range videoInfo.MediaStreams {
+		if stream.IsExternal == false && stream.Language == "eng" && stream.Codec == "subrip" {
+			haveInsideEngSub = true
+			InsideEngSubIndex = stream.Index
+			break
+		}
+	}
+	// 没有找到则跳过
+	if haveInsideEngSub == false {
+		return false, nil, nil, nil
+	}
+	// 再内置英文字幕能找到的前提下,就可以先找中文的外置字幕,目前版本只能考虑双语字幕
+	// 内置英文字幕,这里把 srt 和 ass 的都导出来
+	var inSubList = make([]emby.SubInfo, 0)
+	// 外置中文双语字幕
+	var exSubList = make([]emby.SubInfo, 0)
+	tmpFileNameWithOutExt := ""
+	for _, stream := range videoInfo.MediaStreams {
+		// 首先找到外置的字幕文件
+		if stream.IsExternal == true && stream.IsTextSubtitleStream == true && stream.SupportsExternalStream == true {
+			// 然后字幕的格式以及语言命名要符合本程序的定义,有字幕
+			if em.subTypeStringOK(stream.Codec) == true &&
+				em.langStringOK(stream.Language) == true &&
+				// 只支持 简英、繁英
+				(strings.Contains(stream.Language, types.MatchLangChsEn) == true || strings.Contains(stream.Language, types.MatchLangChtEn) == true) {
+
+				tmpFileName := filepath.Base(stream.Path)
+				// 去除 .default 或者 .forced
+				tmpFileName = strings.ReplaceAll(tmpFileName, types.Sub_Ext_Mark_Default, "")
+				tmpFileName = strings.ReplaceAll(tmpFileName, types.Sub_Ext_Mark_Forced, "")
+				tmpFileNameWithOutExt = strings.ReplaceAll(tmpFileName, path.Ext(tmpFileName), "")
+				exSubList = append(exSubList, *emby.NewSubInfo(tmpFileNameWithOutExt, "."+stream.Codec, stream.Index))
+			} else {
+				continue
+			}
+		}
+	}
+	// 没有找到则跳过
+	if len(exSubList) == 0 {
+		return false, nil, nil, nil
+	}
+	// 把之前 Internal 英文字幕的 SubInfo 实例的信息补充完整
+	// 但是也不是绝对的,因为后续去 emby 下载字幕的时候,需要与外置字幕的后缀名一致
+	// 这里开始去下载字幕
+	// 先下载内置的文的
+	for i := 0; i < 2; i++ {
+		tmpExt := common.SubExtSRT
+		if i == 1 {
+			tmpExt = common.SubExtASS
+		}
+		subFileData, err := em.embyApi.GetSubFileData(videoId, mediaSourcesId, fmt.Sprintf("%d", InsideEngSubIndex), tmpExt)
+		if err != nil {
+			return false, nil, nil, err
+		}
+		tmpInSubInfo := emby.NewSubInfo(videoFileNameWithOutExt, tmpExt, InsideEngSubIndex)
+		tmpInSubInfo.Content = []byte(subFileData)
+		inSubList = append(inSubList, *tmpInSubInfo)
+	}
+	// 再下载外置的
+	for i, subInfo := range exSubList {
+		subFileData, err := em.embyApi.GetSubFileData(videoId, mediaSourcesId, fmt.Sprintf("%d", subInfo.EmbyStreamIndex), subInfo.Ext)
+		if err != nil {
+			return false, nil, nil, err
+		}
+		exSubList[i].Content = []byte(subFileData)
+	}
+
+	return true, inSubList, exSubList, nil
+}
+
 // langStringOK 从 Emby api 拿到字幕的 Language string是否是符合本程序要求的
 func (em *EmbyHelper) langStringOK(inLang string) bool {
 

+ 17 - 0
internal/logic/emby_helper/embyhelper_test.go

@@ -25,3 +25,20 @@ func TestEmbyHelper_RefreshEmbySubList(t *testing.T) {
 	}
 	println(bok)
 }
+
+func TestEmbyHelper_GetInternalEngSubAndExSub(t *testing.T) {
+	config := pkg.GetConfig()
+	em := NewEmbyHelper(config.EmbyConfig)
+	// 81873 -- R&M - S05E01
+	// R&M S05E10  2 org english, 5 简英 	145499
+	// 基地 S01E03 							166840
+	found, internalEngSub, exCh_EngSub, err := em.GetInternalEngSubAndExChineseEnglishSub("166840")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if found == false {
+		t.Fatal("need found sub")
+	}
+
+	println(internalEngSub[0].FileName, exCh_EngSub[0].FileName)
+}

+ 149 - 0
internal/logic/sub_timeline_fixer/sub_timeline_fixer_helper.go

@@ -0,0 +1,149 @@
+package sub_timeline_fixer
+
+import (
+	"github.com/allanpk716/ChineseSubFinder/internal/common"
+	"github.com/allanpk716/ChineseSubFinder/internal/logic/emby_helper"
+	"github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser/ass"
+	"github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser/srt"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg"
+	embyHelper "github.com/allanpk716/ChineseSubFinder/internal/pkg/emby_api"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_parser_hub"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_timeline_fixer"
+	"github.com/allanpk716/ChineseSubFinder/internal/types/emby"
+	"os"
+	"path"
+	"time"
+)
+
+type SubTimelineFixerHelper struct {
+	embyApi      *embyHelper.EmbyApi
+	embyHelper   *emby_helper.EmbyHelper
+	EmbyConfig   emby.EmbyConfig
+	subParserHub *sub_parser_hub.SubParserHub
+	threads      int
+	timeOut      time.Duration
+}
+
+func NewSubTimelineFixerHelper(embyConfig emby.EmbyConfig) *SubTimelineFixerHelper {
+	sub := SubTimelineFixerHelper{
+		EmbyConfig:   embyConfig,
+		embyHelper:   emby_helper.NewEmbyHelper(embyConfig),
+		embyApi:      embyHelper.NewEmbyApi(embyConfig),
+		subParserHub: sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser()),
+		threads:      6,
+		timeOut:      60 * time.Second,
+	}
+	return &sub
+}
+
+func (s SubTimelineFixerHelper) FixRecentlyItemsSubTimeline() error {
+
+	items, err := s.embyApi.GetRecentlyItems()
+	if err != nil {
+		return err
+	}
+	for _, item := range items.Items {
+		err = s.fixOneVideoSub(item.Id)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (s SubTimelineFixerHelper) fixOneVideoSub(videoId string) error {
+	// internalEngSub 默认第一个是 srt 然后第二个是 ass,就不要去遍历了
+	found, internalEngSub, exCh_EngSub, err := s.embyHelper.GetInternalEngSubAndExChineseEnglishSub(videoId)
+	if err != nil {
+		return err
+	}
+	if found == false {
+		return nil
+	}
+	// 从外置双语(中英)字幕中找对对应的内置 srt 字幕进行匹配比较
+	for _, exSubInfo := range exCh_EngSub {
+		inSelectSubIndex := 1
+		if exSubInfo.Ext == common.SubExtSRT {
+			inSelectSubIndex = 0
+		}
+
+		bFound, err := s.fixSubTimeline(internalEngSub[inSelectSubIndex], exSubInfo)
+		if err != nil {
+			return err
+		}
+		if bFound == false {
+			continue
+		}
+	}
+
+	return nil
+}
+
+func (s SubTimelineFixerHelper) fixSubTimeline(enSubFile emby.SubInfo, ch_enSubFile emby.SubInfo) (bool, error) {
+
+	bFind, infoBase, err := s.subParserHub.DetermineFileTypeFromBytes(enSubFile.Content, enSubFile.Ext)
+	if err != nil {
+		return false, err
+	}
+	if bFind == false {
+		return false, nil
+	}
+	infoBase.Name = enSubFile.FileName
+	bFind, infoSrc, err := s.subParserHub.DetermineFileTypeFromBytes(ch_enSubFile.Content, ch_enSubFile.Ext)
+	if err != nil {
+		return false, err
+	}
+	if bFind == false {
+		return false, nil
+	}
+	infoSrc.Name = ch_enSubFile.FileName
+
+	// 把原始的文件缓存下来
+	if pkg.IsDir(infoBase.Name) == false {
+		err = os.MkdirAll(infoBase.Name, os.ModePerm)
+		if err != nil {
+			return false, err
+		}
+	}
+	offsetTime, err := sub_timeline_fixer.GetOffsetTime(infoBase, infoSrc, path.Join(infoBase.Name, "bar.html"))
+	if err != nil {
+		return false, err
+	}
+	// 偏移很小就无视了
+	if offsetTime < 0.2 {
+		_ = pkg.ClearFolder(infoBase.Name)
+		return false, nil
+	}
+
+	err = s.saveOrgSubFile(path.Join(infoBase.Name, infoBase.Name+infoBase.Ext), infoBase.Content)
+	if err != nil {
+		return false, err
+	}
+	err = s.saveOrgSubFile(path.Join(infoBase.Name, infoSrc.Name+infoSrc.Ext), infoSrc.Content)
+	if err != nil {
+		return false, err
+	}
+	err = sub_timeline_fixer.FixSubTimeline(infoSrc, offsetTime, path.Join(infoBase.Name, infoBase.Name+".chinese(fix)"+ch_enSubFile.Ext))
+	if err != nil {
+		return false, err
+	}
+
+	return true, nil
+}
+
+func (s SubTimelineFixerHelper) saveOrgSubFile(desSaveSubFileFullPath string, content string) error {
+	dstFile, err := os.Create(desSaveSubFileFullPath)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		_ = dstFile.Close()
+	}()
+	_, err = dstFile.WriteString(content)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 26 - 0
internal/logic/sub_timeline_fixer/sub_timeline_fixer_helper_test.go

@@ -0,0 +1,26 @@
+package sub_timeline_fixer
+
+import (
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg"
+	"testing"
+)
+
+func TestSubTimelineFixerHelper_FixRecentlyItemsSubTimeline(t *testing.T) {
+	config := pkg.GetConfig()
+	fixer := NewSubTimelineFixerHelper(config.EmbyConfig)
+	err := fixer.FixRecentlyItemsSubTimeline()
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestSubTimelineFixerHelper_fixOneVideoSub(t *testing.T) {
+	// What If  - S01E09    171499
+	// What If  - S01E09    172412
+	config := pkg.GetConfig()
+	fixer := NewSubTimelineFixerHelper(config.EmbyConfig)
+	err := fixer.fixOneVideoSub("171499")
+	if err != nil {
+		t.Fatal(err)
+	}
+}

+ 3 - 3
internal/pkg/emby_helper/emby.go → internal/pkg/emby_api/emby_api.go

@@ -1,4 +1,4 @@
-package emby_helper
+package emby_api
 
 import (
 	"fmt"
@@ -18,7 +18,7 @@ type EmbyApi struct {
 	timeOut    time.Duration
 }
 
-func NewEmbyHelper(embyConfig emby.EmbyConfig) *EmbyApi {
+func NewEmbyApi(embyConfig emby.EmbyConfig) *EmbyApi {
 	em := EmbyApi{}
 	em.embyConfig = embyConfig
 	if em.embyConfig.LimitCount < common.EmbyApiGetItemsLimitMin ||
@@ -27,7 +27,7 @@ func NewEmbyHelper(embyConfig emby.EmbyConfig) *EmbyApi {
 		em.embyConfig.LimitCount = common.EmbyApiGetItemsLimitMin
 	}
 	em.threads = 6
-	em.timeOut = 60 * time.Second
+	em.timeOut = 5 * 60 * time.Second
 	return &em
 }
 

+ 11 - 10
internal/pkg/emby_helper/emby_test.go → internal/pkg/emby_api/emby_api_test.go

@@ -1,4 +1,4 @@
-package emby_helper
+package emby_api
 
 import (
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg"
@@ -7,7 +7,7 @@ import (
 
 func TestEmbyHelper_GetRecentlyItems(t *testing.T) {
 
-	em := NewEmbyHelper(pkg.GetConfig().EmbyConfig)
+	em := NewEmbyApi(pkg.GetConfig().EmbyConfig)
 	items, err := em.GetRecentlyItems()
 	if err != nil {
 		t.Fatal(err)
@@ -19,7 +19,7 @@ func TestEmbyHelper_GetRecentlyItems(t *testing.T) {
 }
 
 func TestEmbyHelper_GetItemsAncestors(t *testing.T) {
-	em := NewEmbyHelper(pkg.GetConfig().EmbyConfig)
+	em := NewEmbyApi(pkg.GetConfig().EmbyConfig)
 	items, err := em.GetItemAncestors("96564")
 	if err != nil {
 		t.Fatal(err)
@@ -33,12 +33,13 @@ func TestEmbyHelper_GetItemsAncestors(t *testing.T) {
 }
 
 func TestEmbyHelper_GetItemVideoInfo(t *testing.T) {
-	em := NewEmbyHelper(pkg.GetConfig().EmbyConfig)
+	em := NewEmbyApi(pkg.GetConfig().EmbyConfig)
 	// 95813 -- 命运夜
 	// 96564 -- The Bad Batch - S01E11
 	// R&M S05E10  2 org english, 5 简英 145499
 	// 基地 S01E03 166840
-	videoInfo, err := em.GetItemVideoInfo("145499")
+	// 算牌人 166837
+	videoInfo, err := em.GetItemVideoInfo("166837")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -47,7 +48,7 @@ func TestEmbyHelper_GetItemVideoInfo(t *testing.T) {
 }
 
 func TestEmbyHelper_GetItemVideoInfoByUserId(t *testing.T) {
-	em := NewEmbyHelper(pkg.GetConfig().EmbyConfig)
+	em := NewEmbyApi(pkg.GetConfig().EmbyConfig)
 	// 95813 -- 命运夜
 	// 96564 -- The Bad Batch - S01E11
 	// 108766 -- R&M - S05E06
@@ -61,7 +62,7 @@ func TestEmbyHelper_GetItemVideoInfoByUserId(t *testing.T) {
 }
 
 func TestEmbyHelper_UpdateVideoSubList(t *testing.T) {
-	em := NewEmbyHelper(pkg.GetConfig().EmbyConfig)
+	em := NewEmbyApi(pkg.GetConfig().EmbyConfig)
 	// 95813 -- 命运夜
 	// 96564 -- The Bad Batch - S01E11
 	// 81873 -- R&M - S05E01
@@ -74,7 +75,7 @@ func TestEmbyHelper_UpdateVideoSubList(t *testing.T) {
 }
 
 func TestEmbyHelper_GetUserIdList(t *testing.T) {
-	em := NewEmbyHelper(pkg.GetConfig().EmbyConfig)
+	em := NewEmbyApi(pkg.GetConfig().EmbyConfig)
 	userIds, err := em.GetUserIdList()
 	if err != nil {
 		t.Fatal(err)
@@ -85,7 +86,7 @@ func TestEmbyHelper_GetUserIdList(t *testing.T) {
 }
 
 func TestEmbyApi_GetSubFileData(t *testing.T) {
-	em := NewEmbyHelper(pkg.GetConfig().EmbyConfig)
+	em := NewEmbyApi(pkg.GetConfig().EmbyConfig)
 	// R&M S05E10  2 org english, 5 简英					"145499", "c4678509adb72a8b5034bdac2f1fccde", "5", ".ass"
 	// 基地 S01E03		2=eng 	6=chi 	45=简英			"166840", "d6c68ec6097aeceb9f5c1d82add66213", "2", ".ass"
 
@@ -101,7 +102,7 @@ func TestEmbyApi_GetSubFileData(t *testing.T) {
 
 func TestEmbyApi_RefreshRecentlyVideoInfo(t *testing.T) {
 
-	em := NewEmbyHelper(pkg.GetConfig().EmbyConfig)
+	em := NewEmbyApi(pkg.GetConfig().EmbyConfig)
 	err := em.RefreshRecentlyVideoInfo()
 	if err != nil {
 		t.Fatal("RefreshRecentlyVideoInfo() error = " + err.Error())

+ 17 - 8
internal/pkg/sub_timeline_fixer/fixer.go

@@ -47,9 +47,6 @@ func StopWordCounter(inString string, per int) []string {
 // GetOffsetTime 暂时只支持英文的基准字幕,源字幕必须是双语中英字幕
 func GetOffsetTime(infoBase, infoSrc *subparser.FileInfo, staticLineFileSavePath string) (float64, error) {
 
-	if staticLineFileSavePath == "" {
-		staticLineFileSavePath = "bar.html"
-	}
 	// 构建基准语料库,目前阶段只需要考虑是 En 的就行了
 	var baseCorpus = make([]string, 0)
 	for _, oneDialogueEx := range infoBase.DialoguesEx {
@@ -127,6 +124,14 @@ func GetOffsetTime(infoBase, infoSrc *subparser.FileInfo, staticLineFileSavePath
 		//	baseIndex, infoBase.DialoguesEx[baseIndex].StartTime, infoBase.DialoguesEx[baseIndex].EndTime, baseCorpus[baseIndex],
 		//	srcIndex, srcOneDialogueEx.StartTime, srcOneDialogueEx.EndTime, srcOneDialogueEx.EnLine))
 	}
+
+	// 这里需要考虑,找到的连续 5 句话匹配的有多少句,占比整体所有的 Dialogue 是多少,太低也需要跳过
+	matchIndexLineCount := len(matchIndexList) * maxCompareDialogue
+	perMatch := float64(matchIndexLineCount) / float64(len(infoSrc.DialoguesEx))
+	if perMatch > 0.5 {
+
+	}
+
 	timeFormat := ""
 	if infoBase.Ext == common.SubExtASS || infoBase.Ext == common.SubExtSSA {
 		timeFormat = timeFormatAss
@@ -233,11 +238,15 @@ func GetOffsetTime(infoBase, infoSrc *subparser.FileInfo, staticLineFileSavePath
 		newSd = oldSd
 	}
 
-	err = SaveStaticLine(staticLineFileSavePath, infoBase.Name, infoSrc.Name,
-		per, oldMean, oldSd, newMean, newSd, xAxis,
-		startDiffTimeLineData, endDiffTimeLineData)
-	if err != nil {
-		return 0, err
+	// 不为空的时候,生成调试文件
+	if staticLineFileSavePath != "" {
+		//staticLineFileSavePath = "bar.html"
+		err = SaveStaticLine(staticLineFileSavePath, infoBase.Name, infoSrc.Name,
+			per, oldMean, oldSd, newMean, newSd, xAxis,
+			startDiffTimeLineData, endDiffTimeLineData)
+		if err != nil {
+			return 0, err
+		}
 	}
 
 	return newMean, nil

+ 4 - 4
internal/pkg/sub_timeline_fixer/fixer_test.go

@@ -47,15 +47,15 @@ func TestGetOffsetTime(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	//enSubFile := path.Join(testRootDir, "R&M S05E01 - English.srt")
-	//ch_enSubFile := path.Join(testRootDir, "R&M S05E01 - 简英.srt")
+	enSubFile := path.Join(testRootDir, "R&M S05E01 - English.srt")
+	ch_enSubFile := path.Join(testRootDir, "R&M S05E01 - 简英.srt")
 
 	//enSubFile := path.Join(testRootDir, "R&M S05E10 - English.ass")
 	//ch_enSubFile := path.Join(testRootDir, "R&M S05E10 - 简英.ass")
 	//ch_enSubFile := path.Join(testRootDir, "R&M S05E10 - 简英-shooter.ass")
 
-	enSubFile := path.Join(testRootDir, "基地 S01E03 - English.ass")
-	ch_enSubFile := path.Join(testRootDir, "基地 S01E03 - 简英.ass")
+	//enSubFile := path.Join(testRootDir, "基地 S01E03 - English.ass")
+	//ch_enSubFile := path.Join(testRootDir, "基地 S01E03 - 简英.ass")
 
 	subParserHub := sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser())
 	bFind, infoBase, err := subParserHub.DetermineFileTypeFromFile(enSubFile)

+ 17 - 0
internal/types/emby/sub_info.go

@@ -0,0 +1,17 @@
+package emby
+
+type SubInfo struct {
+	FileName        string // 文件名称
+	Content         []byte // 文件的内容
+	Ext             string // 文件的后缀名
+	EmbyStreamIndex int    // 在 Emby Stream 中的索引
+}
+
+func NewSubInfo(fileName, ext string, embyIndex int) *SubInfo {
+	sub := SubInfo{
+		FileName:        fileName,
+		Ext:             ext,
+		EmbyStreamIndex: embyIndex,
+	}
+	return &sub
+}