Browse Source

保存进度

Signed-off-by: allan716 <[email protected]>
allan716 3 years ago
parent
commit
4576992430

+ 220 - 0
internal/logic/scan_played_video_subinfo/scan_played_video_subinfo.go

@@ -0,0 +1,220 @@
+package scan_played_video_subinfo
+
+import (
+	"github.com/allanpk716/ChineseSubFinder/internal/dao"
+	embyHelper "github.com/allanpk716/ChineseSubFinder/internal/logic/emby_helper"
+	"github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser/ass"
+	"github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser/srt"
+	"github.com/allanpk716/ChineseSubFinder/internal/logic/sub_supplier/shooter"
+	"github.com/allanpk716/ChineseSubFinder/internal/models"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/decode"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/imdb_helper"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/language"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/settings"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_parser_hub"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/task_control"
+	"github.com/sirupsen/logrus"
+	"path/filepath"
+	"sync"
+)
+
+type ScanPlayedVideoSubInfo struct {
+	settings settings.Settings
+	log      *logrus.Logger
+
+	embyHelper *embyHelper.EmbyHelper
+
+	taskControl  *task_control.TaskControl
+	canceled     bool
+	canceledLock sync.Mutex
+
+	subParserHub *sub_parser_hub.SubParserHub
+
+	movieSubMap  map[string]string
+	seriesSubMap map[string]string
+}
+
+func NewScanPlayedVideoSubInfo(_settings settings.Settings) (*ScanPlayedVideoSubInfo, error) {
+	var err error
+	var scanPlayedVideoSubInfo ScanPlayedVideoSubInfo
+	scanPlayedVideoSubInfo.log = log_helper.GetLogger()
+	// 参入设置信息
+	scanPlayedVideoSubInfo.settings = _settings
+	// 检测是否某些参数超出范围
+	scanPlayedVideoSubInfo.settings.Check()
+	// 初始化 Emby API 接口
+	if scanPlayedVideoSubInfo.settings.EmbySettings.Enable == true && scanPlayedVideoSubInfo.settings.EmbySettings.AddressUrl != "" && scanPlayedVideoSubInfo.settings.EmbySettings.APIKey != "" {
+		scanPlayedVideoSubInfo.embyHelper = embyHelper.NewEmbyHelper(*scanPlayedVideoSubInfo.settings.EmbySettings)
+	}
+
+	// 初始化任务控制
+	scanPlayedVideoSubInfo.taskControl, err = task_control.NewTaskControl(scanPlayedVideoSubInfo.settings.CommonSettings.Threads, log_helper.GetLogger())
+	if err != nil {
+		return nil, err
+	}
+	// 字幕解析器
+	scanPlayedVideoSubInfo.subParserHub = sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser())
+
+	return &scanPlayedVideoSubInfo, nil
+}
+
+func (s *ScanPlayedVideoSubInfo) Cancel() {
+	s.canceledLock.Lock()
+	s.canceled = true
+	s.canceledLock.Unlock()
+
+	s.taskControl.Release()
+}
+
+func (s *ScanPlayedVideoSubInfo) GetPlayedItemsSubtitle() (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()
+	if err != nil {
+		return false, err
+	}
+
+	return true, nil
+}
+
+func (s *ScanPlayedVideoSubInfo) ScanMovie() error {
+
+	defer func() {
+		s.log.Infoln("ScanPlayedVideoSubInfo Movie Sub End")
+	}()
+
+	s.log.Infoln("-----------------------------------------------")
+	s.log.Infoln("ScanPlayedVideoSubInfo Movie Sub Start...")
+
+	imdbInfoCache := make(map[string]*models.IMDBInfo)
+	for movieFPath, subFPath := range s.movieSubMap {
+
+		// 通过视频的绝对路径,从本地的视频文件对应的 nfo 获取到这个视频的 IMDB ID,
+		imdbInfo4Movie, err := decode.GetImdbInfo4Movie(movieFPath)
+		if err != nil {
+			// 如果找不到当前电影的 IMDB Info 本地文件,那么就跳过
+			s.log.Warningln("ScanPlayedVideoSubInfo.ScanMovie.GetImdbInfo4Movie", movieFPath, err)
+			continue
+		}
+		// 使用 shooter 的技术 hash 的算法,得到视频的唯一 ID
+		fileHash, err := shooter.ComputeFileHash(movieFPath)
+		if err != nil {
+			s.log.Warningln("ScanPlayedVideoSubInfo.ScanMovie.ComputeFileHash", movieFPath, err)
+			continue
+		}
+
+		var imdbInfo *models.IMDBInfo
+		var ok bool
+		if imdbInfo, ok = imdbInfoCache[imdbInfo4Movie.ImdbId]; ok == false {
+			// 不存在,那么就去查询和新建缓存
+			imdbInfo, err = imdb_helper.GetVideoIMDBInfoFromLocal(imdbInfo4Movie.ImdbId, *s.settings.AdvancedSettings.ProxySettings)
+			if err != nil {
+				return err
+			}
+			imdbInfoCache[imdbInfo4Movie.ImdbId] = imdbInfo
+		}
+
+		// 查找关联的 VideoSubInfo
+		var videoSubInfos []models.VideoSubInfo
+		err = dao.GetDb().Model(imdbInfo).Association("VideoSubInfos").Find(&videoSubInfos)
+		if err != nil {
+			return err
+		}
+		// 判断找到的关联字幕信息是否已经存在了,不存在则新增关联
+		var exist bool
+		for _, info := range videoSubInfos {
+
+			// 首先,这里会进行已有缓存字幕是否存在的判断,把不存在的字幕给删除了
+			if my_util.IsFile(info.StoreFPath) == false {
+				// 关联删除了,但是不会删除这些对象,所以后续还需要再次删除
+				err := dao.GetDb().Model(imdbInfo).Association("VideoSubInfos").Delete(&info)
+				if err != nil {
+					s.log.Warningln("ScanPlayedVideoSubInfo.ScanMovie.Delete Association", info.Feature, err)
+					continue
+				}
+				// 继续删除这个对象
+				dao.GetDb().Delete(&info)
+				s.log.Infoln("Delete Not Exist Sub Association", info.SubName, err)
+				continue
+			}
+
+			if info.Feature == fileHash {
+				exist = true
+				break
+			}
+		}
+		if exist == true {
+			// 存在
+			continue
+		}
+		// 不存在,插入,建立关系
+		bok, fileInfo, err := s.subParserHub.DetermineFileTypeFromFile(subFPath)
+		if err != nil {
+			return err
+		}
+		if bok == false {
+			s.log.Warningln("ScanPlayedVideoSubInfo.ScanMovie.DetermineFileTypeFromFile", imdbInfo4Movie.Title, err)
+			continue
+		}
+
+		oneVideoSubInfo := models.NewVideoSubInfo(
+			fileHash,
+			filepath.Base(subFPath),
+			"language_iso",
+			language.IsBilingualSubtitle(fileInfo.Lang),
+			"chinese_iso",
+			fileInfo.Lang.String(),
+			subFPath,
+		)
+
+		err = dao.GetDb().Model(imdbInfo).Association("VideoSubInfos").Append(oneVideoSubInfo)
+		if err != nil {
+			s.log.Warningln("ScanPlayedVideoSubInfo.ScanMovie.Append Association", oneVideoSubInfo.SubName, err)
+			continue
+		}
+
+	}
+
+	return nil
+}
+
+func (s *ScanPlayedVideoSubInfo) ScanSeries() error {
+
+	defer func() {
+		s.log.Infoln("ScanPlayedVideoSubInfo Series Sub End")
+	}()
+
+	s.log.Infoln("ScanPlayedVideoSubInfo Series Sub Start...")
+
+	for episodeFPath, subFPath := range s.seriesSubMap {
+
+		// 通过视频的绝对路径,,从本地的视频文件对应的 nfo 获取到这个视频的 IMDB ID
+		imdbInfo4Series, err := decode.GetSeriesImdbInfoFromEpisode(episodeFPath)
+		if err != nil {
+			// 如果找不到当前电影的 IMDB Info 本地文件,那么就跳过
+			s.log.Warningln("ScanPlayedVideoSubInfo.ScanMovie.GetSeriesImdbInfoFromEpisode", episodeFPath, err)
+			continue
+		}
+		// 使用 shooter 的技术 hash 的算法,得到视频的唯一 ID
+		fileHash, err := shooter.ComputeFileHash(episodeFPath)
+		if err != nil {
+			s.log.Warningln("ScanPlayedVideoSubInfo.ScanMovie.ComputeFileHash", episodeFPath, err)
+			continue
+		}
+
+		println(subFPath)
+		println(imdbInfo4Series)
+		println(fileHash)
+	}
+
+	return nil
+}

+ 2 - 2
internal/logic/sub_supplier/shooter/shooter.go

@@ -81,7 +81,7 @@ func (s Supplier) getSubListFromFile(filePath string) ([]supplier.SubInfo, error
 	var outSubInfoList []supplier.SubInfo
 	var jsonList []SublistShooter
 
-	hash, err := s.computeFileHash(filePath)
+	hash, err := ComputeFileHash(filePath)
 	if err != nil {
 		return nil, err
 	}
@@ -146,7 +146,7 @@ func (s Supplier) getSubInfos(fileHash, fileName, qLan string) ([]SublistShooter
 	return jsonList, nil
 }
 
-func (s Supplier) computeFileHash(filePath string) (string, error) {
+func ComputeFileHash(filePath string) (string, error) {
 	hash := ""
 	fp, err := os.Open(filePath)
 	if err != nil {

+ 8 - 5
internal/models/video_sub_info.go

@@ -2,18 +2,21 @@ package models
 
 import "gorm.io/gorm"
 
-// VideoSubInfo 属于 IMDBInfo,IMDBInfoID 是外键
+// VideoSubInfo 属于 IMDBInfo,IMDBInfoID 是外键,使用了 GORM 的 HasMany 关联
 type VideoSubInfo struct {
 	gorm.Model
 	Feature     string `gorm:"primaryKey" json:"feature"` // 特征码,这个未必有,比如是蓝光格式,分散成多个视频文件的时候,暂定使用 shooter 的特征提前方式
 	SubName     string `json:"sub_name"`                  // 字幕的文件名
-	LanguageISO string `json:"language_iso"`              // 字幕的语言,目标语言,就算是双语,中英,也应该是中文。ISO_639-1_codes 标准,见 ISOLanguage.go 文件
+	Season      int    `json:"season"`                    // 如果对应的是电影则可能是 0,没有
+	Episode     int    `json:"episode"`                   // 如果对应的是电影则可能是 0,没有
+	LanguageISO string `json:"language_iso"`              // 字幕的语言,目标语言,就算是双语,中英,也应该是中文。ISO_639-1_codes 标准,见 ISOLanguage.go 文件,这里无法区分简体繁体
 	IsDouble    bool   `json:"is_double"`                 // 是否是双语,上面是主体语言,比如是中文,
-	ChineseISO  string `json:"chinese_iso"`               // 中文语言编码变种,见 ISOLanguage.go 文件
+	ChineseISO  string `json:"chinese_iso"`               // 中文语言编码变种,见 ISOLanguage.go 文件,这里区分简体繁体
 	MyLanguage  string `json:"my_language"`               // 这个是本程序定义的语言类型,见 my_language.go 文件
+	StoreFPath  string `json:"store_f_path"`              // 字幕存在出本地的哪里绝对路径上
 	IMDBInfoID  uint
 }
 
-func NewVideoSubInfo(feature string, subName string, languageISO string, isDouble bool, chineseISO string, myLanguage string) *VideoSubInfo {
-	return &VideoSubInfo{Feature: feature, SubName: subName, LanguageISO: languageISO, IsDouble: isDouble, ChineseISO: chineseISO, MyLanguage: myLanguage}
+func NewVideoSubInfo(feature string, subName string, languageISO string, isDouble bool, chineseISO string, myLanguage string, storeFPath string) *VideoSubInfo {
+	return &VideoSubInfo{Feature: feature, SubName: subName, LanguageISO: languageISO, IsDouble: isDouble, ChineseISO: chineseISO, MyLanguage: myLanguage, StoreFPath: storeFPath}
 }

+ 37 - 0
internal/pkg/decode/decode.go

@@ -200,6 +200,43 @@ func GetImdbInfo4SeriesDir(seriesDir string) (types.VideoIMDBInfo, error) {
 	return imdbInfo, nil
 }
 
+func GetSeriesImdbInfoFromEpisode(oneEpFPath string) (types.VideoIMDBInfo, error) {
+
+	var err error
+	// 当前季的路径
+	EPdir := filepath.Dir(oneEpFPath)
+	// 先判断是否存在 tvshow.nfo
+	nfoFilePath := ""
+	dir, err := os.ReadDir(EPdir)
+	for _, fi := range dir {
+		if fi.IsDir() == true {
+			continue
+		}
+		upperName := strings.ToUpper(fi.Name())
+		if upperName == strings.ToUpper(MetadateTVNfo) {
+			// 连续剧的 nfo 文件
+			nfoFilePath = filepath.Join(EPdir, fi.Name())
+			break
+		}
+	}
+	if nfoFilePath == "" {
+
+		// 没有找到,那么就向上一级再次找
+		seasonDir := filepath.Base(EPdir)
+		seriesDir := EPdir[:len(EPdir)-len(seasonDir)]
+
+		return GetImdbInfo4SeriesDir(seriesDir)
+
+	} else {
+		var imdbInfo types.VideoIMDBInfo
+		imdbInfo, err = getImdbAndYearNfo(nfoFilePath, "tvshow")
+		if err != nil {
+			return types.VideoIMDBInfo{}, err
+		}
+		return imdbInfo, nil
+	}
+}
+
 func GetImdbInfo4OneSeriesEpisode(oneEpFPath string) (types.VideoIMDBInfo, error) {
 
 	// 从这一集的视频文件全路径去推算对应的 nfo 文件是否存在

+ 28 - 20
internal/pkg/imdb_helper/imdb.go

@@ -25,11 +25,8 @@ func GetVideoInfoFromIMDB(imdbID string, _proxySettings ...settings.ProxySetting
 	return t, nil
 }
 
-// IsChineseVideo 从 imdbID 去查询判断是否是中文视频
-func IsChineseVideo(imdbID string, _proxySettings ...settings.ProxySettings) (bool, *models.IMDBInfo, error) {
-
-	const chName0 = "chinese"
-	const chName1 = "mandarin"
+// GetVideoIMDBInfoFromLocal 从本地获取 IMDB 信息,如果找不到则去网络获取并写入本地缓存
+func GetVideoIMDBInfoFromLocal(imdbID string, _proxySettings ...settings.ProxySettings) (*models.IMDBInfo, error) {
 
 	var proxySettings settings.ProxySettings
 	if len(_proxySettings) > 0 {
@@ -40,12 +37,11 @@ func IsChineseVideo(imdbID string, _proxySettings ...settings.ProxySettings) (bo
 	var imdbInfos []models.IMDBInfo
 	dao.GetDb().Limit(1).Where(&models.IMDBInfo{IMDBID: imdbID}).Find(&imdbInfos)
 
-	var firstLangLowCase string
 	if len(imdbInfos) <= 0 {
-		// 没有找到
+		// 没有找到,去网上获取
 		t, err := GetVideoInfoFromIMDB(imdbID, proxySettings)
 		if err != nil {
-			return false, nil, err
+			return nil, err
 		}
 		// 存入数据库
 		nowIMDBInfo := models.NewIMDBInfo(imdbID, t.Name, t.Year, t.Description, t.Languages, t.AKA)
@@ -53,25 +49,37 @@ func IsChineseVideo(imdbID string, _proxySettings ...settings.ProxySettings) (bo
 		imdbInfos = append(imdbInfos, *nowIMDBInfo)
 		dao.GetDb().Create(nowIMDBInfo)
 
-		if len(t.Languages) <= 0 {
-			return false, nil, nil
-		}
-
-		firstLangLowCase = strings.ToLower(t.Languages[0])
-
+		return nowIMDBInfo, nil
 	} else {
 		// 找到
-		if len(imdbInfos[0].Languages) <= 0 {
-			return false, nil, nil
-		}
+		return &imdbInfos[0], nil
+	}
+}
+
+// IsChineseVideo 从 imdbID 去查询判断是否是中文视频
+func IsChineseVideo(imdbID string, _proxySettings ...settings.ProxySettings) (bool, *models.IMDBInfo, error) {
+
+	const chName0 = "chinese"
+	const chName1 = "mandarin"
+
+	var proxySettings settings.ProxySettings
+	if len(_proxySettings) > 0 {
+		proxySettings = _proxySettings[0]
+	}
 
-		firstLangLowCase = strings.ToLower(imdbInfos[0].Languages[0])
+	localIMDBInfo, err := GetVideoIMDBInfoFromLocal(imdbID, proxySettings)
+	if err != nil {
+		return false, nil, err
+	}
+	if len(localIMDBInfo.Languages) <= 0 {
+		return false, nil, nil
 	}
+	firstLangLowCase := strings.ToLower(localIMDBInfo.Languages[0])
 	// 判断第一语言是否是中文
 	switch firstLangLowCase {
 	case chName0, chName1:
-		return true, &imdbInfos[0], nil
+		return true, localIMDBInfo, nil
 	default:
-		return false, &imdbInfos[0], nil
+		return false, localIMDBInfo, nil
 	}
 }

+ 52 - 6
internal/pkg/my_util/folder.go

@@ -1,6 +1,7 @@
 package my_util
 
 import (
+	"fmt"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/get_access_time"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/global_value"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
@@ -313,6 +314,50 @@ func GetSubFixCacheFolderByName(folderName string) (string, error) {
 	return tmpFolderFullPath, nil
 }
 
+// --------------------------------------------------------------
+// Share Sub Cache
+// --------------------------------------------------------------
+
+// GetShareSubRootFolder 在程序的根目录新建,字幕共享的缓存根目录,下级还有具体是按发行的时间去划分的子集目录
+func GetShareSubRootFolder() (string, error) {
+
+	nowProcessRoot, err := os.Getwd()
+	if err != nil {
+		return "", err
+	}
+	nowProcessRoot = filepath.Join(nowProcessRoot, cacheRootFolderName, ShareSubFileCache)
+	err = os.MkdirAll(nowProcessRoot, os.ModePerm)
+	if err != nil {
+		return "", err
+	}
+	return nowProcessRoot, err
+}
+
+// GetShareFolderByYear 缓存的文件夹以发行的年为一个单位存储
+func GetShareFolderByYear(year int) (string, error) {
+	rootPath, err := GetShareSubRootFolder()
+	if err != nil {
+		return "", err
+	}
+	tmpFolderFullPath := filepath.Join(rootPath, fmt.Sprintf("%d", year))
+	err = os.MkdirAll(tmpFolderFullPath, os.ModePerm)
+	if err != nil {
+		return "", err
+	}
+	return tmpFolderFullPath, nil
+}
+
+// ClearShareSubFolderByYear 清理指定的缓存文件夹
+func ClearShareSubFolderByYear(year int) error {
+
+	nowTmpFolder, err := GetShareFolderByYear(year)
+	if err != nil {
+		return err
+	}
+
+	return ClearFolder(nowTmpFolder)
+}
+
 // --------------------------------------------------------------
 // Common
 // --------------------------------------------------------------
@@ -436,12 +481,13 @@ func ClearIdleSubFixCacheFolder(rootSubFixCacheFolder string, outOfDate time.Dur
 
 // 缓存文件的位置信息,都是在程序的根目录下的 cache 中
 const (
-	cacheRootFolderName = "cache"           // 缓存文件夹总名称
-	TmpFolder           = "tmp"             // 临时缓存的文件夹
-	RodCacheFolder      = "rod"             // rod 的缓存目录
-	PluginFolder        = "Plugin"          // 插件的目录
-	DebugFolder         = "CSF-DebugThings" // 调试相关的文件夹
-	SubFixCacheFolder   = "CSF-SubFixCache" // 字幕时间校正的缓存文件夹,一般可以不清理
+	cacheRootFolderName = "cache"             // 缓存文件夹总名称
+	TmpFolder           = "tmp"               // 临时缓存的文件夹
+	RodCacheFolder      = "rod"               // rod 的缓存目录
+	PluginFolder        = "Plugin"            // 插件的目录
+	DebugFolder         = "CSF-DebugThings"   // 调试相关的文件夹
+	SubFixCacheFolder   = "CSF-SubFixCache"   // 字幕时间校正的缓存文件夹,一般可以不清理
+	ShareSubFileCache   = "CSF-ShareSubCache" // 字幕共享的缓存目录,不建议删除
 )
 
 const (