embyhelper.go 18 KB

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