浏览代码

新增,视频和字幕预览后端功能

Signed-off-by: allan716 <[email protected]>
allan716 3 年之前
父节点
当前提交
ad29192ace

+ 1 - 1
cmd/chinesesubfinder/main.go

@@ -125,7 +125,7 @@ func main() {
 		common.SetApiToken("")
 	}
 	// 是否开启开发模式,跳过某些流程
-	settings.Get().SpeedDevMode = false
+	settings.Get().SpeedDevMode = true
 	err := settings.Get().Save()
 	if err != nil {
 		loggerBase.Panicln("settings.Get().Save() err:", err)

+ 9 - 0
internal/backend/backend.go

@@ -8,6 +8,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/allanpk716/ChineseSubFinder/pkg"
+
 	"github.com/allanpk716/ChineseSubFinder/pkg/local_http_proxy_server"
 	"github.com/allanpk716/ChineseSubFinder/pkg/settings"
 
@@ -68,6 +70,13 @@ func (b *BackEnd) start() {
 	engine.StaticFS(dist.SpaFolderFonts, dist.Assets(dist.SpaFolderName+dist.SpaFolderFonts, dist.SpaFonts))
 	engine.StaticFS(dist.SpaFolderIcons, dist.Assets(dist.SpaFolderName+dist.SpaFolderIcons, dist.SpaIcons))
 	engine.StaticFS(dist.SpaFolderImages, dist.Assets(dist.SpaFolderName+dist.SpaFolderImages, dist.SpaImages))
+	// 用于预览视频和字幕的静态文件服务
+	previewCacheFolder, err := pkg.GetVideoAndSubPreviewCacheFolder()
+	if err != nil {
+		b.logger.Errorln("GetVideoAndSubPreviewCacheFolder Error:", err)
+		return
+	}
+	engine.StaticFS("/static/preview", http.Dir(previewCacheFolder))
 
 	engine.Any("/api", func(c *gin.Context) {
 		c.Redirect(http.StatusMovedPermanently, "/")

+ 6 - 0
internal/backend/base_router.go

@@ -117,6 +117,12 @@ func InitRouter(
 		GroupV1.POST("/subtitles/manual_upload_2_local", cbV1.ManualUploadSubtitle2Local)
 		GroupV1.GET("/subtitles/list_manual_upload_2_local_job", cbV1.ListManualUploadSubtitle2LocalJob)
 		GroupV1.POST("/subtitles/is_manual_upload_2_local_in_queue", cbV1.IsManualUploadSubtitle2LocalJobInQueue)
+
+		GroupV1.POST("/preview/add", cbV1.PreviewAdd)
+		GroupV1.GET("/preview/list", cbV1.PreviewList)
+		GroupV1.POST("/preview/is_in_queue", cbV1.PreviewIsJobInQueue)
+		GroupV1.POST("/preview/export_info", cbV1.PreviewGetExportInfo)
+		GroupV1.POST("/preview/clean_up", cbV1.PreviewCleanUp)
 	}
 
 	GroupAPIV1 := router.Group("/api/v1")

+ 109 - 0
internal/backend/controllers/v1/preview.go

@@ -0,0 +1,109 @@
+package v1
+
+import (
+	"net/http"
+	"strconv"
+
+	"github.com/allanpk716/ChineseSubFinder/pkg"
+
+	"github.com/allanpk716/ChineseSubFinder/pkg/preview_queue"
+	backend2 "github.com/allanpk716/ChineseSubFinder/pkg/types/backend"
+	"github.com/gin-gonic/gin"
+)
+
+// PreviewAdd 添加需要预览的任务
+func (cb *ControllerBase) PreviewAdd(c *gin.Context) {
+
+	var err error
+	defer func() {
+		// 统一的异常处理
+		cb.ErrorProcess(c, "PreviewAdd", err)
+	}()
+
+	job := preview_queue.Job{}
+	err = c.ShouldBindJSON(&job)
+	if err != nil {
+		return
+	}
+
+	cb.cronHelper.Downloader.PreviewQueue.Add(&job)
+	c.JSON(http.StatusOK, backend2.ReplyCommon{Message: "ok"})
+	return
+}
+
+// PreviewList 列举预览任务
+func (cb *ControllerBase) PreviewList(c *gin.Context) {
+
+	var err error
+	defer func() {
+		// 统一的异常处理
+		cb.ErrorProcess(c, "PreviewList", err)
+	}()
+
+	listJob := cb.cronHelper.Downloader.PreviewQueue.ListJob()
+	c.JSON(http.StatusOK, preview_queue.Reply{
+		Jobs: listJob,
+	})
+}
+
+// PreviewIsJobInQueue 预览的任务是否在列表中,或者说是在执行中
+func (cb *ControllerBase) PreviewIsJobInQueue(c *gin.Context) {
+	var err error
+	defer func() {
+		// 统一的异常处理
+		cb.ErrorProcess(c, "PreviewIsJobInQueue", err)
+	}()
+
+	job := preview_queue.Job{}
+	err = c.ShouldBindJSON(&job)
+	if err != nil {
+		return
+	}
+
+	found := cb.cronHelper.Downloader.PreviewQueue.IsJobInQueue(&preview_queue.Job{
+		VideoFPath: job.VideoFPath,
+	})
+
+	c.JSON(http.StatusOK, backend2.ReplyCommon{Message: strconv.FormatBool(found)})
+	return
+}
+
+// PreviewGetExportInfo 预览的任务的导出信息
+func (cb *ControllerBase) PreviewGetExportInfo(c *gin.Context) {
+	var err error
+	defer func() {
+		// 统一的异常处理
+		cb.ErrorProcess(c, "PreviewGetExportInfo", err)
+	}()
+
+	job := preview_queue.Job{}
+	err = c.ShouldBindJSON(&job)
+	if err != nil {
+		return
+	}
+
+	m3u8, subPath, err := cb.cronHelper.Downloader.PreviewQueue.GetVideoHLSAndSubByTimeRangeExportPathInfo(job.VideoFPath, job.SubFPath, job.StartTime, job.EndTime)
+	if err != nil {
+		return
+	}
+
+	c.JSON(http.StatusOK, preview_queue.Job{
+		VideoFPath: m3u8,
+		SubFPath:   subPath,
+	})
+	return
+}
+
+func (cb *ControllerBase) PreviewCleanUp(c *gin.Context) {
+	var err error
+	defer func() {
+		// 统一的异常处理
+		cb.ErrorProcess(c, "PreviewCleanUp", err)
+	}()
+
+	err = pkg.ClearVideoAndSubPreviewCacheFolder()
+	if err != nil {
+		return
+	}
+	c.JSON(http.StatusOK, backend2.ReplyCommon{Message: "ok"})
+}

+ 4 - 0
pkg/downloader/downloader.go

@@ -5,6 +5,8 @@ import (
 	"fmt"
 	"sync"
 
+	"github.com/allanpk716/ChineseSubFinder/pkg/preview_queue"
+
 	"github.com/allanpk716/ChineseSubFinder/pkg/manual_upload_sub_2_local"
 
 	"github.com/allanpk716/ChineseSubFinder/pkg/save_sub_helper"
@@ -48,6 +50,7 @@ type Downloader struct {
 	ScanLogic                *scan_logic.ScanLogic                            // 是否扫描逻辑
 	SaveSubHelper            *save_sub_helper.SaveSubHelper                   // 保存字幕的逻辑
 	ManualUploadSub2Local    *manual_upload_sub_2_local.ManualUploadSub2Local // 手动上传字幕到本地
+	PreviewQueue             *preview_queue.PreviewQueue                      // 预览队列
 
 	cacheLocker   sync.Mutex
 	movieInfoMap  map[string]MovieInfo  // 给 Web 界面使用的,Key: VideoFPath
@@ -103,6 +106,7 @@ func NewDownloader(inSubFormatter ifaces.ISubFormatter, fileDownloader *file_dow
 		downloader.subTimelineFixerHelperEx)
 
 	downloader.ManualUploadSub2Local = manual_upload_sub_2_local.NewManualUploadSub2Local(downloader.log, downloader.SaveSubHelper, downloader.ScanLogic)
+	downloader.PreviewQueue = preview_queue.NewPreviewQueue(downloader.log)
 
 	downloader.movieInfoMap = make(map[string]MovieInfo)
 	downloader.seasonInfoMap = make(map[string]SeasonInfo)

+ 2 - 2
pkg/ffmpeg_helper/ffmpeg_helper_test.go

@@ -134,8 +134,8 @@ func TestExportVideoHLSAndSubByTimeRange(t *testing.T) {
 
 	outDirPath := "C:\\Tmp\\media\\test\\hls"
 	videoFPath := "C:\\Tmp\\media\\test\\Chainsaw Man - S01E02 - ARRIVAL IN TOKYO HDTV-1080p.mp4"
-	//subFPath := "C:\\Tmp\\media\\test\\Chainsaw Man - S01E02 - ARRIVAL IN TOKYO HDTV-1080p.chinese(简,csf).default.srt"
-	subFPath := "C:\\Tmp\\media\\test\\Three Thousand Years of Longing (2022) WEBDL-1080p.chinese(简英,assrt).ass"
+	subFPath := "C:\\Tmp\\media\\test\\Chainsaw Man - S01E02 - ARRIVAL IN TOKYO HDTV-1080p.chinese(简,csf).default.srt"
+	//subFPath := "C:\\Tmp\\media\\test\\Three Thousand Years of Longing (2022) WEBDL-1080p.chinese(简英,assrt).ass"
 	f := NewFFMPEGHelper(log_helper.GetLogger4Tester())
 	m3u8, sub, err := f.ExportVideoHLSAndSubByTimeRange(videoFPath, subFPath, "10", "300", "5.000", outDirPath)
 	if err != nil {

+ 61 - 9
pkg/folder.go

@@ -394,6 +394,57 @@ func ClearManualSubUploadCacheFolder() error {
 	return nil
 }
 
+// --------------------------------------------------------------
+// 视频和字幕的预览缓存
+// --------------------------------------------------------------
+
+// GetVideoAndSubPreviewCacheFolder 视频和字幕的预览缓存
+func GetVideoAndSubPreviewCacheFolder() (string, error) {
+
+	nowProcessRoot, err := os.Getwd()
+	if err != nil {
+		return "", err
+	}
+	nowProcessRoot = filepath.Join(nowProcessRoot, cacheRootFolderName, VideoAndSubPreviewCacheFolder)
+	err = os.MkdirAll(nowProcessRoot, os.ModePerm)
+	if err != nil {
+		return "", err
+	}
+	return nowProcessRoot, err
+}
+
+// ClearVideoAndSubPreviewCacheFolder 清理视频和字幕的预览缓存
+func ClearVideoAndSubPreviewCacheFolder() error {
+
+	nowTmpFolder, err := GetVideoAndSubPreviewCacheFolder()
+	if err != nil {
+		return err
+	}
+
+	pathSep := string(os.PathSeparator)
+	files, err := os.ReadDir(nowTmpFolder)
+	if err != nil {
+		return err
+	}
+	for _, curFile := range files {
+		fullPath := nowTmpFolder + pathSep + curFile.Name()
+		if curFile.IsDir() {
+			err = os.RemoveAll(fullPath)
+			if err != nil {
+				return err
+			}
+		} else {
+			// 这里就是文件了
+			err = os.Remove(fullPath)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
 // --------------------------------------------------------------
 // Common
 // --------------------------------------------------------------
@@ -536,15 +587,16 @@ func ClearIdleSubFixCacheFolder(l *logrus.Logger, rootSubFixCacheFolder string,
 
 // 缓存文件的位置信息,都是在程序的根目录下的 cache 中
 const (
-	cacheRootFolderName        = "cache"                    // 缓存文件夹总名称
-	TmpFolder                  = "tmp"                      // 临时缓存的文件夹
-	RodCacheFolder             = "rod"                      // rod 的缓存目录
-	PluginFolder               = "Plugin"                   // 插件的目录
-	DebugFolder                = "CSF-DebugThings"          // 调试相关的文件夹
-	SubFixCacheFolder          = "CSF-SubFixCache"          // 字幕时间校正的缓存文件夹,一般可以不清理
-	ShareSubFileCache          = "CSF-ShareSubCache"        // 字幕共享的缓存目录,不建议删除
-	CacheCenterFolder          = "CSF-CacheCenter"          // 下载缓存、队列缓存、下载次数缓存的文件夹
-	ManualSubUploadCacheFolder = "CSF-ManualSubUploadCache" // 手动上传字幕的缓存文件夹
+	cacheRootFolderName           = "cache"                       // 缓存文件夹总名称
+	TmpFolder                     = "tmp"                         // 临时缓存的文件夹
+	RodCacheFolder                = "rod"                         // rod 的缓存目录
+	PluginFolder                  = "Plugin"                      // 插件的目录
+	DebugFolder                   = "CSF-DebugThings"             // 调试相关的文件夹
+	SubFixCacheFolder             = "CSF-SubFixCache"             // 字幕时间校正的缓存文件夹,一般可以不清理
+	ShareSubFileCache             = "CSF-ShareSubCache"           // 字幕共享的缓存目录,不建议删除
+	CacheCenterFolder             = "CSF-CacheCenter"             // 下载缓存、队列缓存、下载次数缓存的文件夹
+	ManualSubUploadCacheFolder    = "CSF-ManualSubUploadCache"    // 手动上传字幕的缓存文件夹
+	VideoAndSubPreviewCacheFolder = "CSF-VideoAndSubPreviewCache" // 视频和字幕的预览缓存
 )
 
 const (

+ 222 - 0
pkg/preview_queue/queue.go

@@ -0,0 +1,222 @@
+package preview_queue
+
+import (
+	"errors"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+
+	"github.com/allanpk716/ChineseSubFinder/pkg/types/common"
+
+	"github.com/allanpk716/ChineseSubFinder/pkg"
+
+	"github.com/allanpk716/ChineseSubFinder/pkg/ffmpeg_helper"
+	llq "github.com/emirpasic/gods/queues/linkedlistqueue"
+	"github.com/emirpasic/gods/sets/hashset"
+	"github.com/sirupsen/logrus"
+)
+
+type PreviewQueue struct {
+	log          *logrus.Logger
+	ffmpegHelper *ffmpeg_helper.FFMPEGHelper
+	processQueue *llq.Queue
+	jobSet       *hashset.Set
+	addOneSignal chan interface{}
+	addLocker    sync.Mutex
+	workingJob   *Job // 正在操作的任务的路径
+}
+
+func NewPreviewQueue(log *logrus.Logger) *PreviewQueue {
+
+	p := &PreviewQueue{
+		log:          log,
+		ffmpegHelper: ffmpeg_helper.NewFFMPEGHelper(log),
+		processQueue: llq.New(),
+		jobSet:       hashset.New(),
+		addOneSignal: make(chan interface{}, 1),
+		workingJob:   nil,
+	}
+	go func(pu *PreviewQueue) {
+		for {
+			select {
+			case <-pu.addOneSignal:
+				// 有新任务了
+				pu.dealers()
+			}
+		}
+	}(p)
+
+	return p
+}
+
+// GetVideoHLSAndSubByTimeRangeExportPathInfo 获取视频的HLS和字幕的导出路径信息
+func (p *PreviewQueue) GetVideoHLSAndSubByTimeRangeExportPathInfo(videoFullPath, subFullPath, startTimeString, timeLength string) (string, string, error) {
+	// 导出视频
+	if pkg.IsFile(videoFullPath) == false {
+		return "", "", errors.New("video file not exist, maybe is bluray file, so not support yet")
+	}
+
+	if pkg.IsFile(subFullPath) == false {
+		return "", "", errors.New("sub file not exist")
+	}
+
+	outDirPath, err := pkg.GetVideoAndSubPreviewCacheFolder()
+	if err != nil {
+		return "", "", err
+	}
+
+	fileName := filepath.Base(videoFullPath)
+	frontName := strings.ReplaceAll(fileName, filepath.Ext(fileName), "")
+
+	outDirSubPath := filepath.Join(outDirPath, frontName, startTimeString+"-"+timeLength)
+	if pkg.IsDir(outDirSubPath) == true {
+		err := os.RemoveAll(outDirSubPath)
+		if err != nil {
+			return "", "", err
+		}
+	}
+	err = os.MkdirAll(outDirSubPath, os.ModePerm)
+	if err != nil {
+		return "", "", err
+	}
+
+	outSubFileFPath := filepath.Join(outDirSubPath, frontName+common.SubExtSRT)
+	// 字幕的相对位置
+	subRelPath, err := filepath.Rel(outDirPath, outSubFileFPath)
+	if err != nil {
+		return "", "", err
+	}
+	// outputlist.m3u8 的相对位置
+	outputListRelPath, err := filepath.Rel(outDirPath, filepath.Join(outDirSubPath, "outputlist.m3u8"))
+	if err != nil {
+		return "", "", err
+	}
+
+	return outputListRelPath, subRelPath, nil
+}
+
+// IsJobInQueue 是否正在队列中排队,或者正在被处理
+func (p *PreviewQueue) IsJobInQueue(job *Job) bool {
+	p.addLocker.Lock()
+	defer func() {
+		p.addLocker.Unlock()
+	}()
+
+	if job == nil || job.VideoFPath == "" {
+		return false
+	}
+	if p.jobSet.Contains(job.VideoFPath) == true {
+		// 已经在队列中了
+		return true
+	} else {
+
+		if p.workingJob == nil {
+			return false
+		}
+		// 还有一种可能,任务从队列拿出来了,正在处理,那么在外部开来也还是在队列中的
+		if p.workingJob.VideoFPath == job.VideoFPath {
+			return true
+		}
+	}
+	return false
+}
+
+// Add 添加任务
+func (p *PreviewQueue) Add(job *Job) {
+
+	p.addLocker.Lock()
+	defer func() {
+		p.addLocker.Unlock()
+	}()
+
+	if p.jobSet.Contains(job.VideoFPath) == true {
+		// 已经在队列中了
+		return
+	}
+	p.processQueue.Enqueue(job)
+	p.jobSet.Add(job.VideoFPath)
+	// 通知有新任务了
+	p.addOneSignal <- struct{}{}
+
+	return
+}
+
+// ListJob 任务列表
+func (p *PreviewQueue) ListJob() []*Job {
+
+	p.addLocker.Lock()
+	defer func() {
+		p.addLocker.Unlock()
+	}()
+	ret := make([]*Job, 0)
+	for _, v := range p.processQueue.Values() {
+		ret = append(ret, v.(*Job))
+	}
+	if p.workingJob != nil {
+		ret = append(ret, p.workingJob)
+	}
+	return ret
+}
+
+func (p *PreviewQueue) dealers() {
+
+	p.addLocker.Lock()
+	if p.processQueue.Empty() == true {
+		// 没有任务了
+		p.addLocker.Unlock()
+		return
+	}
+	job, ok := p.processQueue.Dequeue()
+	if ok == false {
+		// 没有任务了
+		p.addLocker.Unlock()
+		return
+	}
+	// 移除这个任务
+	p.jobSet.Remove(job.(*Job).VideoFPath)
+	// 标记这个正在处理
+	p.workingJob = job.(*Job)
+	p.addLocker.Unlock()
+	// 具体处理这个任务
+	err := p.processSub(job.(*Job))
+	if err != nil {
+		p.log.Error(err)
+	}
+}
+
+func (p *PreviewQueue) processSub(job *Job) error {
+
+	defer func() {
+		// 任务处理完了
+		p.addLocker.Lock()
+		p.workingJob = nil
+		p.addLocker.Unlock()
+	}()
+
+	const segmentTime = "5.000"
+	nowOutRootDirPath, err := pkg.GetVideoAndSubPreviewCacheFolder()
+	if err != nil {
+		return err
+	}
+	// 具体处理这个任务,这个任务在加入队列之前就可以预测将要存放在哪,以及名称是什么
+	m3u8FPath, subFPath, err := p.ffmpegHelper.ExportVideoHLSAndSubByTimeRange(job.VideoFPath, job.SubFPath, job.StartTime, job.EndTime, segmentTime, nowOutRootDirPath)
+	if err != nil {
+		return err
+	}
+	p.log.Infoln("preview m3u8FPath:", m3u8FPath)
+	p.log.Infoln("preview subFPath:", subFPath)
+
+	return nil
+}
+
+type Job struct {
+	VideoFPath string `json:"video_f_path"`
+	SubFPath   string `json:"sub_f_path"`
+	StartTime  string `json:"start_time"`
+	EndTime    string `json:"end_time"`
+}
+
+type Reply struct {
+	Jobs []*Job `json:"jobs"`
+}