embyhelper.go 41 KB

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