embyhelper.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. package emby_helper
  2. import (
  3. "fmt"
  4. "github.com/allanpk716/ChineseSubFinder/internal/common"
  5. embyHelper "github.com/allanpk716/ChineseSubFinder/internal/pkg/emby_api"
  6. "github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
  7. "github.com/allanpk716/ChineseSubFinder/internal/types"
  8. "github.com/allanpk716/ChineseSubFinder/internal/types/emby"
  9. "github.com/panjf2000/ants/v2"
  10. "golang.org/x/net/context"
  11. "path"
  12. "path/filepath"
  13. "strings"
  14. "sync"
  15. "time"
  16. )
  17. type EmbyHelper struct {
  18. embyApi *embyHelper.EmbyApi
  19. EmbyConfig emby.EmbyConfig
  20. threads int
  21. timeOut time.Duration
  22. listLock sync.Mutex
  23. }
  24. func NewEmbyHelper(embyConfig emby.EmbyConfig) *EmbyHelper {
  25. em := EmbyHelper{EmbyConfig: embyConfig}
  26. em.embyApi = embyHelper.NewEmbyApi(embyConfig)
  27. em.threads = 6
  28. em.timeOut = 60 * time.Second
  29. return &em
  30. }
  31. func (em *EmbyHelper) GetRecentlyAddVideoList(movieRootDir, seriesRootDir string) ([]emby.EmbyMixInfo, map[string][]emby.EmbyMixInfo, error) {
  32. // 获取电影和连续剧的文件夹名称
  33. movieFolderName := filepath.Base(movieRootDir)
  34. seriesFolderName := filepath.Base(seriesRootDir)
  35. var EpisodeIdList = make([]string, 0)
  36. var MovieIdList = make([]string, 0)
  37. // 获取最近的影片列表
  38. items, err := em.embyApi.GetRecentlyItems()
  39. if err != nil {
  40. return nil, nil, err
  41. }
  42. // 分类
  43. for _, item := range items.Items {
  44. if item.Type == "Episode" {
  45. // 这个里面可能混有其他的内容,比如目标是连续剧,但是 emby_helper 其实会把其他的混合内容也标记进去
  46. EpisodeIdList = append(EpisodeIdList, item.Id)
  47. } else if item.Type == "Movie" {
  48. // 这个里面可能混有其他的内容,比如目标是连续剧,但是 emby_helper 其实会把其他的混合内容也标记进去
  49. MovieIdList = append(MovieIdList, item.Id)
  50. }
  51. }
  52. // 过滤出有效的电影、连续剧的资源出来
  53. filterMovieList, err := em.filterEmbyVideoList(movieFolderName, MovieIdList, true)
  54. if err != nil {
  55. return nil, nil, err
  56. }
  57. filterSeriesList, err := em.filterEmbyVideoList(seriesFolderName, EpisodeIdList, false)
  58. if err != nil {
  59. return nil, nil, err
  60. }
  61. // 将没有字幕的找出来
  62. noSubMovieList, err := em.filterNoChineseSubVideoList(filterMovieList)
  63. if err != nil {
  64. return nil, nil, err
  65. }
  66. noSubSeriesList, err := em.filterNoChineseSubVideoList(filterSeriesList)
  67. if err != nil {
  68. return nil, nil, err
  69. }
  70. // 拼接绝对路径
  71. for i, info := range noSubMovieList {
  72. noSubMovieList[i].VideoFileFullPath = path.Join(movieRootDir, info.VideoFileRelativePath)
  73. }
  74. for i, info := range noSubSeriesList {
  75. noSubSeriesList[i].VideoFileFullPath = path.Join(seriesRootDir, info.VideoFileRelativePath)
  76. }
  77. // 需要将连续剧零散的每一集,进行合并到一个连续剧下面,也就是这个连续剧有那些需要更新的
  78. var seriesMap = make(map[string][]emby.EmbyMixInfo)
  79. for _, info := range noSubSeriesList {
  80. _, ok := seriesMap[info.VideoFolderName]
  81. if ok == false {
  82. // 不存在则新建初始化
  83. seriesMap[info.VideoFolderName] = make([]emby.EmbyMixInfo, 0)
  84. }
  85. seriesMap[info.VideoFolderName] = append(seriesMap[info.VideoFolderName], info)
  86. }
  87. return noSubMovieList, seriesMap, nil
  88. }
  89. // RefreshEmbySubList 字幕下载完毕一次,就可以触发一次这个。并发 6 线程去刷新
  90. func (em *EmbyHelper) RefreshEmbySubList() (bool, error) {
  91. if em.embyApi == nil {
  92. return false, nil
  93. }
  94. err := em.embyApi.RefreshRecentlyVideoInfo()
  95. if err != nil {
  96. return false, err
  97. }
  98. return true, nil
  99. }
  100. func (em *EmbyHelper) filterEmbyVideoList(videoFolderName string, videoIdList []string, isMovieOrSeries bool) ([]emby.EmbyMixInfo, error) {
  101. var filterVideoEmbyInfo = make([]emby.EmbyMixInfo, 0)
  102. queryFunc := func(m string) (*emby.EmbyMixInfo, error) {
  103. info, err := em.embyApi.GetItemVideoInfo(m)
  104. if err != nil {
  105. return nil, err
  106. }
  107. ancs, err := em.embyApi.GetItemAncestors(m)
  108. if err != nil {
  109. return nil, err
  110. }
  111. mixInfo := emby.EmbyMixInfo{Ancestors: ancs, VideoInfo: info}
  112. if isMovieOrSeries == true {
  113. // 电影
  114. // 过滤掉不符合要求的
  115. if len(mixInfo.Ancestors) < 2 {
  116. return nil, err
  117. }
  118. // 过滤掉不符合要求的
  119. if mixInfo.Ancestors[0].Name != videoFolderName || mixInfo.Ancestors[0].Type != "Folder" {
  120. return nil, err
  121. }
  122. // 这个电影的文件夹
  123. mixInfo.VideoFolderName = filepath.Base(filepath.Dir(mixInfo.VideoInfo.Path))
  124. mixInfo.VideoFileName = filepath.Base(mixInfo.VideoInfo.Path)
  125. mixInfo.VideoFileRelativePath = filepath.Join(mixInfo.VideoFolderName, mixInfo.VideoFileName)
  126. } else {
  127. // 连续剧
  128. // 过滤掉不符合要求的
  129. if len(mixInfo.Ancestors) < 3 {
  130. return nil, err
  131. }
  132. // 过滤掉不符合要求的
  133. if mixInfo.Ancestors[0].Type != "Season" ||
  134. mixInfo.Ancestors[1].Type != "Series" ||
  135. mixInfo.Ancestors[2].Type != "Folder" ||
  136. mixInfo.Ancestors[2].Name != videoFolderName {
  137. return nil, err
  138. }
  139. // 这个剧集的文件夹
  140. mixInfo.VideoFolderName = filepath.Base(mixInfo.Ancestors[1].Path)
  141. mixInfo.VideoFileName = filepath.Base(mixInfo.VideoInfo.Path)
  142. seasonName := filepath.Base(mixInfo.Ancestors[0].Path)
  143. mixInfo.VideoFileRelativePath = filepath.Join(mixInfo.VideoFolderName, seasonName, mixInfo.VideoFileName)
  144. }
  145. return &mixInfo, nil
  146. }
  147. p, err := ants.NewPoolWithFunc(em.threads, func(inData interface{}) {
  148. data := inData.(InputData)
  149. defer data.Wg.Done()
  150. ctx, cancel := context.WithTimeout(context.Background(), em.timeOut)
  151. defer cancel()
  152. done := make(chan OutData, 1)
  153. panicChan := make(chan interface{}, 1)
  154. go func() {
  155. defer func() {
  156. if p := recover(); p != nil {
  157. panicChan <- p
  158. }
  159. }()
  160. info, err := queryFunc(data.Id)
  161. outData := OutData{
  162. Info: info,
  163. Err: err,
  164. }
  165. done <- outData
  166. }()
  167. select {
  168. case outData := <-done:
  169. // 收到结果,需要加锁
  170. if outData.Err != nil {
  171. log_helper.GetLogger().Errorln("filterEmbyVideoList.NewPoolWithFunc got Err", outData.Err)
  172. return
  173. }
  174. if outData.Info == nil {
  175. return
  176. }
  177. em.listLock.Lock()
  178. filterVideoEmbyInfo = append(filterVideoEmbyInfo, *outData.Info)
  179. em.listLock.Unlock()
  180. return
  181. case p := <-panicChan:
  182. log_helper.GetLogger().Errorln("filterEmbyVideoList.NewPoolWithFunc got panic", p)
  183. case <-ctx.Done():
  184. log_helper.GetLogger().Errorln("filterEmbyVideoList.NewPoolWithFunc got time out", ctx.Err())
  185. return
  186. }
  187. })
  188. if err != nil {
  189. return nil, err
  190. }
  191. defer p.Release()
  192. wg := sync.WaitGroup{}
  193. // 获取视频的 Emby 信息
  194. for _, m := range videoIdList {
  195. wg.Add(1)
  196. err = p.Invoke(InputData{Id: m, Wg: &wg})
  197. if err != nil {
  198. log_helper.GetLogger().Errorln("filterEmbyVideoList ants.Invoke", err)
  199. }
  200. }
  201. wg.Wait()
  202. return filterVideoEmbyInfo, nil
  203. }
  204. func (em *EmbyHelper) filterNoChineseSubVideoList(videoList []emby.EmbyMixInfo) ([]emby.EmbyMixInfo, error) {
  205. currentTime := time.Now()
  206. dayRange3Months, _ := time.ParseDuration(common.DownloadSubDuring3Months)
  207. dayRange7Days, _ := time.ParseDuration(common.DownloadSubDuring7Days)
  208. var noSubVideoList = make([]emby.EmbyMixInfo, 0)
  209. // TODO 这里有一种情况需要考虑的,如果内置有中文的字幕,那么是否需要跳过,目前暂定的一定要有外置的字幕
  210. for _, info := range videoList {
  211. needDlSub3Month := false
  212. // 3个月内,或者没有字幕都要进行下载
  213. if info.VideoInfo.PremiereDate.Add(dayRange3Months).After(currentTime) == true {
  214. // 需要下载的
  215. needDlSub3Month = true
  216. }
  217. // 这个影片只要有一个符合字幕要求的,就可以跳过
  218. // 外置中文字幕
  219. haveExternalChineseSub := false
  220. for _, stream := range info.VideoInfo.MediaStreams {
  221. // 首先找到外置的字幕文件
  222. if stream.IsExternal == true && stream.IsTextSubtitleStream == true && stream.SupportsExternalStream == true {
  223. // 然后字幕的格式以及语言命名要符合本程序的定义,有字幕
  224. if em.subTypeStringOK(stream.Codec) == true && em.langStringOK(stream.Language) == true {
  225. haveExternalChineseSub = true
  226. break
  227. } else {
  228. continue
  229. }
  230. }
  231. }
  232. // 内置中文字幕
  233. haveInsideChineseSub := false
  234. for _, stream := range info.VideoInfo.MediaStreams {
  235. if stream.IsExternal == false && (stream.Language == "chi" || stream.Language == "cht" || stream.Language == "chs") {
  236. haveInsideChineseSub = true
  237. break
  238. }
  239. }
  240. // 比如,创建的时间在3个月内,然后没有额外下载的中文字幕,都符合要求
  241. if haveExternalChineseSub == false {
  242. // 如果创建了7天,且有内置的中文字幕,那么也不进行下载了
  243. if info.VideoInfo.DateCreated.Add(dayRange7Days).After(currentTime) == false && haveInsideChineseSub == true {
  244. continue
  245. }
  246. //// 如果创建了三个月,还是没有字幕,那么也不进行下载了
  247. //if info.VideoInfo.DateCreated.Add(dayRange3Months).After(currentTime) == false {
  248. // continue
  249. //}
  250. // 没有中文字幕就加入下载列表
  251. noSubVideoList = append(noSubVideoList, info)
  252. } else {
  253. // 如果视频发布时间超过两年了,有字幕就直接跳过了,一般字幕稳定了
  254. if currentTime.Year()-2 > info.VideoInfo.PremiereDate.Year() {
  255. continue
  256. }
  257. // 有中文字幕,且如果在三个月内,则需要继续下载字幕`
  258. if needDlSub3Month == true {
  259. noSubVideoList = append(noSubVideoList, info)
  260. }
  261. }
  262. }
  263. return noSubVideoList, nil
  264. }
  265. // GetInternalEngSubAndExChineseEnglishSub 获取对应 videoId 的内置英文字幕,外置中(简体、繁体)英字幕
  266. func (em *EmbyHelper) GetInternalEngSubAndExChineseEnglishSub(videoId string) (bool, []emby.SubInfo, []emby.SubInfo, error) {
  267. // 先刷新以下这个资源,避免找到的字幕不存在了
  268. err := em.embyApi.UpdateVideoSubList(videoId)
  269. if err != nil {
  270. return false, nil, nil, err
  271. }
  272. // 获取这个资源的信息
  273. videoInfo, err := em.embyApi.GetItemVideoInfo(videoId)
  274. if err != nil {
  275. return false, nil, nil, err
  276. }
  277. // 获取 MediaSources ID,这里强制使用第一个视频源(因为 emby 运行有多个版本的视频指向到一个视频ID上,比如一个 web 一个 蓝光)
  278. mediaSourcesId := videoInfo.MediaSources[0].Id
  279. // 视频文件名称带后缀名
  280. videoFileName := filepath.Base(videoInfo.Path)
  281. videoFileNameWithOutExt := strings.ReplaceAll(videoFileName, path.Ext(videoFileName), "")
  282. // TODO 后续会新增一个功能,从视频中提取音频文件,然后识别转为字符,再进行与字幕的匹配
  283. // 获取是否有内置的英文字幕,如果没有则无需继续往下
  284. /*
  285. 这里有个梗,读取到的英文内置字幕很可能是残缺的,比如,基地 S01E04 Eng 第一个 Default Forced Sub,就不对,内容的 Dialogue 很少。
  286. 然后第二个 Eng 字幕才对。那么考虑到兼容性, 可能后续有短视频,也就不能简单的按 Dialogue 的多少去衡量。大概会做一个功能。
  287. 读取到视频的总长度,然后再分析 Dialogue 的时间出现的部分与整体时间轴的占比,又或者是 Dialogue 之间的连续成都分析,这个有待测试。
  288. */
  289. haveInsideEngSub := false
  290. InsideEngSubIndex := 0
  291. for _, stream := range videoInfo.MediaStreams {
  292. if stream.IsExternal == false && stream.Language == "eng" && stream.Codec == "subrip" {
  293. haveInsideEngSub = true
  294. InsideEngSubIndex = stream.Index
  295. break
  296. }
  297. }
  298. // 没有找到则跳过
  299. if haveInsideEngSub == false {
  300. return false, nil, nil, nil
  301. }
  302. // 再内置英文字幕能找到的前提下,就可以先找中文的外置字幕,目前版本只能考虑双语字幕
  303. // 内置英文字幕,这里把 srt 和 ass 的都导出来
  304. var inSubList = make([]emby.SubInfo, 0)
  305. // 外置中文双语字幕
  306. var exSubList = make([]emby.SubInfo, 0)
  307. tmpFileNameWithOutExt := ""
  308. for _, stream := range videoInfo.MediaStreams {
  309. // 首先找到外置的字幕文件
  310. if stream.IsExternal == true && stream.IsTextSubtitleStream == true && stream.SupportsExternalStream == true {
  311. // 然后字幕的格式以及语言命名要符合本程序的定义,有字幕
  312. if em.subTypeStringOK(stream.Codec) == true &&
  313. em.langStringOK(stream.Language) == true &&
  314. // 只支持 简英、繁英
  315. (strings.Contains(stream.Language, types.MatchLangChsEn) == true || strings.Contains(stream.Language, types.MatchLangChtEn) == true) {
  316. tmpFileName := filepath.Base(stream.Path)
  317. // 去除 .default 或者 .forced
  318. tmpFileName = strings.ReplaceAll(tmpFileName, types.Sub_Ext_Mark_Default, "")
  319. tmpFileName = strings.ReplaceAll(tmpFileName, types.Sub_Ext_Mark_Forced, "")
  320. tmpFileNameWithOutExt = strings.ReplaceAll(tmpFileName, path.Ext(tmpFileName), "")
  321. exSubList = append(exSubList, *emby.NewSubInfo(tmpFileNameWithOutExt+"."+stream.Codec, "."+stream.Codec, stream.Index))
  322. } else {
  323. continue
  324. }
  325. }
  326. }
  327. // 没有找到则跳过
  328. if len(exSubList) == 0 {
  329. return false, nil, nil, nil
  330. }
  331. // 把之前 Internal 英文字幕的 SubInfo 实例的信息补充完整
  332. // 但是也不是绝对的,因为后续去 emby 下载字幕的时候,需要与外置字幕的后缀名一致
  333. // 这里开始去下载字幕
  334. // 先下载内置的文的
  335. for i := 0; i < 2; i++ {
  336. tmpExt := common.SubExtSRT
  337. if i == 1 {
  338. tmpExt = common.SubExtASS
  339. }
  340. subFileData, err := em.embyApi.GetSubFileData(videoId, mediaSourcesId, fmt.Sprintf("%d", InsideEngSubIndex), tmpExt)
  341. if err != nil {
  342. return false, nil, nil, err
  343. }
  344. tmpInSubInfo := emby.NewSubInfo(videoFileNameWithOutExt+tmpExt, tmpExt, InsideEngSubIndex)
  345. tmpInSubInfo.Content = []byte(subFileData)
  346. inSubList = append(inSubList, *tmpInSubInfo)
  347. }
  348. // 再下载外置的
  349. for i, subInfo := range exSubList {
  350. subFileData, err := em.embyApi.GetSubFileData(videoId, mediaSourcesId, fmt.Sprintf("%d", subInfo.EmbyStreamIndex), subInfo.Ext)
  351. if err != nil {
  352. return false, nil, nil, err
  353. }
  354. exSubList[i].Content = []byte(subFileData)
  355. }
  356. return true, inSubList, exSubList, nil
  357. }
  358. // langStringOK 从 Emby api 拿到字幕的 Language string是否是符合本程序要求的
  359. func (em *EmbyHelper) langStringOK(inLang string) bool {
  360. tmpString := strings.ToLower(inLang)
  361. nextString := tmpString
  362. // 去除 [xunlie] 类似的标记
  363. spStrings := strings.Split(tmpString, "[")
  364. if len(spStrings) > 1 {
  365. nextString = spStrings[0]
  366. } else {
  367. spStrings = strings.Split(tmpString, "(")
  368. if len(spStrings) > 1 {
  369. nextString = spStrings[0]
  370. }
  371. }
  372. switch nextString {
  373. case em.replaceLangString(types.Emby_chi),
  374. em.replaceLangString(types.Emby_chn),
  375. em.replaceLangString(types.Emby_chs),
  376. em.replaceLangString(types.Emby_cht),
  377. em.replaceLangString(types.Emby_chs_en),
  378. em.replaceLangString(types.Emby_cht_en),
  379. em.replaceLangString(types.Emby_chs_jp),
  380. em.replaceLangString(types.Emby_cht_jp),
  381. em.replaceLangString(types.Emby_chs_kr),
  382. em.replaceLangString(types.Emby_cht_kr):
  383. return true
  384. case em.replaceLangString(types.Emby_chinese):
  385. return true
  386. default:
  387. return false
  388. }
  389. }
  390. // subTypeStringOK 从 Emby api 拿到字幕的 sub 类型 string (Codec) 是否是符合本程序要求的
  391. func (em *EmbyHelper) subTypeStringOK(inSubType string) bool {
  392. tmpString := strings.ToLower(inSubType)
  393. if tmpString == common.SubTypeSRT ||
  394. tmpString == common.SubTypeASS ||
  395. tmpString == common.SubTypeSSA {
  396. return true
  397. }
  398. return false
  399. }
  400. func (em *EmbyHelper) replaceLangString(inString string) string {
  401. tmpString := strings.ToLower(inString)
  402. one := strings.ReplaceAll(tmpString, ".", "")
  403. two := strings.ReplaceAll(one, "_", "")
  404. return two
  405. }
  406. type InputData struct {
  407. Id string
  408. Wg *sync.WaitGroup
  409. }
  410. type OutData struct {
  411. Info *emby.EmbyMixInfo
  412. Err error
  413. }