embyhelper.go 16 KB

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