scan_played_video_subinfo.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. package scan_played_video_subinfo
  2. import (
  3. "errors"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "sync"
  9. "github.com/allanpk716/ChineseSubFinder/pkg"
  10. "github.com/allanpk716/ChineseSubFinder/pkg/ifaces"
  11. common2 "github.com/allanpk716/ChineseSubFinder/pkg/types/common"
  12. embyHelper "github.com/allanpk716/ChineseSubFinder/pkg/logic/emby_helper"
  13. "github.com/allanpk716/ChineseSubFinder/pkg/logic/file_downloader"
  14. "github.com/allanpk716/ChineseSubFinder/pkg/mix_media_info"
  15. "github.com/allanpk716/ChineseSubFinder/internal/dao"
  16. "github.com/allanpk716/ChineseSubFinder/internal/models"
  17. "github.com/allanpk716/ChineseSubFinder/pkg/decode"
  18. "github.com/allanpk716/ChineseSubFinder/pkg/imdb_helper"
  19. "github.com/allanpk716/ChineseSubFinder/pkg/language"
  20. "github.com/allanpk716/ChineseSubFinder/pkg/settings"
  21. "github.com/allanpk716/ChineseSubFinder/pkg/sub_file_hash"
  22. "github.com/allanpk716/ChineseSubFinder/pkg/sub_formatter/emby"
  23. "github.com/allanpk716/ChineseSubFinder/pkg/sub_parser_hub"
  24. "github.com/allanpk716/ChineseSubFinder/pkg/sub_share_center"
  25. "github.com/allanpk716/ChineseSubFinder/pkg/task_control"
  26. "github.com/sirupsen/logrus"
  27. "golang.org/x/net/context"
  28. )
  29. type ScanPlayedVideoSubInfo struct {
  30. settings *settings.Settings
  31. log *logrus.Logger
  32. fileDownloader *file_downloader.FileDownloader
  33. embyHelper *embyHelper.EmbyHelper
  34. taskControl *task_control.TaskControl
  35. canceled bool
  36. canceledLock sync.Mutex
  37. movieSubMap map[string]string
  38. seriesSubMap map[string]string
  39. subFormatter ifaces.ISubFormatter
  40. shareRootDir string
  41. imdbInfoCache map[string]*models.IMDBInfo
  42. cacheImdbInfoCacheLocker sync.Mutex
  43. }
  44. func NewScanPlayedVideoSubInfo(log *logrus.Logger, _settings *settings.Settings, fileDownloader *file_downloader.FileDownloader) (*ScanPlayedVideoSubInfo, error) {
  45. var err error
  46. var scanPlayedVideoSubInfo ScanPlayedVideoSubInfo
  47. scanPlayedVideoSubInfo.log = log
  48. // 下载实例
  49. scanPlayedVideoSubInfo.fileDownloader = fileDownloader
  50. // 参入设置信息
  51. // 最大获取的视频数目设置到 100W
  52. scanPlayedVideoSubInfo.settings = _settings
  53. //scanPlayedVideoSubInfo.settings = clone.Clone(_settings).(*settings.Settings)
  54. //scanPlayedVideoSubInfo.settings.EmbySettings.MaxRequestVideoNumber = 1000000
  55. // 检测是否某些参数超出范围
  56. scanPlayedVideoSubInfo.settings.Check()
  57. // 初始化 Emby API 接口
  58. if scanPlayedVideoSubInfo.settings.EmbySettings.Enable == true && scanPlayedVideoSubInfo.settings.EmbySettings.AddressUrl != "" &&
  59. scanPlayedVideoSubInfo.settings.EmbySettings.APIKey != "" {
  60. scanPlayedVideoSubInfo.embyHelper = embyHelper.NewEmbyHelper(log, scanPlayedVideoSubInfo.settings)
  61. scanPlayedVideoSubInfo.embyHelper.SetMaxRequestVideoNumber(common2.EmbyApiGetItemsLimitMax)
  62. }
  63. // 初始化任务控制
  64. scanPlayedVideoSubInfo.taskControl, err = task_control.NewTaskControl(scanPlayedVideoSubInfo.settings.CommonSettings.Threads, log)
  65. if err != nil {
  66. return nil, err
  67. }
  68. // 字幕命名格式解析器
  69. scanPlayedVideoSubInfo.subFormatter = emby.NewFormatter()
  70. // 缓存目录的根目录
  71. shareRootDir, err := pkg.GetShareSubRootFolder()
  72. if err != nil {
  73. return nil, err
  74. }
  75. scanPlayedVideoSubInfo.shareRootDir = shareRootDir
  76. // 初始化缓存
  77. scanPlayedVideoSubInfo.imdbInfoCache = make(map[string]*models.IMDBInfo)
  78. return &scanPlayedVideoSubInfo, nil
  79. }
  80. func (s *ScanPlayedVideoSubInfo) Cancel() {
  81. defer func() {
  82. s.log.Infoln("ScanPlayedVideoSubInfo.Cancel()")
  83. }()
  84. s.canceledLock.Lock()
  85. s.canceled = true
  86. s.canceledLock.Unlock()
  87. s.taskControl.Release()
  88. }
  89. func (s *ScanPlayedVideoSubInfo) GetPlayedItemsSubtitle() (bool, error) {
  90. var err error
  91. // 是否是通过 emby_helper api 获取的列表
  92. if s.embyHelper == nil {
  93. // 没有填写 emby_helper api 的信息,那么就跳过
  94. s.log.Infoln("Skip ScanPlayedVideoSubInfo, Emby Settings is null")
  95. return false, nil
  96. }
  97. s.movieSubMap, s.seriesSubMap, err = s.embyHelper.GetPlayedItemsSubtitle()
  98. if err != nil {
  99. return false, err
  100. }
  101. return true, nil
  102. }
  103. // Clear 清理无效的缓存字幕信息
  104. func (s *ScanPlayedVideoSubInfo) Clear() {
  105. defer func() {
  106. s.log.Infoln("ScanPlayedVideoSubInfo.Clear Sub End")
  107. s.log.Infoln("----------------------------------------------")
  108. }()
  109. s.log.Infoln("-----------------------------------------------")
  110. s.log.Infoln("ScanPlayedVideoSubInfo.Clear Sub Start...")
  111. var imdbInfos []models.IMDBInfo
  112. // 把嵌套关联的 has many 的信息都查询出来
  113. dao.GetDb().Preload("VideoSubInfos").Find(&imdbInfos)
  114. // 同时需要把不在数据库记录的字幕给删除,那么就需要把数据库查询出来的给做成 map
  115. dbSubMap := make(map[string]int)
  116. for _, info := range imdbInfos {
  117. for _, oneSubInfo := range info.VideoSubInfos {
  118. s.log.Infoln("ScanPlayedVideoSubInfo.Clear Sub", oneSubInfo.SubName)
  119. // 转换到绝对路径
  120. cacheSubFPath := filepath.Join(s.shareRootDir, oneSubInfo.StoreRPath)
  121. if pkg.IsFile(cacheSubFPath) == false {
  122. // 如果文件不存在,那么就删除之前的关联
  123. // 关联删除了,但是不会删除这些对象,所以后续还需要再次删除
  124. s.delSubInfo(&info, &oneSubInfo)
  125. s.log.Debugln("ScanPlayedVideoSubInfo.Clear Sub delSubInfo", oneSubInfo.SubName)
  126. continue
  127. }
  128. dbSubMap[oneSubInfo.StoreRPath] = 0
  129. }
  130. }
  131. // 搜索缓存文件夹所有的字幕出来,对比上面的 map 进行比较
  132. subFiles, err := sub_parser_hub.SearchMatchedSubFile(s.log, s.shareRootDir)
  133. if err != nil {
  134. return
  135. }
  136. for _, file := range subFiles {
  137. subRelPath, err := filepath.Rel(s.shareRootDir, file)
  138. if err != nil {
  139. s.log.Warningln("ScanPlayedVideoSubInfo.Scan.Rel", file, err)
  140. continue
  141. }
  142. _, bok := dbSubMap[subRelPath]
  143. if bok == false {
  144. err = os.Remove(file)
  145. s.log.Debugln("ScanPlayedVideoSubInfo.Clear Sub Remove", file)
  146. if err != nil {
  147. s.log.Debugln("ScanPlayedVideoSubInfo.Clear Sub Remove", file, err)
  148. continue
  149. }
  150. }
  151. }
  152. }
  153. func (s *ScanPlayedVideoSubInfo) Scan() error {
  154. // Emby 观看的列表
  155. {
  156. // 从数据库中查询出所有的 IMDBInfo
  157. // 清空缓存
  158. s.imdbInfoCache = make(map[string]*models.IMDBInfo)
  159. // -----------------------------------------------------
  160. // 并发控制
  161. s.taskControl.SetCtxProcessFunc("ScanSubPlayedPool", s.scan, common2.ScanPlayedSubTimeOut)
  162. // -----------------------------------------------------
  163. err := s.taskControl.Invoke(&task_control.TaskData{
  164. Index: 0,
  165. Count: len(s.movieSubMap),
  166. DataEx: ScanInputData{
  167. Videos: s.movieSubMap,
  168. IsMovie: true,
  169. },
  170. })
  171. if err != nil {
  172. s.log.Errorln("ScanPlayedVideoSubInfo.Movie Sub Error", err)
  173. }
  174. err = s.taskControl.Invoke(&task_control.TaskData{
  175. Index: 0,
  176. Count: len(s.seriesSubMap),
  177. DataEx: ScanInputData{
  178. Videos: s.seriesSubMap,
  179. IsMovie: false,
  180. },
  181. })
  182. if err != nil {
  183. s.log.Errorln("ScanPlayedVideoSubInfo.Series Sub Error", err)
  184. }
  185. s.taskControl.Hold()
  186. }
  187. // 使用 Http API 标记的已观看列表
  188. {
  189. // TODO 暂时屏蔽掉 http api 提交的已看字幕的接口上传
  190. if false {
  191. // 下面需要把给出外部的 HTTP API 提交的视频和字幕信息(ThirdPartSetVideoPlayedInfo)进行判断,存入数据库
  192. shareRootDir, err := pkg.GetShareSubRootFolder()
  193. if err != nil {
  194. return err
  195. }
  196. var videoPlayedInfos []models.ThirdPartSetVideoPlayedInfo
  197. dao.GetDb().Find(&videoPlayedInfos)
  198. for i, thirdPartSetVideoPlayedInfo := range videoPlayedInfos {
  199. // 先要判断这个是 Movie 还是 Series
  200. // 因为设计这个 API 的时候为了简化提交的参数,也假定传入的可能不是正确的分类(电影or连续剧)
  201. // 所以只能比较傻的,低效率的匹配映射的目录来做到识别是哪个分类的
  202. bFoundMovie := false
  203. bFoundSeries := false
  204. for _, moviePath := range s.settings.CommonSettings.MoviePaths {
  205. // 先判断类型是否是 Movie
  206. if strings.HasPrefix(thirdPartSetVideoPlayedInfo.PhysicalVideoFileFullPath, moviePath) == true {
  207. bFoundMovie = true
  208. break
  209. }
  210. }
  211. if bFoundMovie == false {
  212. for _, seriesPath := range s.settings.CommonSettings.SeriesPaths {
  213. // 判断是否是 Series
  214. if strings.HasPrefix(thirdPartSetVideoPlayedInfo.PhysicalVideoFileFullPath, seriesPath) == true {
  215. bFoundSeries = true
  216. break
  217. }
  218. }
  219. }
  220. if bFoundMovie == false && bFoundSeries == false {
  221. // 说明提交的这个视频文件无法匹配电影或者连续剧的目录前缀
  222. s.log.Warningln("Not matched Movie and Series Prefix Path", thirdPartSetVideoPlayedInfo.PhysicalVideoFileFullPath)
  223. continue
  224. }
  225. IsMovie := false
  226. videoTypes := common2.Movie
  227. if bFoundMovie == true {
  228. videoTypes = common2.Movie
  229. IsMovie = true
  230. }
  231. if bFoundSeries == true {
  232. videoTypes = common2.Series
  233. IsMovie = false
  234. }
  235. tmpSubFPath := filepath.Join(filepath.Dir(thirdPartSetVideoPlayedInfo.PhysicalVideoFileFullPath), thirdPartSetVideoPlayedInfo.SubName)
  236. s.dealOneVideo(i, thirdPartSetVideoPlayedInfo.PhysicalVideoFileFullPath, tmpSubFPath, videoTypes.String(), shareRootDir, IsMovie, s.imdbInfoCache)
  237. }
  238. }
  239. }
  240. return nil
  241. }
  242. func (s *ScanPlayedVideoSubInfo) scan(ctx context.Context, inData interface{}) error {
  243. taskData := inData.(*task_control.TaskData)
  244. scanInputData := taskData.DataEx.(ScanInputData)
  245. videoTypes := ""
  246. if scanInputData.IsMovie == true {
  247. videoTypes = "Movie"
  248. } else {
  249. videoTypes = "Series"
  250. }
  251. defer func() {
  252. s.log.Infoln("ScanPlayedVideoSubInfo", videoTypes, "Sub End")
  253. s.log.Infoln("-----------------------------------------------")
  254. }()
  255. s.log.Infoln("-----------------------------------------------")
  256. s.log.Infoln("ScanPlayedVideoSubInfo", videoTypes, "Sub Start...")
  257. shareRootDir, err := pkg.GetShareSubRootFolder()
  258. if err != nil {
  259. return err
  260. }
  261. index := 0
  262. for videoFPath, orgSubFPath := range scanInputData.Videos {
  263. index++
  264. stage := make(chan interface{}, 1)
  265. go func() {
  266. defer func() {
  267. close(stage)
  268. }()
  269. s.dealOneVideo(index, videoFPath, orgSubFPath, videoTypes, shareRootDir, scanInputData.IsMovie, s.imdbInfoCache)
  270. stage <- 1
  271. }()
  272. select {
  273. case <-ctx.Done():
  274. {
  275. return errors.New(fmt.Sprintf("cancel at scan: %s", videoFPath))
  276. }
  277. case <-stage:
  278. break
  279. }
  280. }
  281. return nil
  282. }
  283. func (s *ScanPlayedVideoSubInfo) dealOneVideo(index int, videoFPath, orgSubFPath, videoTypes, shareRootDir string,
  284. isMovie bool,
  285. imdbInfoCache map[string]*models.IMDBInfo) {
  286. s.log.Infoln(index, orgSubFPath)
  287. if pkg.IsFile(orgSubFPath) == false {
  288. s.log.Errorln("Skip", orgSubFPath, "not exist")
  289. return
  290. }
  291. s.log.Debugln(0)
  292. // 通过视频的绝对路径,从本地的视频文件对应的 nfo 获取到这个视频的 IMDB ID,
  293. var err error
  294. imdbInfoFromVideoFile, err := imdb_helper.GetIMDBInfoFromVideoFile(s.log, videoFPath, isMovie, s.settings.AdvancedSettings.ProxySettings)
  295. if err != nil {
  296. s.log.Errorln("GetIMDBInfoFromVideoFile", err)
  297. return
  298. }
  299. s.log.Debugln(1)
  300. // 使用本程序的 hash 的算法,得到视频的唯一 ID
  301. fileHash, err := sub_file_hash.Calculate(videoFPath)
  302. if err != nil {
  303. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".ComputeFileHash", videoFPath, err)
  304. return
  305. }
  306. s.log.Debugln(2)
  307. var imdbInfo *models.IMDBInfo
  308. var ok bool
  309. // 先把 IMDB 信息查询查来,不管是从数据库还是网络(查询出来也得写入到数据库)
  310. s.cacheImdbInfoCacheLocker.Lock()
  311. imdbInfo, ok = imdbInfoCache[imdbInfoFromVideoFile.IMDBID]
  312. s.cacheImdbInfoCacheLocker.Unlock()
  313. if ok == false {
  314. s.cacheImdbInfoCacheLocker.Lock()
  315. imdbInfoCache[imdbInfoFromVideoFile.IMDBID] = imdbInfoFromVideoFile
  316. imdbInfo = imdbInfoFromVideoFile
  317. s.cacheImdbInfoCacheLocker.Unlock()
  318. }
  319. s.log.Debugln(3)
  320. // 这里需要判断是否已经获取了 TMDB Info,如果没有则需要去获取
  321. if imdbInfo.TmdbId == "" {
  322. s.log.Debugln("3-2")
  323. videoType := "movie"
  324. if imdbInfo.IsMovie == false {
  325. videoType = "series"
  326. }
  327. _, err = mix_media_info.GetMediaInfoAndSave(
  328. s.fileDownloader.MediaInfoDealers,
  329. imdbInfo,
  330. imdbInfo.IMDBID, "imdb", videoType)
  331. if err != nil {
  332. s.log.Errorln("dealOneVideo.GetMediaInfoAndSave,", imdbInfo.Name, err)
  333. return
  334. }
  335. }
  336. s.log.Debugln("3-2")
  337. // 当前扫描到的找个字幕的 sha256 是否已经存在与缓存中了
  338. tmpSHA256String, err := pkg.GetFileSHA256String(orgSubFPath)
  339. if err != nil {
  340. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, "orgSubFPath.GetFileSHA256String", videoFPath, err)
  341. return
  342. }
  343. s.log.Debugln(4)
  344. // 判断找到的关联字幕信息是否已经存在了,不存在则新增关联
  345. for _, cacheInfo := range imdbInfo.VideoSubInfos {
  346. if cacheInfo.SHA256 == tmpSHA256String {
  347. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, "SHA256 Exist == true, Skip", orgSubFPath)
  348. return
  349. }
  350. }
  351. s.log.Debugln(5)
  352. // 新增插入
  353. // 把现有的字幕 copy 到缓存目录中
  354. bok, subCacheFPath := sub_share_center.CopySub2Cache(s.log, orgSubFPath, imdbInfo.IMDBID, imdbInfo.Year, false)
  355. if bok == false {
  356. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".CopySub2Cache", orgSubFPath, err)
  357. return
  358. }
  359. s.log.Debugln(6)
  360. // 不存在,插入,建立关系
  361. bok, fileInfo, err := s.fileDownloader.SubParserHub.DetermineFileTypeFromFile(subCacheFPath)
  362. if err != nil {
  363. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".DetermineFileTypeFromFile", imdbInfo.IMDBID, err)
  364. return
  365. }
  366. if bok == false {
  367. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".DetermineFileTypeFromFile == false", imdbInfo.IMDBID)
  368. return
  369. }
  370. s.log.Debugln(7)
  371. // 特指 emby 字幕的情况
  372. _, _, _, _, extraSubPreName := s.subFormatter.IsMatchThisFormat(filepath.Base(subCacheFPath))
  373. // 转相对路径存储
  374. subRelPath, err := filepath.Rel(shareRootDir, subCacheFPath)
  375. if err != nil {
  376. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".Rel", imdbInfo.IMDBID, err)
  377. return
  378. }
  379. s.log.Debugln(8)
  380. // 计算需要插入字幕的 sha256
  381. saveSHA256String, err := pkg.GetFileSHA256String(subCacheFPath)
  382. if err != nil {
  383. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, "GetFileSHA256String", videoFPath, err)
  384. return
  385. }
  386. // 这个字幕文件是否已经存在了
  387. var videoSubInfos []models.VideoSubInfo
  388. dao.GetDb().Where("sha256 = ?", saveSHA256String).Find(&videoSubInfos)
  389. if len(videoSubInfos) > 0 {
  390. // 存在,跳过
  391. s.log.Infoln("ScanPlayedVideoSubInfo.Scan", videoTypes, "SHA256 Exist == true, Skip", orgSubFPath)
  392. return
  393. }
  394. s.log.Debugln(9)
  395. // 如果不存在,那么就标记这个字幕是未发送
  396. oneVideoSubInfo := models.NewVideoSubInfo(
  397. fileHash,
  398. filepath.Base(subCacheFPath),
  399. language.MyLang2ISO_639_1_String(fileInfo.Lang),
  400. language.IsBilingualSubtitle(fileInfo.Lang),
  401. language.MyLang2ChineseISO(fileInfo.Lang),
  402. fileInfo.Lang.String(),
  403. subRelPath,
  404. extraSubPreName,
  405. saveSHA256String,
  406. isMovie,
  407. )
  408. oneVideoSubInfo.IsSend = false
  409. if isMovie == false {
  410. // 连续剧的时候,如果可能应该获取是 第几季 第几集
  411. epsVideoNfoInfo, err := decode.GetVideoNfoInfo4OneSeriesEpisode(videoFPath)
  412. if err != nil {
  413. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".GetVideoNfoInfo4OneSeriesEpisode", imdbInfo.Name, err)
  414. return
  415. }
  416. oneVideoSubInfo.Season = epsVideoNfoInfo.Season
  417. oneVideoSubInfo.Episode = epsVideoNfoInfo.Episode
  418. }
  419. s.log.Debugln(10)
  420. err = dao.GetDb().Model(imdbInfo).Association("VideoSubInfos").Append(oneVideoSubInfo)
  421. if err != nil {
  422. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".Append Association", oneVideoSubInfo.SubName, err)
  423. return
  424. }
  425. }
  426. // 如果文件不存在,那么就删除之前的关联
  427. // 关联删除了,但是不会删除这些对象,所以后续还需要再次删除
  428. func (s *ScanPlayedVideoSubInfo) delSubInfo(imdbInfo *models.IMDBInfo, cacheInfo *models.VideoSubInfo) bool {
  429. err := dao.GetDb().Model(imdbInfo).Association("VideoSubInfos").Delete(cacheInfo)
  430. if err != nil {
  431. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", ".Delete Association", cacheInfo.SubName, err)
  432. return false
  433. }
  434. // 继续删除这个对象
  435. dao.GetDb().Delete(cacheInfo)
  436. s.log.Infoln("Delete Not Exist or SHA256 not the same, Sub Association", cacheInfo.SubName)
  437. return true
  438. }
  439. type ScanInputData struct {
  440. Videos map[string]string
  441. IsMovie bool
  442. }