123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- package scan_played_video_subinfo
- import (
- "errors"
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "sync"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/ifaces"
- common2 "github.com/ChineseSubFinder/ChineseSubFinder/pkg/types/common"
- embyHelper "github.com/ChineseSubFinder/ChineseSubFinder/pkg/logic/emby_helper"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/logic/file_downloader"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/mix_media_info"
- "github.com/ChineseSubFinder/ChineseSubFinder/internal/dao"
- "github.com/ChineseSubFinder/ChineseSubFinder/internal/models"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/decode"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/imdb_helper"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/language"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/settings"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/sub_file_hash"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/sub_formatter/emby"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/sub_parser_hub"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/sub_share_center"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/task_control"
- "github.com/sirupsen/logrus"
- "golang.org/x/net/context"
- )
- type ScanPlayedVideoSubInfo struct {
- log *logrus.Logger
- fileDownloader *file_downloader.FileDownloader
- embyHelper *embyHelper.EmbyHelper
- taskControl *task_control.TaskControl
- canceled bool
- canceledLock sync.Mutex
- movieSubMap map[string]string
- seriesSubMap map[string]string
- subFormatter ifaces.ISubFormatter
- shareRootDir string
- imdbInfoCache map[string]*models.IMDBInfo
- cacheImdbInfoCacheLocker sync.Mutex
- }
- func NewScanPlayedVideoSubInfo(log *logrus.Logger, fileDownloader *file_downloader.FileDownloader) (*ScanPlayedVideoSubInfo, error) {
- var err error
- var scanPlayedVideoSubInfo ScanPlayedVideoSubInfo
- scanPlayedVideoSubInfo.log = log
- // 下载实例
- scanPlayedVideoSubInfo.fileDownloader = fileDownloader
- // 检测是否某些参数超出范围
- settings.Get().Check()
- // 初始化 Emby API 接口
- if settings.Get().EmbySettings.Enable == true && settings.Get().EmbySettings.AddressUrl != "" &&
- settings.Get().EmbySettings.APIKey != "" {
- scanPlayedVideoSubInfo.embyHelper = embyHelper.NewEmbyHelper(fileDownloader.MediaInfoDealers)
- }
- // 初始化任务控制
- scanPlayedVideoSubInfo.taskControl, err = task_control.NewTaskControl(settings.Get().CommonSettings.Threads, log)
- if err != nil {
- return nil, err
- }
- // 字幕命名格式解析器
- scanPlayedVideoSubInfo.subFormatter = emby.NewFormatter()
- // 缓存目录的根目录
- shareRootDir, err := pkg.GetShareSubRootFolder()
- if err != nil {
- return nil, err
- }
- scanPlayedVideoSubInfo.shareRootDir = shareRootDir
- // 初始化缓存
- scanPlayedVideoSubInfo.imdbInfoCache = make(map[string]*models.IMDBInfo)
- return &scanPlayedVideoSubInfo, nil
- }
- func (s *ScanPlayedVideoSubInfo) Cancel() {
- defer func() {
- s.log.Infoln("ScanPlayedVideoSubInfo.Cancel()")
- }()
- s.canceledLock.Lock()
- s.canceled = true
- s.canceledLock.Unlock()
- s.taskControl.Release()
- }
- func (s *ScanPlayedVideoSubInfo) GetPlayedItemsSubtitle(embySettings *settings.EmbySettings, maxRequestVideoNumber int) (bool, error) {
- var err error
- // 是否是通过 emby_helper api 获取的列表
- if s.embyHelper == nil {
- // 没有填写 emby_helper api 的信息,那么就跳过
- s.log.Infoln("Skip ScanPlayedVideoSubInfo, Emby Settings is null")
- return false, nil
- }
- s.movieSubMap, s.seriesSubMap, err = s.embyHelper.GetPlayedItemsSubtitle(embySettings, maxRequestVideoNumber)
- if err != nil {
- return false, err
- }
- return true, nil
- }
- // Clear 清理无效的缓存字幕信息
- func (s *ScanPlayedVideoSubInfo) Clear() {
- defer func() {
- s.log.Infoln("ScanPlayedVideoSubInfo.Clear Sub End")
- s.log.Infoln("----------------------------------------------")
- }()
- s.log.Infoln("-----------------------------------------------")
- s.log.Infoln("ScanPlayedVideoSubInfo.Clear Sub Start...")
- var imdbInfos []models.IMDBInfo
- // 把嵌套关联的 has many 的信息都查询出来
- dao.GetDb().Preload("VideoSubInfos").Find(&imdbInfos)
- // 同时需要把不在数据库记录的字幕给删除,那么就需要把数据库查询出来的给做成 map
- dbSubMap := make(map[string]int)
- for _, info := range imdbInfos {
- for _, oneSubInfo := range info.VideoSubInfos {
- s.log.Infoln("ScanPlayedVideoSubInfo.Clear Sub", oneSubInfo.SubName)
- // 转换到绝对路径
- cacheSubFPath := filepath.Join(s.shareRootDir, oneSubInfo.StoreRPath)
- if pkg.IsFile(cacheSubFPath) == false {
- // 如果文件不存在,那么就删除之前的关联
- // 关联删除了,但是不会删除这些对象,所以后续还需要再次删除
- s.delSubInfo(&info, &oneSubInfo)
- s.log.Debugln("ScanPlayedVideoSubInfo.Clear Sub delSubInfo", oneSubInfo.SubName)
- continue
- }
- dbSubMap[oneSubInfo.StoreRPath] = 0
- }
- }
- // 搜索缓存文件夹所有的字幕出来,对比上面的 map 进行比较
- subFiles, err := sub_parser_hub.SearchMatchedSubFile(s.log, s.shareRootDir)
- if err != nil {
- return
- }
- for _, file := range subFiles {
- subRelPath, err := filepath.Rel(s.shareRootDir, file)
- if err != nil {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan.Rel", file, err)
- continue
- }
- _, bok := dbSubMap[subRelPath]
- if bok == false {
- err = os.Remove(file)
- s.log.Debugln("ScanPlayedVideoSubInfo.Clear Sub Remove", file)
- if err != nil {
- s.log.Debugln("ScanPlayedVideoSubInfo.Clear Sub Remove", file, err)
- continue
- }
- }
- }
- }
- func (s *ScanPlayedVideoSubInfo) Scan() error {
- // Emby 观看的列表
- {
- // 从数据库中查询出所有的 IMDBInfo
- // 清空缓存
- s.imdbInfoCache = make(map[string]*models.IMDBInfo)
- // -----------------------------------------------------
- // 并发控制
- s.taskControl.SetCtxProcessFunc("ScanSubPlayedPool", s.scan, common2.ScanPlayedSubTimeOut)
- // -----------------------------------------------------
- err := s.taskControl.Invoke(&task_control.TaskData{
- Index: 0,
- Count: len(s.movieSubMap),
- DataEx: ScanInputData{
- Videos: s.movieSubMap,
- IsMovie: true,
- },
- })
- if err != nil {
- s.log.Errorln("ScanPlayedVideoSubInfo.Movie Sub Error", err)
- }
- err = s.taskControl.Invoke(&task_control.TaskData{
- Index: 0,
- Count: len(s.seriesSubMap),
- DataEx: ScanInputData{
- Videos: s.seriesSubMap,
- IsMovie: false,
- },
- })
- if err != nil {
- s.log.Errorln("ScanPlayedVideoSubInfo.Series Sub Error", err)
- }
- s.taskControl.Hold()
- }
- // 使用 Http API 标记的已观看列表
- {
- // TODO 暂时屏蔽掉 http api 提交的已看字幕的接口上传
- if false {
- // 下面需要把给出外部的 HTTP API 提交的视频和字幕信息(ThirdPartSetVideoPlayedInfo)进行判断,存入数据库
- shareRootDir, err := pkg.GetShareSubRootFolder()
- if err != nil {
- return err
- }
- var videoPlayedInfos []models.ThirdPartSetVideoPlayedInfo
- dao.GetDb().Find(&videoPlayedInfos)
- for i, thirdPartSetVideoPlayedInfo := range videoPlayedInfos {
- // 先要判断这个是 Movie 还是 Series
- // 因为设计这个 API 的时候为了简化提交的参数,也假定传入的可能不是正确的分类(电影or连续剧)
- // 所以只能比较傻的,低效率的匹配映射的目录来做到识别是哪个分类的
- bFoundMovie := false
- bFoundSeries := false
- for _, moviePath := range settings.Get().CommonSettings.MoviePaths {
- // 先判断类型是否是 Movie
- if strings.HasPrefix(thirdPartSetVideoPlayedInfo.PhysicalVideoFileFullPath, moviePath) == true {
- bFoundMovie = true
- break
- }
- }
- if bFoundMovie == false {
- for _, seriesPath := range settings.Get().CommonSettings.SeriesPaths {
- // 判断是否是 Series
- if strings.HasPrefix(thirdPartSetVideoPlayedInfo.PhysicalVideoFileFullPath, seriesPath) == true {
- bFoundSeries = true
- break
- }
- }
- }
- if bFoundMovie == false && bFoundSeries == false {
- // 说明提交的这个视频文件无法匹配电影或者连续剧的目录前缀
- s.log.Warningln("Not matched Movie and Series Prefix Path", thirdPartSetVideoPlayedInfo.PhysicalVideoFileFullPath)
- continue
- }
- IsMovie := false
- videoTypes := common2.Movie
- if bFoundMovie == true {
- videoTypes = common2.Movie
- IsMovie = true
- }
- if bFoundSeries == true {
- videoTypes = common2.Series
- IsMovie = false
- }
- tmpSubFPath := filepath.Join(filepath.Dir(thirdPartSetVideoPlayedInfo.PhysicalVideoFileFullPath), thirdPartSetVideoPlayedInfo.SubName)
- s.dealOneVideo(i, thirdPartSetVideoPlayedInfo.PhysicalVideoFileFullPath, tmpSubFPath, videoTypes.String(), shareRootDir, IsMovie, s.imdbInfoCache)
- }
- }
- }
- return nil
- }
- func (s *ScanPlayedVideoSubInfo) scan(ctx context.Context, inData interface{}) error {
- taskData := inData.(*task_control.TaskData)
- scanInputData := taskData.DataEx.(ScanInputData)
- videoTypes := ""
- if scanInputData.IsMovie == true {
- videoTypes = "Movie"
- } else {
- videoTypes = "Series"
- }
- defer func() {
- s.log.Infoln("ScanPlayedVideoSubInfo", videoTypes, "Sub End")
- s.log.Infoln("-----------------------------------------------")
- }()
- s.log.Infoln("-----------------------------------------------")
- s.log.Infoln("ScanPlayedVideoSubInfo", videoTypes, "Sub Start...")
- shareRootDir, err := pkg.GetShareSubRootFolder()
- if err != nil {
- return err
- }
- index := 0
- for videoFPath, orgSubFPath := range scanInputData.Videos {
- index++
- stage := make(chan interface{}, 1)
- go func() {
- defer func() {
- close(stage)
- }()
- s.dealOneVideo(index, videoFPath, orgSubFPath, videoTypes, shareRootDir, scanInputData.IsMovie, s.imdbInfoCache)
- stage <- 1
- }()
- select {
- case <-ctx.Done():
- {
- return errors.New(fmt.Sprintf("cancel at scan: %s", videoFPath))
- }
- case <-stage:
- break
- }
- }
- return nil
- }
- func (s *ScanPlayedVideoSubInfo) dealOneVideo(index int, videoFPath, orgSubFPath, videoTypes, shareRootDir string,
- isMovie bool,
- imdbInfoCache map[string]*models.IMDBInfo) {
- s.log.Infoln(index, orgSubFPath)
- if pkg.IsFile(orgSubFPath) == false {
- s.log.Errorln("Skip", orgSubFPath, "not exist")
- return
- }
- s.log.Debugln(0)
- // 通过视频的绝对路径,从本地的视频文件对应的 nfo 获取到这个视频的 IMDB ID,
- var err error
- imdbInfoFromVideoFile, err := imdb_helper.GetIMDBInfoFromVideoFile(s.fileDownloader.MediaInfoDealers, videoFPath, isMovie)
- if err != nil {
- s.log.Errorln("GetIMDBInfoFromVideoFile", err)
- return
- }
- s.log.Debugln(1)
- // 使用本程序的 hash 的算法,得到视频的唯一 ID
- fileHash, err := sub_file_hash.Calculate(videoFPath)
- if err != nil {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".ComputeFileHash", videoFPath, err)
- return
- }
- s.log.Debugln(2)
- var imdbInfo *models.IMDBInfo
- var ok bool
- // 先把 IMDB 信息查询查来,不管是从数据库还是网络(查询出来也得写入到数据库)
- s.cacheImdbInfoCacheLocker.Lock()
- imdbInfo, ok = imdbInfoCache[imdbInfoFromVideoFile.IMDBID]
- s.cacheImdbInfoCacheLocker.Unlock()
- if ok == false {
- s.cacheImdbInfoCacheLocker.Lock()
- imdbInfoCache[imdbInfoFromVideoFile.IMDBID] = imdbInfoFromVideoFile
- imdbInfo = imdbInfoFromVideoFile
- s.cacheImdbInfoCacheLocker.Unlock()
- }
- s.log.Debugln(3)
- // 这里需要判断是否已经获取了 TMDB Info,如果没有则需要去获取
- if imdbInfo.TmdbId == "" {
- s.log.Debugln("3-2")
- videoType := "movie"
- if imdbInfo.IsMovie == false {
- videoType = "series"
- }
- _, err = mix_media_info.GetMediaInfoAndSave(
- s.fileDownloader.MediaInfoDealers,
- imdbInfo,
- imdbInfo.IMDBID, "imdb", videoType)
- if err != nil {
- s.log.Errorln("dealOneVideo.GetMediaInfoAndSave,", imdbInfo.Name, err)
- return
- }
- }
- s.log.Debugln("3-2")
- // 当前扫描到的找个字幕的 sha256 是否已经存在与缓存中了
- tmpSHA256String, err := pkg.GetFileSHA256String(orgSubFPath)
- if err != nil {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, "orgSubFPath.GetFileSHA256String", videoFPath, err)
- return
- }
- s.log.Debugln(4)
- // 判断找到的关联字幕信息是否已经存在了,不存在则新增关联
- for _, cacheInfo := range imdbInfo.VideoSubInfos {
- if cacheInfo.SHA256 == tmpSHA256String {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, "SHA256 Exist == true, Skip", orgSubFPath)
- return
- }
- }
- s.log.Debugln(5)
- // 新增插入
- // 把现有的字幕 copy 到缓存目录中
- bok, subCacheFPath := sub_share_center.CopySub2Cache(s.log, orgSubFPath, imdbInfo.IMDBID, imdbInfo.Year, false)
- if bok == false {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".CopySub2Cache", orgSubFPath, err)
- return
- }
- s.log.Debugln(6)
- // 不存在,插入,建立关系
- bok, fileInfo, err := s.fileDownloader.SubParserHub.DetermineFileTypeFromFile(subCacheFPath)
- if err != nil {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".DetermineFileTypeFromFile", imdbInfo.IMDBID, err)
- return
- }
- if bok == false {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".DetermineFileTypeFromFile == false", imdbInfo.IMDBID)
- return
- }
- s.log.Debugln(7)
- // 特指 emby 字幕的情况
- _, _, _, _, extraSubPreName := s.subFormatter.IsMatchThisFormat(filepath.Base(subCacheFPath))
- // 转相对路径存储
- subRelPath, err := filepath.Rel(shareRootDir, subCacheFPath)
- if err != nil {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".Rel", imdbInfo.IMDBID, err)
- return
- }
- s.log.Debugln(8)
- // 计算需要插入字幕的 sha256
- saveSHA256String, err := pkg.GetFileSHA256String(subCacheFPath)
- if err != nil {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, "GetFileSHA256String", videoFPath, err)
- return
- }
- // 这个字幕文件是否已经存在了
- var videoSubInfos []models.VideoSubInfo
- dao.GetDb().Where("sha256 = ?", saveSHA256String).Find(&videoSubInfos)
- if len(videoSubInfos) > 0 {
- // 存在,跳过
- s.log.Infoln("ScanPlayedVideoSubInfo.Scan", videoTypes, "SHA256 Exist == true, Skip", orgSubFPath)
- return
- }
- s.log.Debugln(9)
- // 如果不存在,那么就标记这个字幕是未发送
- oneVideoSubInfo := models.NewVideoSubInfo(
- fileHash,
- filepath.Base(subCacheFPath),
- language.MyLang2ISO_639_1_String(fileInfo.Lang),
- language.IsBilingualSubtitle(fileInfo.Lang),
- language.MyLang2ChineseISO(fileInfo.Lang),
- fileInfo.Lang.String(),
- subRelPath,
- extraSubPreName,
- saveSHA256String,
- isMovie,
- )
- oneVideoSubInfo.IsSend = false
- if isMovie == false {
- // 连续剧的时候,如果可能应该获取是 第几季 第几集
- epsVideoNfoInfo, err := decode.GetVideoNfoInfo4OneSeriesEpisode(videoFPath)
- if err != nil {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".GetVideoNfoInfo4OneSeriesEpisode", imdbInfo.Name, err)
- return
- }
- oneVideoSubInfo.Season = epsVideoNfoInfo.Season
- oneVideoSubInfo.Episode = epsVideoNfoInfo.Episode
- }
- s.log.Debugln(10)
- err = dao.GetDb().Model(imdbInfo).Association("VideoSubInfos").Append(oneVideoSubInfo)
- if err != nil {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan", videoTypes, ".Append Association", oneVideoSubInfo.SubName, err)
- return
- }
- }
- // 如果文件不存在,那么就删除之前的关联
- // 关联删除了,但是不会删除这些对象,所以后续还需要再次删除
- func (s *ScanPlayedVideoSubInfo) delSubInfo(imdbInfo *models.IMDBInfo, cacheInfo *models.VideoSubInfo) bool {
- err := dao.GetDb().Model(imdbInfo).Association("VideoSubInfos").Delete(cacheInfo)
- if err != nil {
- s.log.Warningln("ScanPlayedVideoSubInfo.Scan", ".Delete Association", cacheInfo.SubName, err)
- return false
- }
- // 继续删除这个对象
- dao.GetDb().Delete(cacheInfo)
- s.log.Infoln("Delete Not Exist or SHA256 not the same, Sub Association", cacheInfo.SubName)
- return true
- }
- type ScanInputData struct {
- Videos map[string]string
- IsMovie bool
- }
|