embyhelper.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873
  1. package emby_helper
  2. import (
  3. "fmt"
  4. embyHelper "github.com/allanpk716/ChineseSubFinder/internal/pkg/emby_api"
  5. "github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
  6. "github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
  7. "github.com/allanpk716/ChineseSubFinder/internal/pkg/path_helper"
  8. "github.com/allanpk716/ChineseSubFinder/internal/pkg/settings"
  9. "github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_parser_hub"
  10. common2 "github.com/allanpk716/ChineseSubFinder/internal/types/common"
  11. "github.com/allanpk716/ChineseSubFinder/internal/types/emby"
  12. "github.com/allanpk716/ChineseSubFinder/internal/types/language"
  13. "github.com/panjf2000/ants/v2"
  14. "golang.org/x/net/context"
  15. "path"
  16. "path/filepath"
  17. "sort"
  18. "strings"
  19. "sync"
  20. "time"
  21. )
  22. type EmbyHelper struct {
  23. embyApi *embyHelper.EmbyApi
  24. settings *settings.Settings
  25. threads int
  26. timeOut time.Duration
  27. listLock sync.Mutex
  28. }
  29. func NewEmbyHelper(_settings *settings.Settings) *EmbyHelper {
  30. em := EmbyHelper{settings: _settings}
  31. em.embyApi = embyHelper.NewEmbyApi(_settings.EmbySettings)
  32. em.threads = 6
  33. em.timeOut = 60 * time.Second
  34. return &em
  35. }
  36. // GetRecentlyAddVideoListWithNoChineseSubtitle 获取最近新添加的视频,且没有中文字幕的
  37. func (em *EmbyHelper) GetRecentlyAddVideoListWithNoChineseSubtitle() ([]emby.EmbyMixInfo, map[string][]emby.EmbyMixInfo, error) {
  38. filterMovieList, filterSeriesList, err := em.GetRecentlyAddVideoList()
  39. if err != nil {
  40. return nil, nil, err
  41. }
  42. // 将没有字幕的找出来
  43. noSubMovieList, err := em.filterNoChineseSubVideoList(filterMovieList)
  44. if err != nil {
  45. return nil, nil, err
  46. }
  47. log_helper.GetLogger().Debugln("-----------------")
  48. noSubSeriesList, err := em.filterNoChineseSubVideoList(filterSeriesList)
  49. if err != nil {
  50. return nil, nil, err
  51. }
  52. // 输出调试信息
  53. log_helper.GetLogger().Debugln("-----------------")
  54. log_helper.GetLogger().Debugln("filterNoChineseSubVideoList found no chinese movie", len(noSubMovieList))
  55. for index, info := range filterMovieList {
  56. log_helper.GetLogger().Debugln(index, info.VideoFileName)
  57. }
  58. log_helper.GetLogger().Debugln("-----------------")
  59. log_helper.GetLogger().Debugln("filterNoChineseSubVideoList found no chinese series", len(noSubSeriesList))
  60. for index, info := range filterSeriesList {
  61. log_helper.GetLogger().Debugln(index, info.VideoFileName)
  62. }
  63. log_helper.GetLogger().Debugln("-----------------")
  64. // 需要将连续剧零散的每一集,进行合并到一个连续剧下面,也就是这个连续剧有那些需要更新的
  65. var seriesMap = make(map[string][]emby.EmbyMixInfo)
  66. for _, info := range noSubSeriesList {
  67. _, ok := seriesMap[info.VideoFolderName]
  68. if ok == false {
  69. // 不存在则新建初始化
  70. seriesMap[info.VideoFolderName] = make([]emby.EmbyMixInfo, 0)
  71. }
  72. seriesMap[info.VideoFolderName] = append(seriesMap[info.VideoFolderName], info)
  73. }
  74. return noSubMovieList, seriesMap, nil
  75. }
  76. // GetRecentlyAddVideoList 获取最近新添加的视频
  77. func (em *EmbyHelper) GetRecentlyAddVideoList() ([]emby.EmbyMixInfo, []emby.EmbyMixInfo, error) {
  78. // 获取最近的影片列表
  79. items, err := em.embyApi.GetRecentlyItems()
  80. if err != nil {
  81. return nil, nil, err
  82. }
  83. // 获取电影和连续剧的文件夹名称
  84. var EpisodeIdList = make([]string, 0)
  85. var MovieIdList = make([]string, 0)
  86. log_helper.GetLogger().Debugln("-----------------")
  87. log_helper.GetLogger().Debugln("GetRecentlyAddVideoListWithNoChineseSubtitle - GetRecentlyItems Count", len(items.Items))
  88. // 分类
  89. for index, item := range items.Items {
  90. if item.Type == videoTypeEpisode {
  91. // 这个里面可能混有其他的内容,比如目标是连续剧,但是 emby_helper 其实会把其他的混合内容也标记进去
  92. EpisodeIdList = append(EpisodeIdList, item.Id)
  93. log_helper.GetLogger().Debugln("Episode:", index, item.SeriesName, item.ParentIndexNumber, item.IndexNumber)
  94. } else if item.Type == videoTypeMovie {
  95. // 这个里面可能混有其他的内容,比如目标是连续剧,但是 emby_helper 其实会把其他的混合内容也标记进去
  96. MovieIdList = append(MovieIdList, item.Id)
  97. log_helper.GetLogger().Debugln("Movie:", index, item.Name)
  98. } else {
  99. log_helper.GetLogger().Debugln("GetRecentlyItems - Is not a goal video type:", index, item.Name, item.Type)
  100. }
  101. }
  102. // 过滤出有效的电影、连续剧的资源出来
  103. filterMovieList, err := em.getMoreVideoInfoList(MovieIdList, true)
  104. if err != nil {
  105. return nil, nil, err
  106. }
  107. filterSeriesList, err := em.getMoreVideoInfoList(EpisodeIdList, false)
  108. if err != nil {
  109. return nil, nil, err
  110. }
  111. // 输出调试信息
  112. log_helper.GetLogger().Debugln("-----------------")
  113. log_helper.GetLogger().Debugln("getMoreVideoInfoList found valid movie", len(filterMovieList))
  114. for index, info := range filterMovieList {
  115. log_helper.GetLogger().Debugln(index, info.VideoFileName)
  116. }
  117. log_helper.GetLogger().Debugln("-----------------")
  118. log_helper.GetLogger().Debugln("getMoreVideoInfoList found valid series", len(filterSeriesList))
  119. for index, info := range filterSeriesList {
  120. log_helper.GetLogger().Debugln(index, info.VideoFileName)
  121. }
  122. log_helper.GetLogger().Debugln("-----------------")
  123. return filterMovieList, filterSeriesList, nil
  124. }
  125. // GetPlayedItemsSubtitle 所有用户标记播放过的视频,返回 电影、连续剧, 视频全路径 -- 对应字幕全路径(经过转换的)
  126. func (em *EmbyHelper) GetPlayedItemsSubtitle() (map[string]string, map[string]string, error) {
  127. // 这个用户看过那些视频
  128. var userPlayedItemsList = make([]emby.UserPlayedItems, 0)
  129. // 获取有那些用户
  130. var userIds emby.EmbyUsers
  131. userIds, err := em.embyApi.GetUserIdList()
  132. if err != nil {
  133. return nil, nil, err
  134. }
  135. // 所有用户观看过的视频有那些,需要分用户统计出来
  136. for _, item := range userIds.Items {
  137. tmpRecItems, err := em.embyApi.GetRecentItemsByUserID(item.Id)
  138. if err != nil {
  139. return nil, nil, err
  140. }
  141. // 相同的视频项目,需要判断是否已经看过了,看过的需要排除
  142. // 项目是否相同可以通过 Id 判断
  143. oneUserPlayedItems := emby.UserPlayedItems{
  144. UserName: item.Name,
  145. UserID: item.Id,
  146. Items: make([]emby.EmbyRecentlyItem, 0),
  147. }
  148. for _, recentlyItem := range tmpRecItems.Items {
  149. if recentlyItem.UserData.Played == true {
  150. oneUserPlayedItems.Items = append(oneUserPlayedItems.Items, recentlyItem)
  151. }
  152. }
  153. userPlayedItemsList = append(userPlayedItemsList, oneUserPlayedItems)
  154. }
  155. // 把这些用户看过的视频根据 userID 和 videoID 进行查询,使用的是第几个字幕
  156. // 这里需要区分是 Movie 还是 Series,这样后续的路径映射才能够生效
  157. // 视频 emby 路径 - 字幕 emby 路径
  158. movieEmbyFPathMap := make(map[string]string)
  159. seriesEmbyFPathMap := make(map[string]string)
  160. for _, playedItems := range userPlayedItemsList {
  161. for _, item := range playedItems.Items {
  162. videoInfoByUserId, err := em.embyApi.GetItemVideoInfoByUserId(playedItems.UserID, item.Id)
  163. if err != nil {
  164. return nil, nil, err
  165. }
  166. videoInfo, err := em.embyApi.GetItemVideoInfo(item.Id)
  167. if err != nil {
  168. return nil, nil, err
  169. }
  170. // 首先不能越界
  171. if videoInfoByUserId.GetDefaultSubIndex() < 0 || len(videoInfo.MediaStreams)-1 < videoInfoByUserId.GetDefaultSubIndex() {
  172. log_helper.GetLogger().Debugln("GetPlayedItemsSubtitle", videoInfo.Name, "SubIndex Out Of Range")
  173. continue
  174. }
  175. // 然后找出来的字幕必须是外置字幕,内置还导出个啥子
  176. if videoInfo.MediaStreams[videoInfoByUserId.GetDefaultSubIndex()].IsExternal == false {
  177. log_helper.GetLogger().Debugln("GetPlayedItemsSubtitle", videoInfo.Name,
  178. "Get Played SubIndex", videoInfoByUserId.GetDefaultSubIndex(),
  179. "is IsExternal == false, Skip")
  180. continue
  181. }
  182. // 将这个字幕的 Emby 内部路径保存下来,后续还需要进行一次路径转换才能使用,转换到本程序的路径上
  183. if item.Type == videoTypeEpisode {
  184. seriesEmbyFPathMap[videoInfo.Path] = videoInfo.MediaStreams[videoInfoByUserId.GetDefaultSubIndex()].Path
  185. } else if item.Type == videoTypeMovie {
  186. movieEmbyFPathMap[videoInfo.Path] = videoInfo.MediaStreams[videoInfoByUserId.GetDefaultSubIndex()].Path
  187. }
  188. }
  189. }
  190. // 转换 Emby 内部路径到本程序识别的视频目录上
  191. moviePhyFPathMap := make(map[string]string)
  192. seriesPhyFPathMap := make(map[string]string)
  193. // movie
  194. for key, value := range movieEmbyFPathMap {
  195. bok, prefixOldPath, prefixNewPath := em.findMappingPath(key, true)
  196. if bok == false {
  197. log_helper.GetLogger().Warningln("GetPlayedItemsSubtitle.findMappingPath miss matched,", key)
  198. continue
  199. }
  200. phyVideoPath := strings.ReplaceAll(key, prefixOldPath, prefixNewPath)
  201. phySubPath := strings.ReplaceAll(value, prefixOldPath, prefixNewPath)
  202. moviePhyFPathMap[phyVideoPath] = phySubPath
  203. }
  204. // series
  205. for key, value := range seriesEmbyFPathMap {
  206. bok, prefixOldPath, prefixNewPath := em.findMappingPath(key, false)
  207. if bok == false {
  208. log_helper.GetLogger().Warningln("GetPlayedItemsSubtitle.findMappingPath miss matched,", key)
  209. continue
  210. }
  211. phyVideoPath := strings.ReplaceAll(key, prefixOldPath, prefixNewPath)
  212. phySubPath := strings.ReplaceAll(value, prefixOldPath, prefixNewPath)
  213. seriesPhyFPathMap[phyVideoPath] = phySubPath
  214. }
  215. return moviePhyFPathMap, seriesPhyFPathMap, nil
  216. }
  217. // RefreshEmbySubList 字幕下载完毕一次,就可以触发一次这个。并发 6 线程去刷新
  218. func (em *EmbyHelper) RefreshEmbySubList() (bool, error) {
  219. if em.embyApi == nil {
  220. return false, nil
  221. }
  222. err := em.embyApi.RefreshRecentlyVideoInfo()
  223. if err != nil {
  224. return false, err
  225. }
  226. return true, nil
  227. }
  228. // findMappingPath 从 Emby 内置路径匹配到物理路径,返回,需要替换的前缀,以及替换到的前缀
  229. // X:\电影 - /mnt/share1/电影
  230. // X:\连续剧 - /mnt/share1/连续剧
  231. func (em *EmbyHelper) findMappingPath(fileFPathWithEmby string, isMovieOrSeries bool) (bool, string, string) {
  232. // 这里进行路径匹配的时候需要考虑嵌套路径的问题
  233. // 比如,映射了 /电影 以及 /电影/AA ,那么如果有一部电影 /电影/AA/xx/xx.mkv 那么,应该匹配的是最长的路径 /电影/AA
  234. matchedEmbyPaths := make([]string, 0)
  235. if isMovieOrSeries == true {
  236. // 电影的情况
  237. for _, embyPath := range em.settings.EmbySettings.MoviePathsMapping {
  238. if strings.HasPrefix(fileFPathWithEmby, embyPath) == true {
  239. matchedEmbyPaths = append(matchedEmbyPaths, embyPath)
  240. }
  241. }
  242. } else {
  243. // 连续剧的情况
  244. for _, embyPath := range em.settings.EmbySettings.SeriesPathsMapping {
  245. if strings.HasPrefix(fileFPathWithEmby, embyPath) == true {
  246. matchedEmbyPaths = append(matchedEmbyPaths, embyPath)
  247. }
  248. }
  249. }
  250. if len(matchedEmbyPaths) < 1 {
  251. return false, "", ""
  252. }
  253. // 排序得到匹配上的路径,最长的那个
  254. pathSlices := sortStringSliceByLength(matchedEmbyPaths)
  255. // 然后还需要从这个最长的路径,从 map 中找到对应的物理路径
  256. // nowPhRootPath 这个路径是映射的根目录,如果里面再次嵌套 子文件夹 再到连续剧目录,则是个问题,会丢失子文件夹目录
  257. nowPhRootPath := ""
  258. if isMovieOrSeries == true {
  259. // 电影的情况
  260. for physicalPath, embyPath := range em.settings.EmbySettings.MoviePathsMapping {
  261. if embyPath == pathSlices[0].Path {
  262. nowPhRootPath = physicalPath
  263. break
  264. }
  265. }
  266. } else {
  267. // 连续剧的情况
  268. for physicalPath, embyPath := range em.settings.EmbySettings.SeriesPathsMapping {
  269. if embyPath == pathSlices[0].Path {
  270. nowPhRootPath = physicalPath
  271. break
  272. }
  273. }
  274. }
  275. // 如果匹配不上
  276. if nowPhRootPath == "" {
  277. return false, "", ""
  278. }
  279. return true, pathSlices[0].Path, nowPhRootPath
  280. }
  281. // getVideoIMDBId 从视频的内部 ID 找到 IMDB id
  282. func (em *EmbyHelper) getMoreVideoInfo(videoID string, isMovieOrSeries bool) (*emby.EmbyMixInfo, error) {
  283. if isMovieOrSeries == true {
  284. // 电影的情况
  285. info, err := em.embyApi.GetItemVideoInfo(videoID)
  286. if err != nil {
  287. return nil, err
  288. }
  289. ancs, err := em.embyApi.GetItemAncestors(videoID)
  290. if err != nil {
  291. return nil, err
  292. }
  293. mixInfo := emby.EmbyMixInfo{IMDBId: info.ProviderIds.Imdb, Ancestors: ancs, VideoInfo: info}
  294. return &mixInfo, nil
  295. } else {
  296. // 连续剧的情况,需要从一集对算到 series 目录,得到内部 series 的 ID,然后再得到 IMDB ID
  297. ancs, err := em.embyApi.GetItemAncestors(videoID)
  298. if err != nil {
  299. return nil, err
  300. }
  301. // 暂时不支持蓝光,因为没有下载到对应的连续剧蓝光视频
  302. ancestorIndex := -1
  303. // 找到连续剧文件夹这一层
  304. for i, ancestor := range ancs {
  305. if ancestor.Type == "Series" {
  306. ancestorIndex = i
  307. break
  308. }
  309. }
  310. if ancestorIndex == -1 {
  311. // 说明没有找到连续剧文件夹的名称,那么就应该跳过
  312. return nil, nil
  313. }
  314. info, err := em.embyApi.GetItemVideoInfo(ancs[ancestorIndex].ID)
  315. if err != nil {
  316. return nil, err
  317. }
  318. mixInfo := emby.EmbyMixInfo{IMDBId: info.ProviderIds.Imdb, Ancestors: ancs, VideoInfo: info}
  319. return &mixInfo, nil
  320. }
  321. }
  322. // 根据 IMDB ID 自动转换路径
  323. func (em *EmbyHelper) autoFindMappingPathWithMixInfoByIMDBId(mixInfo *emby.EmbyMixInfo, isMovieOrSeries bool) bool {
  324. if isMovieOrSeries == true {
  325. } else {
  326. }
  327. return false
  328. }
  329. // findMappingPathWithMixInfo 从 Emby 内置路径匹配到物理路径
  330. // X:\电影 - /mnt/share1/电影
  331. // X:\连续剧 - /mnt/share1/连续剧
  332. func (em *EmbyHelper) findMappingPathWithMixInfo(mixInfo *emby.EmbyMixInfo, isMovieOrSeries bool) bool {
  333. defer func() {
  334. // 见 https://github.com/allanpk716/ChineseSubFinder/issues/278
  335. // 进行字符串操作的时候,可能会把 smb://123 转义为 smb:/123
  336. // 修复了个寂寞,没用,这个逻辑保留,但是没用哈。因为就不支持 SMB 的客户端协议
  337. if mixInfo != nil {
  338. mixInfo.PhysicalRootPath = path_helper.FixShareFileProtocolsPath(mixInfo.PhysicalRootPath)
  339. mixInfo.PhysicalVideoFileFullPath = path_helper.FixShareFileProtocolsPath(mixInfo.PhysicalVideoFileFullPath)
  340. }
  341. }()
  342. // 这里进行路径匹配的时候需要考虑嵌套路径的问题
  343. // 比如,映射了 /电影 以及 /电影/AA ,那么如果有一部电影 /电影/AA/xx/xx.mkv 那么,应该匹配的是最长的路径 /电影/AA
  344. matchedEmbyPaths := make([]string, 0)
  345. if isMovieOrSeries == true {
  346. // 电影的情况
  347. for _, embyPath := range em.settings.EmbySettings.MoviePathsMapping {
  348. if strings.HasPrefix(mixInfo.VideoInfo.Path, embyPath) == true {
  349. matchedEmbyPaths = append(matchedEmbyPaths, embyPath)
  350. }
  351. }
  352. } else {
  353. // 连续剧的情况
  354. for _, embyPath := range em.settings.EmbySettings.SeriesPathsMapping {
  355. if strings.HasPrefix(mixInfo.VideoInfo.Path, embyPath) == true {
  356. matchedEmbyPaths = append(matchedEmbyPaths, embyPath)
  357. }
  358. }
  359. }
  360. if len(matchedEmbyPaths) < 1 {
  361. return false
  362. }
  363. // 排序得到匹配上的路径,最长的那个
  364. pathSlices := sortStringSliceByLength(matchedEmbyPaths)
  365. // 然后还需要从这个最长的路径,从 map 中找到对应的物理路径
  366. // nowPhRootPath 这个路径是映射的根目录,如果里面再次嵌套 子文件夹 再到连续剧目录,则是个问题,会丢失子文件夹目录
  367. nowPhRootPath := ""
  368. if isMovieOrSeries == true {
  369. // 电影的情况
  370. for physicalPath, embyPath := range em.settings.EmbySettings.MoviePathsMapping {
  371. if embyPath == pathSlices[0].Path {
  372. nowPhRootPath = physicalPath
  373. break
  374. }
  375. }
  376. } else {
  377. // 连续剧的情况
  378. for physicalPath, embyPath := range em.settings.EmbySettings.SeriesPathsMapping {
  379. if embyPath == pathSlices[0].Path {
  380. nowPhRootPath = physicalPath
  381. break
  382. }
  383. }
  384. }
  385. // 如果匹配不上
  386. if nowPhRootPath == "" {
  387. return false
  388. }
  389. // 下面开始实际的路径替换,从 emby 的内部路径转换为 本程序读取到视频的路径
  390. if isMovieOrSeries == true {
  391. // 电影
  392. // 这里需要考虑蓝光的情况,这种目录比较特殊,在 emby 获取的时候,可以知道这个是否是蓝光,是的话,需要特殊处理
  393. // 伪造一个虚假不存在的 .mp4 文件向后提交给电影的下载函数
  394. /*
  395. 举例:失控玩家(2021) 是一个蓝光电影
  396. 那么下面的 mixInfo.VideoInfo.Path 从 emby 拿到应该是 /mnt/share1/电影/失控玩家(2021)
  397. 就需要再次基础上进行视频的伪造
  398. */
  399. if len(mixInfo.VideoInfo.MediaSources) > 0 && mixInfo.VideoInfo.MediaSources[0].Container == "bluray" {
  400. // 这个就是蓝光了
  401. // 先替换再拼接,不然会出现拼接完成后,在 Windows 下会把 /mnt/share1/电影 变为这样了 \mnt\share1\电影\失控玩家 (2021)\失控玩家 (2021).mp4
  402. videoReplacedDirFPath := strings.ReplaceAll(mixInfo.VideoInfo.Path, pathSlices[0].Path, nowPhRootPath)
  403. fakeVideoFPath := filepath.Join(videoReplacedDirFPath, filepath.Base(mixInfo.VideoInfo.Path)+common2.VideoExtMp4)
  404. mixInfo.PhysicalVideoFileFullPath = strings.ReplaceAll(fakeVideoFPath, pathSlices[0].Path, nowPhRootPath)
  405. // 这个电影的文件夹
  406. mixInfo.VideoFolderName = filepath.Base(mixInfo.VideoInfo.Path)
  407. mixInfo.VideoFileName = filepath.Base(mixInfo.VideoInfo.Path) + common2.VideoExtMp4
  408. } else {
  409. // 常规的电影情况,也就是有一个具体的视频文件 .mp4 or .mkv
  410. mixInfo.PhysicalVideoFileFullPath = strings.ReplaceAll(mixInfo.VideoInfo.Path, pathSlices[0].Path, nowPhRootPath)
  411. // 因为电影搜索的时候使用的是完整的视频目录,所以这个字段并不重要,连续剧的时候才需要关注
  412. //mixInfo.PhysicalRootPath = strings.ReplaceAll(mixInfo.VideoInfo.Path, pathSlices[0].Path, nowPhRootPath)
  413. // 这个电影的文件夹
  414. mixInfo.VideoFolderName = filepath.Base(filepath.Dir(mixInfo.VideoInfo.Path))
  415. mixInfo.VideoFileName = filepath.Base(mixInfo.VideoInfo.Path)
  416. }
  417. } else {
  418. // 连续剧
  419. // 暂时不支持蓝光,因为没有下载到对应的连续剧蓝光视频
  420. ancestorIndex := -1
  421. // 找到连续剧文件夹这一层
  422. for i, ancestor := range mixInfo.Ancestors {
  423. if ancestor.Type == "Series" {
  424. ancestorIndex = i
  425. break
  426. }
  427. }
  428. if ancestorIndex == -1 {
  429. // 说明没有找到连续剧文件夹的名称,那么就应该跳过
  430. return false
  431. }
  432. mixInfo.PhysicalSeriesRootDir = strings.ReplaceAll(mixInfo.Ancestors[ancestorIndex].Path, pathSlices[0].Path, nowPhRootPath)
  433. mixInfo.PhysicalVideoFileFullPath = strings.ReplaceAll(mixInfo.VideoInfo.Path, pathSlices[0].Path, nowPhRootPath)
  434. mixInfo.PhysicalRootPath = strings.ReplaceAll(mixInfo.Ancestors[ancestorIndex+1].Path, pathSlices[0].Path, nowPhRootPath)
  435. // 这个剧集的文件夹
  436. mixInfo.VideoFolderName = filepath.Base(mixInfo.Ancestors[ancestorIndex].Path)
  437. mixInfo.VideoFileName = filepath.Base(mixInfo.VideoInfo.Path)
  438. }
  439. return true
  440. }
  441. // getMoreVideoInfoList 把视频的更多信息查询出来,需要并发去做
  442. func (em *EmbyHelper) getMoreVideoInfoList(videoIdList []string, isMovieOrSeries bool) ([]emby.EmbyMixInfo, error) {
  443. var filterVideoEmbyInfo = make([]emby.EmbyMixInfo, 0)
  444. // 这个方法是使用两边的路径映射表来实现的转换,使用的体验不佳,很多人搞不定
  445. queryFuncByMatchPath := func(m string) (*emby.EmbyMixInfo, error) {
  446. oneMixInfo, err := em.getMoreVideoInfo(m, isMovieOrSeries)
  447. if err != nil {
  448. return nil, err
  449. }
  450. if isMovieOrSeries == true {
  451. // 电影
  452. // 过滤掉不符合要求的,拼接绝对路径
  453. isFit := em.findMappingPathWithMixInfo(oneMixInfo, isMovieOrSeries)
  454. if isFit == false {
  455. return nil, err
  456. }
  457. } else {
  458. // 连续剧
  459. // 过滤掉不符合要求的,拼接绝对路径
  460. isFit := em.findMappingPathWithMixInfo(oneMixInfo, isMovieOrSeries)
  461. if isFit == false {
  462. return nil, err
  463. }
  464. }
  465. return oneMixInfo, nil
  466. }
  467. // em.threads
  468. p, err := ants.NewPoolWithFunc(1, func(inData interface{}) {
  469. data := inData.(InputData)
  470. defer data.Wg.Done()
  471. ctx, cancel := context.WithTimeout(context.Background(), em.timeOut)
  472. defer cancel()
  473. done := make(chan OutData, 1)
  474. panicChan := make(chan interface{}, 1)
  475. go func() {
  476. defer func() {
  477. if p := recover(); p != nil {
  478. panicChan <- p
  479. }
  480. }()
  481. info, err := queryFuncByMatchPath(data.Id)
  482. outData := OutData{
  483. Info: info,
  484. Err: err,
  485. }
  486. done <- outData
  487. }()
  488. select {
  489. case outData := <-done:
  490. // 收到结果,需要加锁
  491. if outData.Err != nil {
  492. log_helper.GetLogger().Errorln("getMoreVideoInfoList.NewPoolWithFunc got Err", outData.Err)
  493. return
  494. }
  495. if outData.Info == nil {
  496. return
  497. }
  498. em.listLock.Lock()
  499. filterVideoEmbyInfo = append(filterVideoEmbyInfo, *outData.Info)
  500. em.listLock.Unlock()
  501. return
  502. case p := <-panicChan:
  503. log_helper.GetLogger().Errorln("getMoreVideoInfoList.NewPoolWithFunc got panic", p)
  504. case <-ctx.Done():
  505. log_helper.GetLogger().Errorln("getMoreVideoInfoList.NewPoolWithFunc got time out", ctx.Err())
  506. return
  507. }
  508. })
  509. if err != nil {
  510. return nil, err
  511. }
  512. defer p.Release()
  513. wg := sync.WaitGroup{}
  514. // 获取视频的 Emby 信息
  515. for _, m := range videoIdList {
  516. wg.Add(1)
  517. err = p.Invoke(InputData{Id: m, Wg: &wg})
  518. if err != nil {
  519. log_helper.GetLogger().Errorln("getMoreVideoInfoList ants.Invoke", err)
  520. }
  521. }
  522. wg.Wait()
  523. return filterVideoEmbyInfo, nil
  524. }
  525. // filterNoChineseSubVideoList 将没有中文字幕的视频找出来
  526. func (em *EmbyHelper) filterNoChineseSubVideoList(videoList []emby.EmbyMixInfo) ([]emby.EmbyMixInfo, error) {
  527. currentTime := time.Now()
  528. var noSubVideoList = make([]emby.EmbyMixInfo, 0)
  529. // TODO 这里有一种情况需要考虑的,如果内置有中文的字幕,那么是否需要跳过,目前暂定的一定要有外置的字幕
  530. for _, info := range videoList {
  531. needDlSub3Month := false
  532. // 3个月内,或者没有字幕都要进行下载
  533. if info.VideoInfo.PremiereDate.AddDate(0, 0, em.settings.AdvancedSettings.TaskQueue.ExpirationTime).After(currentTime) == true {
  534. // 需要下载的
  535. needDlSub3Month = true
  536. }
  537. // 这个影片只要有一个符合字幕要求的,就可以跳过
  538. // 外置中文字幕
  539. haveExternalChineseSub := false
  540. for _, stream := range info.VideoInfo.MediaStreams {
  541. // 首先找到外置的字幕文件
  542. if stream.IsExternal == true && stream.IsTextSubtitleStream == true && stream.SupportsExternalStream == true {
  543. // 然后字幕的格式以及语言命名要符合本程序的定义,有字幕
  544. if sub_parser_hub.IsEmbySubCodecWanted(stream.Codec) == true &&
  545. sub_parser_hub.IsEmbySubChineseLangStringWanted(stream.Language) == true {
  546. haveExternalChineseSub = true
  547. break
  548. } else {
  549. continue
  550. }
  551. }
  552. }
  553. // 内置中文字幕
  554. haveInsideChineseSub := false
  555. for _, stream := range info.VideoInfo.MediaStreams {
  556. if stream.IsExternal == false &&
  557. sub_parser_hub.IsEmbySubChineseLangStringWanted(stream.Language) {
  558. haveInsideChineseSub = true
  559. break
  560. }
  561. }
  562. // 比如,创建的时间在3个月内,然后没有额外下载的中文字幕,都符合要求
  563. if haveExternalChineseSub == false {
  564. // 没有外置字幕
  565. // 如果创建了7天,且有内置的中文字幕,那么也不进行下载了
  566. if info.VideoInfo.DateCreated.AddDate(0, 0, em.settings.AdvancedSettings.TaskQueue.DownloadSubDuringXDays).After(currentTime) == false && haveInsideChineseSub == true {
  567. log_helper.GetLogger().Debugln("Create Over 7 Days, And It Has Inside ChineseSub, Than Skip", info.VideoFileName)
  568. continue
  569. }
  570. //// 如果创建了三个月,还是没有字幕,那么也不进行下载了
  571. //if info.VideoInfo.DateCreated.Add(dayRange3Months).After(currentTime) == false {
  572. // continue
  573. //}
  574. // 没有中文字幕就加入下载列表
  575. noSubVideoList = append(noSubVideoList, info)
  576. } else {
  577. // 有外置字幕
  578. // 如果视频发布时间超过两年了,有字幕就直接跳过了,一般字幕稳定了
  579. if currentTime.Year()-2 > info.VideoInfo.PremiereDate.Year() {
  580. log_helper.GetLogger().Debugln("Create Over 2 Years, And It Has External ChineseSub, Than Skip", info.VideoFileName)
  581. continue
  582. }
  583. // 有中文字幕,且如果在三个月内,则需要继续下载字幕`
  584. if needDlSub3Month == true {
  585. noSubVideoList = append(noSubVideoList, info)
  586. }
  587. }
  588. }
  589. return noSubVideoList, nil
  590. }
  591. // GetInternalEngSubAndExChineseEnglishSub 获取对应 videoId 的内置英文字幕,外置中文字幕(只要是带有中文的都算,简体、繁体、简英、繁英,需要后续额外的判断)字幕
  592. func (em *EmbyHelper) GetInternalEngSubAndExChineseEnglishSub(videoId string) (bool, []emby.SubInfo, []emby.SubInfo, error) {
  593. // 先刷新以下这个资源,避免找到的字幕不存在了
  594. err := em.embyApi.UpdateVideoSubList(videoId)
  595. if err != nil {
  596. return false, nil, nil, err
  597. }
  598. // 获取这个资源的信息
  599. videoInfo, err := em.embyApi.GetItemVideoInfo(videoId)
  600. if err != nil {
  601. return false, nil, nil, err
  602. }
  603. // 获取 MediaSources ID,这里强制使用第一个视频源(因为 emby 运行有多个版本的视频指向到一个视频ID上,比如一个 web 一个 蓝光)
  604. mediaSourcesId := videoInfo.MediaSources[0].Id
  605. // 视频文件名称带后缀名
  606. videoFileName := filepath.Base(videoInfo.Path)
  607. videoFileNameWithOutExt := strings.ReplaceAll(videoFileName, path.Ext(videoFileName), "")
  608. // TODO 后续会新增一个功能,从视频中提取音频文件,然后识别转为字符,再进行与字幕的匹配
  609. // 获取是否有内置的英文字幕,如果没有则无需继续往下
  610. /*
  611. 这里有个梗,读取到的英文内置字幕很可能是残缺的,比如,基地 S01E04 Eng 第一个 Default Forced Sub,就不对,内容的 Dialogue 很少。
  612. 然后第二个 Eng 字幕才对。那么考虑到兼容性, 可能后续有短视频,也就不能简单的按 Dialogue 的多少去衡量。大概会做一个功能。方案有两个:
  613. 1. 读取到视频的总长度,然后再分析 Dialogue 的时间出现的部分与整体时间轴的占比,又或者是 Dialogue 之间的连续成都分析,这个有待测试。
  614. 2. 还有一个更加粗暴的方案,把所有的 Eng 都识别出来,然后找最多的 Dialogue 来做为正确的来使用(够粗暴吧)
  615. */
  616. var insideEngSUbIndexList = make([]int, 0)
  617. for _, stream := range videoInfo.MediaStreams {
  618. if stream.IsExternal == false && stream.Language == language.Emby_English_eng && stream.Codec == streamCodec {
  619. insideEngSUbIndexList = append(insideEngSUbIndexList, stream.Index)
  620. }
  621. }
  622. // 没有找到则跳过
  623. if len(insideEngSUbIndexList) == 0 {
  624. return false, nil, nil, nil
  625. }
  626. // 再内置英文字幕能找到的前提下,就可以先找中文的外置字幕,目前版本只能考虑双语字幕
  627. // 内置英文字幕,这里把 srt 和 ass 的都导出来
  628. var inSubList = make([]emby.SubInfo, 0)
  629. // 外置中文双语字幕
  630. var exSubList = make([]emby.SubInfo, 0)
  631. tmpFileNameWithOutExt := ""
  632. for _, stream := range videoInfo.MediaStreams {
  633. // 首先找到外置的字幕文件
  634. if stream.IsExternal == true && stream.IsTextSubtitleStream == true && stream.SupportsExternalStream == true {
  635. // 然后字幕的格式以及语言命名要符合本程序的定义,有字幕
  636. if sub_parser_hub.IsEmbySubCodecWanted(stream.Codec) == true &&
  637. sub_parser_hub.IsEmbySubChineseLangStringWanted(stream.Language) == true {
  638. tmpFileName := filepath.Base(stream.Path)
  639. // 去除 .default 或者 .forced
  640. //tmpFileName = strings.ReplaceAll(tmpFileName, subparser.Sub_Ext_Mark_Default, "")
  641. //tmpFileName = strings.ReplaceAll(tmpFileName, subparser.Sub_Ext_Mark_Forced, "")
  642. tmpFileNameWithOutExt = strings.ReplaceAll(tmpFileName, path.Ext(tmpFileName), "")
  643. exSubList = append(exSubList, *emby.NewSubInfo(tmpFileNameWithOutExt+"."+stream.Codec, "."+stream.Codec, stream.Index))
  644. } else {
  645. continue
  646. }
  647. }
  648. }
  649. // 没有找到则跳过
  650. if len(exSubList) == 0 {
  651. return false, nil, nil, nil
  652. }
  653. /*
  654. 把之前 Internal 英文字幕的 SubInfo 实例的信息补充完整
  655. 但是也不是绝对的,因为后续去 emby 下载字幕的时候,需要与外置字幕的后缀名一致
  656. 这里开始去下载字幕
  657. 先下载内置的文的
  658. 因为上面下载内置英文字幕的梗,所以,需要预先下载多个内置的英文字幕下来,用体积最大(相同后缀名)的那个来作为最后的输出
  659. */
  660. // 那么现在先下载相同格式(.srt)的两个字幕
  661. InsideEngSubIndex := 0
  662. if len(insideEngSUbIndexList) == 1 {
  663. // 如果就找到一个内置字幕,就默认这个
  664. InsideEngSubIndex = insideEngSUbIndexList[0]
  665. } else {
  666. // 如果找到不止一个就需要判断
  667. var tmpSubContentLenList = make([]int, 0)
  668. for _, index := range insideEngSUbIndexList {
  669. // TODO 这里默认是去 Emby 去拿字幕,但是其实可以缓存在视频文件同级的目录下,这样后续就无需多次下载了,毕竟每次下载都需要读取完整的视频
  670. subFileData, err := em.embyApi.GetSubFileData(videoId, mediaSourcesId, fmt.Sprintf("%d", index), common2.SubExtSRT)
  671. if err != nil {
  672. return false, nil, nil, err
  673. }
  674. tmpSubContentLenList = append(tmpSubContentLenList, len(subFileData))
  675. }
  676. maxContentLen := -1
  677. for index, contentLen := range tmpSubContentLenList {
  678. if maxContentLen < contentLen {
  679. maxContentLen = contentLen
  680. InsideEngSubIndex = insideEngSUbIndexList[index]
  681. }
  682. }
  683. }
  684. // 这里才是下载最佳的那个字幕
  685. for i := 0; i < 2; i++ {
  686. tmpExt := common2.SubExtSRT
  687. if i == 1 {
  688. tmpExt = common2.SubExtASS
  689. }
  690. subFileData, err := em.embyApi.GetSubFileData(videoId, mediaSourcesId, fmt.Sprintf("%d", InsideEngSubIndex), tmpExt)
  691. if err != nil {
  692. return false, nil, nil, err
  693. }
  694. tmpInSubInfo := emby.NewSubInfo(videoFileNameWithOutExt+tmpExt, tmpExt, InsideEngSubIndex)
  695. tmpInSubInfo.Content = []byte(subFileData)
  696. inSubList = append(inSubList, *tmpInSubInfo)
  697. }
  698. // 再下载外置的
  699. for i, subInfo := range exSubList {
  700. subFileData, err := em.embyApi.GetSubFileData(videoId, mediaSourcesId, fmt.Sprintf("%d", subInfo.EmbyStreamIndex), subInfo.Ext)
  701. if err != nil {
  702. return false, nil, nil, err
  703. }
  704. exSubList[i].Content = []byte(subFileData)
  705. }
  706. return true, inSubList, exSubList, nil
  707. }
  708. // CheckPath 检查路径 EmbyConfig 配置中的映射路径是否是有效的,
  709. func (em *EmbyHelper) CheckPath(pathType string) ([]string, error) {
  710. // 获取最近的影片列表
  711. items, err := em.embyApi.GetRecentlyItems()
  712. if err != nil {
  713. return nil, err
  714. }
  715. // 获取电影和连续剧的文件夹名称
  716. var EpisodeIdList = make([]string, 0)
  717. var MovieIdList = make([]string, 0)
  718. // 分类
  719. for index, item := range items.Items {
  720. if item.Type == videoTypeEpisode {
  721. // 这个里面可能混有其他的内容,比如目标是连续剧,但是 emby_helper 其实会把其他的混合内容也标记进去
  722. EpisodeIdList = append(EpisodeIdList, item.Id)
  723. log_helper.GetLogger().Debugln("Episode:", index, item.SeriesName, item.ParentIndexNumber, item.IndexNumber)
  724. } else if item.Type == videoTypeMovie {
  725. // 这个里面可能混有其他的内容,比如目标是连续剧,但是 emby_helper 其实会把其他的混合内容也标记进去
  726. MovieIdList = append(MovieIdList, item.Id)
  727. log_helper.GetLogger().Debugln("Movie:", index, item.Name)
  728. } else {
  729. log_helper.GetLogger().Debugln("GetRecentlyItems - Is not a goal video type:", index, item.Name, item.Type)
  730. }
  731. }
  732. outCount := 0
  733. outList := make([]string, 0)
  734. if pathType == "movie" {
  735. // 过滤出有效的电影、连续剧的资源出来
  736. filterMovieList, err := em.getMoreVideoInfoList(MovieIdList, true)
  737. if err != nil {
  738. return nil, err
  739. }
  740. for _, info := range filterMovieList {
  741. if my_util.IsFile(info.PhysicalVideoFileFullPath) == true {
  742. outList = append(outList, info.PhysicalVideoFileFullPath)
  743. outCount++
  744. if outCount > 5 {
  745. break
  746. }
  747. }
  748. }
  749. } else {
  750. filterSeriesList, err := em.getMoreVideoInfoList(EpisodeIdList, false)
  751. if err != nil {
  752. return nil, err
  753. }
  754. for _, info := range filterSeriesList {
  755. if my_util.IsFile(info.PhysicalVideoFileFullPath) == true {
  756. outList = append(outList, info.PhysicalVideoFileFullPath)
  757. outCount++
  758. if outCount > 5 {
  759. break
  760. }
  761. }
  762. }
  763. }
  764. return outList, nil
  765. }
  766. type InputData struct {
  767. Id string
  768. Wg *sync.WaitGroup
  769. }
  770. type OutData struct {
  771. Info *emby.EmbyMixInfo
  772. Err error
  773. }
  774. type PathSlice struct {
  775. Path string
  776. }
  777. type PathSlices []PathSlice
  778. func (a PathSlices) Len() int { return len(a) }
  779. func (a PathSlices) Less(i, j int) bool { return len(a[i].Path) < len(a[j].Path) }
  780. func (a PathSlices) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
  781. func sortStringSliceByLength(m []string) PathSlices {
  782. p := make(PathSlices, len(m))
  783. i := 0
  784. for _, v := range m {
  785. p[i] = PathSlice{v}
  786. i++
  787. }
  788. sort.Sort(sort.Reverse(p))
  789. return p
  790. }
  791. const (
  792. videoTypeEpisode = "Episode"
  793. videoTypeMovie = "Movie"
  794. streamCodec = "subrip"
  795. )