scan_played_video_subinfo.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. package scan_played_video_subinfo
  2. import (
  3. "errors"
  4. "fmt"
  5. "github.com/allanpk716/ChineseSubFinder/internal/dao"
  6. "github.com/allanpk716/ChineseSubFinder/internal/ifaces"
  7. embyHelper "github.com/allanpk716/ChineseSubFinder/internal/logic/emby_helper"
  8. "github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser/ass"
  9. "github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser/srt"
  10. "github.com/allanpk716/ChineseSubFinder/internal/models"
  11. "github.com/allanpk716/ChineseSubFinder/internal/pkg/decode"
  12. "github.com/allanpk716/ChineseSubFinder/internal/pkg/imdb_helper"
  13. "github.com/allanpk716/ChineseSubFinder/internal/pkg/language"
  14. "github.com/allanpk716/ChineseSubFinder/internal/pkg/my_folder"
  15. "github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
  16. "github.com/allanpk716/ChineseSubFinder/internal/pkg/settings"
  17. "github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_file_hash"
  18. "github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_formatter/emby"
  19. "github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_parser_hub"
  20. "github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_share_center"
  21. "github.com/allanpk716/ChineseSubFinder/internal/pkg/task_control"
  22. "github.com/allanpk716/ChineseSubFinder/internal/types"
  23. "github.com/allanpk716/ChineseSubFinder/internal/types/common"
  24. "github.com/huandu/go-clone"
  25. "github.com/sirupsen/logrus"
  26. "golang.org/x/net/context"
  27. "os"
  28. "path/filepath"
  29. "sync"
  30. )
  31. type ScanPlayedVideoSubInfo struct {
  32. settings *settings.Settings
  33. log *logrus.Logger
  34. embyHelper *embyHelper.EmbyHelper
  35. taskControl *task_control.TaskControl
  36. canceled bool
  37. canceledLock sync.Mutex
  38. subParserHub *sub_parser_hub.SubParserHub
  39. movieSubMap map[string]string
  40. seriesSubMap map[string]string
  41. subFormatter ifaces.ISubFormatter
  42. shareRootDir string
  43. }
  44. func NewScanPlayedVideoSubInfo(log *logrus.Logger, _settings *settings.Settings) (*ScanPlayedVideoSubInfo, error) {
  45. var err error
  46. var scanPlayedVideoSubInfo ScanPlayedVideoSubInfo
  47. scanPlayedVideoSubInfo.log = log
  48. // 参入设置信息
  49. // 最大获取的视频数目设置到 100W
  50. scanPlayedVideoSubInfo.settings = clone.Clone(_settings).(*settings.Settings)
  51. scanPlayedVideoSubInfo.settings.EmbySettings.MaxRequestVideoNumber = 1000000
  52. // 检测是否某些参数超出范围
  53. scanPlayedVideoSubInfo.settings.Check()
  54. // 初始化 Emby API 接口
  55. if scanPlayedVideoSubInfo.settings.EmbySettings.Enable == true && scanPlayedVideoSubInfo.settings.EmbySettings.AddressUrl != "" &&
  56. scanPlayedVideoSubInfo.settings.EmbySettings.APIKey != "" {
  57. scanPlayedVideoSubInfo.embyHelper = embyHelper.NewEmbyHelper(log, scanPlayedVideoSubInfo.settings)
  58. }
  59. // 初始化任务控制
  60. scanPlayedVideoSubInfo.taskControl, err = task_control.NewTaskControl(scanPlayedVideoSubInfo.settings.CommonSettings.Threads, log)
  61. if err != nil {
  62. return nil, err
  63. }
  64. // 字幕解析器
  65. scanPlayedVideoSubInfo.subParserHub = sub_parser_hub.NewSubParserHub(log, ass.NewParser(log), srt.NewParser(log))
  66. // 字幕命名格式解析器
  67. scanPlayedVideoSubInfo.subFormatter = emby.NewFormatter()
  68. // 缓存目录的根目录
  69. shareRootDir, err := my_folder.GetShareSubRootFolder()
  70. if err != nil {
  71. return nil, err
  72. }
  73. scanPlayedVideoSubInfo.shareRootDir = shareRootDir
  74. return &scanPlayedVideoSubInfo, nil
  75. }
  76. func (s *ScanPlayedVideoSubInfo) Cancel() {
  77. s.canceledLock.Lock()
  78. s.canceled = true
  79. s.canceledLock.Unlock()
  80. s.taskControl.Release()
  81. }
  82. func (s *ScanPlayedVideoSubInfo) GetPlayedItemsSubtitle() (bool, error) {
  83. var err error
  84. // 是否是通过 emby_helper api 获取的列表
  85. if s.embyHelper == nil {
  86. // 没有填写 emby_helper api 的信息,那么就跳过
  87. s.log.Infoln("Skip ScanPlayedVideoSubInfo, Emby Settings is null")
  88. return false, nil
  89. }
  90. s.movieSubMap, s.seriesSubMap, err = s.embyHelper.GetPlayedItemsSubtitle()
  91. if err != nil {
  92. return false, err
  93. }
  94. return true, nil
  95. }
  96. // Clear 清理无效的缓存字幕信息
  97. func (s *ScanPlayedVideoSubInfo) Clear() {
  98. defer func() {
  99. s.log.Infoln("ScanPlayedVideoSubInfo.Clear Sub End")
  100. s.log.Infoln("----------------------------------------------")
  101. }()
  102. s.log.Infoln("-----------------------------------------------")
  103. s.log.Infoln("ScanPlayedVideoSubInfo.Clear Sub Start...")
  104. var imdbInfos []models.IMDBInfo
  105. // 把嵌套关联的 has many 的信息都查询出来
  106. dao.GetDb().Preload("VideoSubInfos").Find(&imdbInfos)
  107. // 同时需要把不在数据库记录的字幕给删除,那么就需要把数据库查询出来的给做成 map
  108. dbSubMap := make(map[string]int)
  109. for _, info := range imdbInfos {
  110. for _, oneSubInfo := range info.VideoSubInfos {
  111. s.log.Infoln("ScanPlayedVideoSubInfo.Clear Sub", oneSubInfo.SubName)
  112. // 转换到绝对路径
  113. cacheSubFPath := filepath.Join(s.shareRootDir, oneSubInfo.StoreRPath)
  114. if my_util.IsFile(cacheSubFPath) == false {
  115. // 如果文件不存在,那么就删除之前的关联
  116. // 关联删除了,但是不会删除这些对象,所以后续还需要再次删除
  117. s.delSubInfo(&info, &oneSubInfo)
  118. s.log.Debugln("ScanPlayedVideoSubInfo.Clear Sub delSubInfo", oneSubInfo.SubName)
  119. continue
  120. }
  121. dbSubMap[oneSubInfo.StoreRPath] = 0
  122. }
  123. }
  124. // 搜索缓存文件夹所有的字幕出来,对比上面的 map 进行比较
  125. subFiles, err := sub_parser_hub.SearchMatchedSubFile(s.log, s.shareRootDir)
  126. if err != nil {
  127. return
  128. }
  129. for _, file := range subFiles {
  130. subRelPath, err := filepath.Rel(s.shareRootDir, file)
  131. if err != nil {
  132. s.log.Warningln("ScanPlayedVideoSubInfo.Scan.Rel", file, err)
  133. continue
  134. }
  135. _, bok := dbSubMap[subRelPath]
  136. if bok == false {
  137. err = os.Remove(file)
  138. s.log.Debugln("ScanPlayedVideoSubInfo.Clear Sub Remove", file)
  139. if err != nil {
  140. s.log.Debugln("ScanPlayedVideoSubInfo.Clear Sub Remove", file, err)
  141. continue
  142. }
  143. }
  144. }
  145. }
  146. func (s *ScanPlayedVideoSubInfo) Scan() error {
  147. // -----------------------------------------------------
  148. // 并发控制
  149. s.taskControl.SetCtxProcessFunc("ScanSubPlayedPool", s.scan, common.ScanPlayedSubTimeOut)
  150. // -----------------------------------------------------
  151. err := s.taskControl.Invoke(&task_control.TaskData{
  152. Index: 0,
  153. Count: len(s.movieSubMap),
  154. DataEx: ScanInputData{
  155. Videos: s.movieSubMap,
  156. IsMovie: true,
  157. },
  158. })
  159. if err != nil {
  160. s.log.Errorln("ScanPlayedVideoSubInfo.Movie Sub Error", err)
  161. }
  162. err = s.taskControl.Invoke(&task_control.TaskData{
  163. Index: 0,
  164. Count: len(s.seriesSubMap),
  165. DataEx: ScanInputData{
  166. Videos: s.seriesSubMap,
  167. IsMovie: false,
  168. },
  169. })
  170. if err != nil {
  171. s.log.Errorln("ScanPlayedVideoSubInfo.Series Sub Error", err)
  172. }
  173. s.taskControl.Hold()
  174. return nil
  175. }
  176. func (s *ScanPlayedVideoSubInfo) scan(ctx context.Context, inData interface{}) error {
  177. taskData := inData.(*task_control.TaskData)
  178. scanInputData := taskData.DataEx.(ScanInputData)
  179. videoTypes := ""
  180. if scanInputData.IsMovie == true {
  181. videoTypes = "Movie"
  182. } else {
  183. videoTypes = "Series"
  184. }
  185. defer func() {
  186. s.log.Infoln("ScanPlayedVideoSubInfo", videoTypes, "Sub End")
  187. s.log.Infoln("-----------------------------------------------")
  188. }()
  189. s.log.Infoln("-----------------------------------------------")
  190. s.log.Infoln("ScanPlayedVideoSubInfo", videoTypes, "Sub Start...")
  191. shareRootDir, err := my_folder.GetShareSubRootFolder()
  192. if err != nil {
  193. return err
  194. }
  195. imdbInfoCache := make(map[string]*models.IMDBInfo)
  196. index := 0
  197. for videoFPath, orgSubFPath := range scanInputData.Videos {
  198. index++
  199. stage := make(chan interface{}, 1)
  200. go func() {
  201. s.dealOneVideo(index, videoFPath, orgSubFPath, videoTypes, shareRootDir, scanInputData.IsMovie, imdbInfoCache)
  202. stage <- 1
  203. }()
  204. select {
  205. case <-ctx.Done():
  206. {
  207. close(stage)
  208. return errors.New(fmt.Sprintf("cancel at scan: %s", videoFPath))
  209. }
  210. case <-stage:
  211. close(stage)
  212. break
  213. }
  214. }
  215. return nil
  216. }
  217. func (s *ScanPlayedVideoSubInfo) dealOneVideo(index int, videoFPath, orgSubFPath, videoTypes, shareRootDir string,
  218. isMovie bool,
  219. imdbInfoCache map[string]*models.IMDBInfo) {
  220. s.log.Infoln(index, orgSubFPath)
  221. if my_util.IsFile(orgSubFPath) == false {
  222. s.log.Errorln("Skip", orgSubFPath, "not exist")
  223. return
  224. }
  225. s.log.Debugln(0)
  226. // 通过视频的绝对路径,从本地的视频文件对应的 nfo 获取到这个视频的 IMDB ID,
  227. var err error
  228. var imdbInfo4Video types.VideoIMDBInfo
  229. if isMovie == true {
  230. imdbInfo4Video, err = decode.GetImdbInfo4Movie(videoFPath)
  231. } else {
  232. imdbInfo4Video, err = decode.GetSeriesSeasonImdbInfoFromEpisode(videoFPath)
  233. }
  234. if err != nil {
  235. // 如果找不到当前电影的 IMDB Info 本地文件,那么就跳过
  236. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".GetImdbInfo", videoFPath, err)
  237. return
  238. }
  239. s.log.Debugln(1)
  240. // 使用本程序的 hash 的算法,得到视频的唯一 ID
  241. fileHash, err := sub_file_hash.Calculate(videoFPath)
  242. if err != nil {
  243. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".ComputeFileHash", videoFPath, err)
  244. return
  245. }
  246. s.log.Debugln(2)
  247. var imdbInfo *models.IMDBInfo
  248. var ok bool
  249. // 先把 IMDB 信息查询查来,不管是从数据库还是网络(查询出来也得写入到数据库)
  250. if imdbInfo, ok = imdbInfoCache[imdbInfo4Video.ImdbId]; ok == false {
  251. // 不存在,那么就去查询和新建缓存
  252. imdbInfo, err = imdb_helper.GetVideoIMDBInfoFromLocal(s.log, imdbInfo4Video)
  253. if err != nil {
  254. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".GetVideoIMDBInfoFromLocal", videoFPath, err)
  255. return
  256. }
  257. if len(imdbInfo.Description) <= 0 {
  258. // 需要去外网获去补全信息,然后更新本地的信息
  259. t, err := imdb_helper.GetVideoInfoFromIMDBWeb(imdbInfo4Video, s.settings.AdvancedSettings.ProxySettings)
  260. if err != nil {
  261. s.log.Errorln("dealOneVideo.GetVideoInfoFromIMDBWeb,", imdbInfo4Video.Title, err)
  262. return
  263. }
  264. imdbInfo.Year = t.Year
  265. imdbInfo.AKA = t.AKA
  266. imdbInfo.Description = t.Description
  267. imdbInfo.Languages = t.Languages
  268. dao.GetDb().Save(imdbInfo)
  269. }
  270. imdbInfoCache[imdbInfo4Video.ImdbId] = imdbInfo
  271. }
  272. s.log.Debugln(3)
  273. // 当前扫描到的找个字幕的 sha256 是否已经存在与缓存中了
  274. tmpSHA256String, err := my_util.GetFileSHA256String(orgSubFPath)
  275. if err != nil {
  276. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, "orgSubFPath.GetFileSHA256String", videoFPath, err)
  277. return
  278. }
  279. s.log.Debugln(4)
  280. // 判断找到的关联字幕信息是否已经存在了,不存在则新增关联
  281. for _, cacheInfo := range imdbInfo.VideoSubInfos {
  282. if cacheInfo.SHA256 == tmpSHA256String {
  283. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, "SHA256 Exist == true, Skip", orgSubFPath)
  284. return
  285. }
  286. }
  287. s.log.Debugln(5)
  288. // 新增插入
  289. // 把现有的字幕 copy 到缓存目录中
  290. bok, subCacheFPath := sub_share_center.CopySub2Cache(s.log, orgSubFPath, imdbInfo.IMDBID, imdbInfo.Year)
  291. if bok == false {
  292. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".CopySub2Cache", orgSubFPath, err)
  293. return
  294. }
  295. s.log.Debugln(6)
  296. // 不存在,插入,建立关系
  297. bok, fileInfo, err := s.subParserHub.DetermineFileTypeFromFile(subCacheFPath)
  298. if err != nil {
  299. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".DetermineFileTypeFromFile", imdbInfo4Video.ImdbId, err)
  300. return
  301. }
  302. if bok == false {
  303. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".DetermineFileTypeFromFile == false", imdbInfo4Video.ImdbId)
  304. return
  305. }
  306. s.log.Debugln(7)
  307. // 特指 emby 字幕的情况
  308. _, _, _, _, extraSubPreName := s.subFormatter.IsMatchThisFormat(filepath.Base(subCacheFPath))
  309. // 转相对路径存储
  310. subRelPath, err := filepath.Rel(shareRootDir, subCacheFPath)
  311. if err != nil {
  312. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".Rel", imdbInfo4Video.ImdbId, err)
  313. return
  314. }
  315. s.log.Debugln(8)
  316. // 计算需要插入字幕的 sha256
  317. saveSHA256String, err := my_util.GetFileSHA256String(subCacheFPath)
  318. if err != nil {
  319. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, "GetFileSHA256String", videoFPath, err)
  320. return
  321. }
  322. s.log.Debugln(9)
  323. oneVideoSubInfo := models.NewVideoSubInfo(
  324. fileHash,
  325. filepath.Base(subCacheFPath),
  326. language.MyLang2ISO_639_1_String(fileInfo.Lang),
  327. language.IsBilingualSubtitle(fileInfo.Lang),
  328. language.MyLang2ChineseISO(fileInfo.Lang),
  329. fileInfo.Lang.String(),
  330. subRelPath,
  331. extraSubPreName,
  332. saveSHA256String,
  333. )
  334. if isMovie == false {
  335. // 连续剧的时候,如果可能应该获取是 第几季 第几集
  336. torrentInfo, _, err := decode.GetVideoInfoFromFileFullPath(videoFPath)
  337. if err != nil {
  338. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".GetVideoInfoFromFileFullPath", imdbInfo4Video.Title, err)
  339. return
  340. }
  341. oneVideoSubInfo.Season = torrentInfo.Season
  342. oneVideoSubInfo.Episode = torrentInfo.Episode
  343. }
  344. s.log.Debugln(10)
  345. err = dao.GetDb().Model(imdbInfo).Association("VideoSubInfos").Append(oneVideoSubInfo)
  346. if err != nil {
  347. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".Append Association", oneVideoSubInfo.SubName, err)
  348. return
  349. }
  350. }
  351. // 如果文件不存在,那么就删除之前的关联
  352. // 关联删除了,但是不会删除这些对象,所以后续还需要再次删除
  353. func (s *ScanPlayedVideoSubInfo) delSubInfo(imdbInfo *models.IMDBInfo, cacheInfo *models.VideoSubInfo) bool {
  354. err := dao.GetDb().Model(imdbInfo).Association("VideoSubInfos").Delete(cacheInfo)
  355. if err != nil {
  356. s.log.Warningln("ScanPlayedVideoSubInfo.Scan", ".Delete Association", cacheInfo.SubName, err)
  357. return false
  358. }
  359. // 继续删除这个对象
  360. dao.GetDb().Delete(cacheInfo)
  361. s.log.Infoln("Delete Not Exist or SHA256 not the same, Sub Association", cacheInfo.SubName)
  362. return true
  363. }
  364. type ScanInputData struct {
  365. Videos map[string]string
  366. IsMovie bool
  367. }