Initial
This commit is contained in:
commit
ba4aad0ae5
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
dist/
|
||||
.idea/
|
51
api/oss_upload.go
Normal file
51
api/oss_upload.go
Normal file
@ -0,0 +1,51 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func UploadTaskFile(task dto.Task, file dto.FileObject) error {
|
||||
url, err := QueryUploadUrlForTask(task.TaskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := OssUpload(url, file.URL); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func OssUpload(url, filePath string) error {
|
||||
// 使用 http put 请求上传文件
|
||||
file, err := os.Open(filePath)
|
||||
defer os.Remove(filePath)
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", url, bytes.NewBuffer(fileBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Length", fmt.Sprintf("%d", len(fileBytes)))
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("upload failed with status code %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
57
api/sync_task.go
Normal file
57
api/sync_task.go
Normal file
@ -0,0 +1,57 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/config"
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func SyncTask() ([]dto.Task, error) {
|
||||
url := config.Config.Api.BaseUrl + "/sync"
|
||||
requestBody := map[string]interface{}{
|
||||
"version": "0.0.1",
|
||||
"devices": config.Config.Devices,
|
||||
}
|
||||
jsonData, err := json.Marshal(requestBody)
|
||||
if err != nil {
|
||||
log.Println("Error marshaling JSON:", err)
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
log.Println("Error creating request:", err)
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Println("Error sending request:", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Error reading response body:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析响应体为 map
|
||||
var response dto.TaskListResponse
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
log.Println("->:", string(body))
|
||||
log.Println("Error unmarshaling response body:", err)
|
||||
return nil, err
|
||||
}
|
||||
if response.Code != 200 {
|
||||
log.Println("Error response code:", response.Code)
|
||||
return nil, fmt.Errorf(response.Msg)
|
||||
}
|
||||
return response.Data, nil
|
||||
}
|
72
api/task_report.go
Normal file
72
api/task_report.go
Normal file
@ -0,0 +1,72 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/config"
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func QueryUploadUrlForTask(taskId string) (string, error) {
|
||||
url := config.Config.Api.BaseUrl + "/" + taskId + "/uploadUrl"
|
||||
req, err := http.NewRequest("POST", url, nil)
|
||||
if err != nil {
|
||||
log.Println("Error creating request:", err)
|
||||
return "", err
|
||||
}
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Println("Error sending request:", err)
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Error reading response body:", err)
|
||||
return "", err
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func ReportTaskFailure(taskId string) {
|
||||
url := config.Config.Api.BaseUrl + "/" + taskId + "/failure"
|
||||
|
||||
req, err := http.NewRequest("POST", url, nil)
|
||||
if err != nil {
|
||||
log.Println("Error creating request:", err)
|
||||
return
|
||||
}
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Println("Error sending request:", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
func ReportTaskSuccess(taskId string, file *dto.FileObject) {
|
||||
url := config.Config.Api.BaseUrl + "/" + taskId + "/success"
|
||||
jsonData, err := json.Marshal(file)
|
||||
if err != nil {
|
||||
log.Println("Error marshaling JSON:", err)
|
||||
return
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
log.Println("Error creating request:", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Println("Error sending request:", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}
|
17
config.yaml
Normal file
17
config.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
api:
|
||||
# baseUrl: "https://zhentuai.com/vpt/v1/scenic/3946669713328836608"
|
||||
baseUrl: "http://127.0.0.1:8030/vpt/v1/scenic/3946669713328836608"
|
||||
record:
|
||||
storage:
|
||||
type: "local"
|
||||
path: "/root/opt/"
|
||||
duration: 60
|
||||
devices:
|
||||
- deviceNo: "34020000001322200001"
|
||||
name: "192.168.55.201"
|
||||
path: "/root/opt/34020000001322200001/"
|
||||
fileName:
|
||||
timeSplit: "-"
|
||||
dateSeparator: "-"
|
||||
fileExt: "dav"
|
||||
unFinExt: "dav_"
|
34
config/dto.go
Normal file
34
config/dto.go
Normal file
@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
type ApiConfig struct {
|
||||
BaseUrl string `mapstructure:"baseUrl"`
|
||||
}
|
||||
|
||||
type RecordConfig struct {
|
||||
Storage StorageConfig `mapstructure:"storage"`
|
||||
Duration int `mapstructure:"duration"`
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
Type string `mapstructure:"type"`
|
||||
Path string `mapstructure:"path"`
|
||||
}
|
||||
|
||||
type DeviceMapping struct {
|
||||
DeviceNo string `mapstructure:"deviceNo" json:"deviceNo"`
|
||||
Name string `mapstructure:"name" json:"name"`
|
||||
}
|
||||
|
||||
type FileNameConfig struct {
|
||||
DateSeparator string `mapstructure:"dateSeparator"`
|
||||
TimeSplit string `mapstructure:"timeSplit"`
|
||||
FileExt string `mapstructure:"fileExt"`
|
||||
UnfinishedFileExt string `mapstructure:"unFinExt"`
|
||||
}
|
||||
|
||||
type MainConfig struct {
|
||||
Api ApiConfig `mapstructure:"api"`
|
||||
Record RecordConfig `mapstructure:"record"`
|
||||
Devices []DeviceMapping `mapstructure:"devices"`
|
||||
FileName FileNameConfig `mapstructure:"fileName"`
|
||||
}
|
27
config/service.go
Normal file
27
config/service.go
Normal file
@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
)
|
||||
|
||||
var Config MainConfig
|
||||
|
||||
func LoadConfig() error {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
// 读取配置文件
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Fatalf("Error reading config file, %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 反序列化配置到结构体
|
||||
if err := viper.Unmarshal(&Config); err != nil {
|
||||
log.Fatalf("Unable to decode into struct, %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
35
core/task.go
Normal file
35
core/task.go
Normal file
@ -0,0 +1,35 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/config"
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
"ZhenTuLocalPassiveAdapter/fs"
|
||||
"ZhenTuLocalPassiveAdapter/util"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func HandleTask(device config.DeviceMapping, task dto.Task) (*dto.FileObject, error) {
|
||||
adapter := fs.GetAdapter()
|
||||
fileList, err := adapter.GetFileList(device.Name + "/" + task.StartTime.Format("2006"+config.Config.FileName.DateSeparator+"01"+config.Config.FileName.DateSeparator+"02"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files := util.FilterAndSortFiles(fileList, task.StartTime, task.EndTime)
|
||||
if len(files) == 0 {
|
||||
return nil, fmt.Errorf("没有找到文件")
|
||||
}
|
||||
constructTask, err := util.CheckFileCoverageAndConstructTask(files, task.StartTime, task.EndTime, task)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ok := util.RunFfmpegTask(constructTask)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ffmpeg任务执行失败")
|
||||
}
|
||||
return &dto.FileObject{
|
||||
CreateTime: task.EndTime,
|
||||
EndTime: task.EndTime,
|
||||
NeedDownload: true,
|
||||
URL: constructTask.OutputFile,
|
||||
}, nil
|
||||
}
|
13
dto/common_response.go
Normal file
13
dto/common_response.go
Normal file
@ -0,0 +1,13 @@
|
||||
package dto
|
||||
|
||||
type ApiResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type TaskListResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data []Task `json:"data"`
|
||||
}
|
8
dto/ffmpeg.go
Normal file
8
dto/ffmpeg.go
Normal file
@ -0,0 +1,8 @@
|
||||
package dto
|
||||
|
||||
type FfmpegTask struct {
|
||||
Files []File
|
||||
Length int
|
||||
Offset int
|
||||
OutputFile string
|
||||
}
|
16
dto/file.go
Normal file
16
dto/file.go
Normal file
@ -0,0 +1,16 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
type File struct {
|
||||
BasePath string `json:"basePath"`
|
||||
Url string `json:"url"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
|
||||
func (f *File) GetDiffMs() int64 {
|
||||
return f.EndTime.Sub(f.StartTime).Milliseconds()
|
||||
}
|
62
dto/task.go
Normal file
62
dto/task.go
Normal file
@ -0,0 +1,62 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 自定义时间格式
|
||||
const customTimeFormat = "2006-01-02 15:04:05"
|
||||
|
||||
func (t *Task) UnmarshalJSON(data []byte) error {
|
||||
type Alias Task
|
||||
aux := &struct {
|
||||
StartTime string `json:"startTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(t),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
t.StartTime, err = time.ParseInLocation(customTimeFormat, aux.StartTime, time.Local)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.EndTime, err = time.ParseInLocation(customTimeFormat, aux.EndTime, time.Local)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
TaskID string `json:"taskId"`
|
||||
ScenicID string `json:"scenicId"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
DeviceNo string `json:"deviceNo"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
|
||||
type FileObject struct {
|
||||
URL string `json:"url"`
|
||||
NeedDownload bool `json:"needDownload"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
|
||||
func (f *FileObject) MarshalJSON() ([]byte, error) {
|
||||
type Alias FileObject
|
||||
return json.Marshal(&struct {
|
||||
CreateTime string `json:"createTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
*Alias
|
||||
}{
|
||||
CreateTime: f.CreateTime.Format(customTimeFormat),
|
||||
EndTime: f.EndTime.Format(customTimeFormat),
|
||||
Alias: (*Alias)(f),
|
||||
})
|
||||
}
|
16
fs/adapter.go
Normal file
16
fs/adapter.go
Normal file
@ -0,0 +1,16 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/config"
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
)
|
||||
|
||||
type Adapter interface {
|
||||
GetFileList(path string) ([]dto.File, error)
|
||||
}
|
||||
|
||||
func GetAdapter() Adapter {
|
||||
return &LocalAdapter{
|
||||
config.Config.Record.Storage,
|
||||
}
|
||||
}
|
70
fs/local_adapter.go
Normal file
70
fs/local_adapter.go
Normal file
@ -0,0 +1,70 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/config"
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
"ZhenTuLocalPassiveAdapter/util"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LocalAdapter struct {
|
||||
StorageConfig config.StorageConfig
|
||||
}
|
||||
|
||||
func (l *LocalAdapter) GetFileList(path string) ([]dto.File, error) {
|
||||
if l.StorageConfig.Path == "" {
|
||||
return nil, fmt.Errorf("未配置存储路径")
|
||||
}
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
if path[0] != '/' {
|
||||
path = "/" + path
|
||||
}
|
||||
if path[len(path)-1] != '/' {
|
||||
path = path + "/"
|
||||
}
|
||||
// 读取文件夹下目录
|
||||
files, err := os.ReadDir(l.StorageConfig.Path + path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var fileList []dto.File
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
if !util.IsVideoFile(file.Name()) {
|
||||
continue
|
||||
}
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// TODO: 0点左右会出问题
|
||||
relDt := info.ModTime()
|
||||
startTime, stopTime, err := util.ParseStartStopTime(info.Name(), relDt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if startTime.Equal(stopTime) {
|
||||
stopTime = stopTime.Add(time.Second * time.Duration(config.Config.Record.Duration))
|
||||
}
|
||||
fileList = append(fileList, dto.File{
|
||||
BasePath: l.StorageConfig.Path,
|
||||
Name: file.Name(),
|
||||
Path: path,
|
||||
Url: fmt.Sprintf("%s%s%s", l.StorageConfig.Path, path, file.Name()),
|
||||
StartTime: startTime,
|
||||
EndTime: stopTime,
|
||||
})
|
||||
sort.Slice(fileList, func(i, j int) bool {
|
||||
return fileList[i].StartTime.Before(fileList[j].StartTime)
|
||||
})
|
||||
}
|
||||
return fileList, nil
|
||||
}
|
26
go.mod
Normal file
26
go.mod
Normal file
@ -0,0 +1,26 @@
|
||||
module ZhenTuLocalPassiveAdapter
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.19.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
54
go.sum
Normal file
54
go.sum
Normal file
@ -0,0 +1,54 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
72
main.go
Normal file
72
main.go
Normal file
@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/api"
|
||||
"ZhenTuLocalPassiveAdapter/config"
|
||||
"ZhenTuLocalPassiveAdapter/core"
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func startTask(device config.DeviceMapping, task dto.Task) {
|
||||
fo, err := core.HandleTask(device, task)
|
||||
if err != nil {
|
||||
log.Printf("处理任务失败, TaskID:【%s】, DeviceNo: %s, 错误: %v\n", task.TaskID, task.DeviceNo, err)
|
||||
api.ReportTaskFailure(task.TaskID)
|
||||
return
|
||||
}
|
||||
log.Printf("处理任务成功, TaskID:【%s】, DeviceNo: %s\n", task.TaskID, task.DeviceNo)
|
||||
err = api.UploadTaskFile(task, *fo)
|
||||
if err != nil {
|
||||
log.Printf("上传文件失败, TaskID:【%s】, DeviceNo: %s, 错误: %v\n", task.TaskID, task.DeviceNo, err)
|
||||
api.ReportTaskFailure(task.TaskID)
|
||||
return
|
||||
}
|
||||
log.Printf("上传文件成功, TaskID:【%s】, DeviceNo: %s\n", task.TaskID, task.DeviceNo)
|
||||
api.ReportTaskSuccess(task.TaskID, fo)
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := config.LoadConfig()
|
||||
if err != nil {
|
||||
log.Println("加载配置文件失败:", err)
|
||||
return
|
||||
}
|
||||
// 日志文件路径
|
||||
logFilePath := "app.log"
|
||||
|
||||
// 创建或打开日志文件
|
||||
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open log file: %v", err)
|
||||
}
|
||||
defer logFile.Close()
|
||||
|
||||
// 设置日志输出到文件
|
||||
log.SetOutput(logFile)
|
||||
// 每两秒定时执行
|
||||
for {
|
||||
// 执行任务
|
||||
tasks, err := api.SyncTask()
|
||||
if err == nil {
|
||||
for _, task := range tasks {
|
||||
log.Printf("开始处理任务, TaskID:【%s】,DeviceNo: %s,开始时间: %s,结束时间: %s\n", task.TaskID, task.DeviceNo, task.StartTime, task.EndTime)
|
||||
// 处理任务
|
||||
for _, device := range config.Config.Devices {
|
||||
if device.DeviceNo == task.DeviceNo {
|
||||
// 处理任务
|
||||
go startTask(device, task)
|
||||
break // 提前返回,避免不必要的循环
|
||||
}
|
||||
}
|
||||
log.Printf("处理任务结束, TaskID:【%s】,DeviceNo: %s\n", task.TaskID, task.DeviceNo)
|
||||
}
|
||||
} else {
|
||||
log.Println("同步任务失败:", err)
|
||||
}
|
||||
// 等待两秒
|
||||
<-time.After(2 * time.Second)
|
||||
}
|
||||
}
|
297
util/ffmpeg.go
Normal file
297
util/ffmpeg.go
Normal file
@ -0,0 +1,297 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/config"
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const FfmpegExec = "ffmpeg"
|
||||
|
||||
func RunFfmpegTask(task *dto.FfmpegTask) bool {
|
||||
var result bool
|
||||
if len(task.Files) == 1 {
|
||||
// 单个文件切割,用简单方法
|
||||
result = runFfmpegForSingleFile(task)
|
||||
} else {
|
||||
// 多个文件切割,用速度快的
|
||||
result = runFfmpegForMultipleFile1(task)
|
||||
}
|
||||
// 先尝试方法1
|
||||
if result {
|
||||
return true
|
||||
}
|
||||
log.Printf("FFMPEG简易方法失败,尝试复杂方法转码")
|
||||
// 不行再尝试方法二
|
||||
return runFfmpegForMultipleFile2(task)
|
||||
}
|
||||
|
||||
func runFfmpegForMultipleFile1(task *dto.FfmpegTask) bool {
|
||||
// 多文件,方法一:先转换成ts,然后合并切割
|
||||
// 步骤一:先转换成ts,并行转换
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
var notOk bool
|
||||
|
||||
for i := range task.Files {
|
||||
wg.Add(1)
|
||||
go func(file *dto.File) {
|
||||
defer wg.Done()
|
||||
tmpFile := os.TempDir() + "/" + file.Name + ".ts"
|
||||
result, err := convertMp4ToTs(*file, tmpFile)
|
||||
if err != nil {
|
||||
log.Printf("转码出错: %v", err)
|
||||
mu.Lock()
|
||||
notOk = true
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
if result {
|
||||
mu.Lock()
|
||||
file.Url = tmpFile
|
||||
mu.Unlock()
|
||||
} else {
|
||||
// 失败了,务必删除临时文件
|
||||
os.Remove(tmpFile)
|
||||
}
|
||||
}(&task.Files[i])
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if notOk {
|
||||
return false
|
||||
}
|
||||
|
||||
// 步骤二:使用concat协议拼接裁切
|
||||
result, err := QuickConcatVideoCut(task.Files, int64(task.Offset), int64(task.Length), task.OutputFile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 步骤三:删除临时文件
|
||||
for _, file := range task.Files {
|
||||
if err := os.Remove(file.Url); err != nil {
|
||||
log.Printf("删除临时文件失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func runFfmpegForMultipleFile2(task *dto.FfmpegTask) bool {
|
||||
// 多文件,方法二:使用计算资源编码
|
||||
result, err := SlowVideoCut(task.Files, int64(task.Offset), int64(task.Length), task.OutputFile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func runFfmpegForSingleFile(task *dto.FfmpegTask) bool {
|
||||
result, err := QuickVideoCut(task.Files[0].Url, int64(task.Offset), int64(task.Length), task.OutputFile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func CheckFileCoverageAndConstructTask(fileList []dto.File, beginDt, endDt time.Time, task dto.Task) (*dto.FfmpegTask, error) {
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
log.Printf("无法根据要求找到对应录制片段!ID:【%s】,开始时间:【%s】,结束时间:【%s】", task.TaskID, beginDt, endDt)
|
||||
return nil, fmt.Errorf("无法根据要求找到对应录制片段")
|
||||
}
|
||||
|
||||
// 如果片段在中间断开时间过长
|
||||
if len(fileList) > 1 {
|
||||
var lastFile *dto.File
|
||||
for _, file := range fileList {
|
||||
if lastFile == nil {
|
||||
lastFile = &file
|
||||
continue
|
||||
}
|
||||
if file.StartTime.Sub(lastFile.EndTime).Milliseconds() > 2000 {
|
||||
// 片段断开
|
||||
log.Printf("分析FFMPEG任务失败:ID:【%s】,文件片段:【%s,%s】中间断开【%f】秒(超过2秒)", task.TaskID, lastFile.Name, file.Name, file.StartTime.Sub(lastFile.EndTime).Seconds())
|
||||
return nil, fmt.Errorf("片段断开")
|
||||
}
|
||||
lastFile = &file
|
||||
}
|
||||
}
|
||||
|
||||
// 通过文件列表构造的任务仍然是缺失的
|
||||
if fileList[len(fileList)-1].EndTime.Before(endDt) {
|
||||
log.Printf("分析FFMPEG任务失败:ID:【%s】,文件片段:【%s】,无法完整覆盖时间点【%s】", task.TaskID, fileList[len(fileList)-1].Name, endDt)
|
||||
return nil, fmt.Errorf("片段断开")
|
||||
}
|
||||
|
||||
// 构造FfmpegTaskPo
|
||||
ffmpegTask := &dto.FfmpegTask{
|
||||
Files: fileList,
|
||||
Length: int(endDt.Sub(beginDt).Seconds()),
|
||||
Offset: int(beginDt.Sub(fileList[0].StartTime).Seconds()),
|
||||
OutputFile: os.TempDir() + "/" + task.TaskID + ".mp4",
|
||||
}
|
||||
|
||||
if ffmpegTask.Offset > (config.Config.Record.Duration) {
|
||||
log.Printf("分析FFMPEG任务失败:ID:【%s】,文件片段:【%s】,无法完整覆盖时间点【%s】", task.TaskID, fileList[len(fileList)-1].Name, endDt)
|
||||
return nil, fmt.Errorf("无法完整覆盖时间点")
|
||||
}
|
||||
|
||||
return ffmpegTask, nil
|
||||
}
|
||||
|
||||
func convertMp4ToTs(file dto.File, outFileName string) (bool, error) {
|
||||
ffmpegCmd := []string{
|
||||
FfmpegExec,
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-i", file.Url,
|
||||
"-c", "copy",
|
||||
"-bsf:v", "h264_mp4toannexb",
|
||||
"-f", "mpegts",
|
||||
outFileName,
|
||||
}
|
||||
return handleFfmpegProcess(ffmpegCmd)
|
||||
}
|
||||
|
||||
func convertHevcToTs(file dto.File, outFileName string) (bool, error) {
|
||||
ffmpegCmd := []string{
|
||||
FfmpegExec,
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-i", file.Url,
|
||||
"-c", "copy",
|
||||
"-bsf:v", "hevc_mp4toannexb",
|
||||
"-f", "mpegts",
|
||||
outFileName,
|
||||
}
|
||||
return handleFfmpegProcess(ffmpegCmd)
|
||||
}
|
||||
|
||||
func QuickVideoCut(inputFile string, offset, length int64, outputFile string) (bool, error) {
|
||||
ffmpegCmd := []string{
|
||||
FfmpegExec,
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-ss", strconv.FormatInt(offset, 10),
|
||||
"-i", inputFile,
|
||||
"-c:v", "copy",
|
||||
"-an",
|
||||
"-t", strconv.FormatInt(length, 10),
|
||||
"-f", "mp4",
|
||||
outputFile,
|
||||
}
|
||||
return handleFfmpegProcess(ffmpegCmd)
|
||||
}
|
||||
|
||||
func QuickConcatVideoCut(inputFiles []dto.File, offset, length int64, outputFile string) (bool, error) {
|
||||
tmpFile := fmt.Sprintf("tmp%.10f.txt", rand.Float64())
|
||||
tmpFileObj, err := os.Create(tmpFile)
|
||||
if err != nil {
|
||||
log.Printf("创建临时文件失败:%s", tmpFile)
|
||||
return false, err
|
||||
}
|
||||
defer os.Remove(tmpFile)
|
||||
defer tmpFileObj.Close()
|
||||
|
||||
for _, filePo := range inputFiles {
|
||||
_, err := tmpFileObj.WriteString(fmt.Sprintf("file '%s'\n", filePo.Url))
|
||||
if err != nil {
|
||||
log.Printf("写入临时文件失败:%s", tmpFile)
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
ffmpegCmd := []string{
|
||||
FfmpegExec,
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
"-f", "concat",
|
||||
"-safe", "0",
|
||||
"-i", tmpFile,
|
||||
"-c:v", "copy",
|
||||
"-an",
|
||||
"-ss", strconv.FormatInt(offset, 10),
|
||||
"-t", strconv.FormatInt(length, 10),
|
||||
"-f", "mp4",
|
||||
outputFile,
|
||||
}
|
||||
return handleFfmpegProcess(ffmpegCmd)
|
||||
}
|
||||
|
||||
func SlowVideoCut(inputFiles []dto.File, offset, length int64, outputFile string) (bool, error) {
|
||||
ffmpegCmd := []string{
|
||||
FfmpegExec,
|
||||
"-hide_banner",
|
||||
"-y",
|
||||
}
|
||||
|
||||
for _, file := range inputFiles {
|
||||
ffmpegCmd = append(ffmpegCmd, "-i", file.Url)
|
||||
}
|
||||
|
||||
inputCount := len(inputFiles)
|
||||
filterComplex := strings.Builder{}
|
||||
for i := 0; i < inputCount; i++ {
|
||||
filterComplex.WriteString(fmt.Sprintf("[%d:v]", i))
|
||||
}
|
||||
filterComplex.WriteString(fmt.Sprintf("concat=n=%d:v=1[v]", inputCount))
|
||||
|
||||
ffmpegCmd = append(ffmpegCmd,
|
||||
"-filter_complex", filterComplex.String(),
|
||||
"-map", "[v]",
|
||||
"-preset:v", "fast",
|
||||
"-an",
|
||||
"-ss", strconv.FormatInt(offset, 10),
|
||||
"-t", strconv.FormatInt(length, 10),
|
||||
"-f", "mp4",
|
||||
outputFile,
|
||||
)
|
||||
|
||||
return handleFfmpegProcess(ffmpegCmd)
|
||||
}
|
||||
|
||||
func handleFfmpegProcess(ffmpegCmd []string) (bool, error) {
|
||||
startTime := time.Now()
|
||||
log.Printf("FFMPEG执行命令:【%s】", strings.Join(ffmpegCmd, " "))
|
||||
cmd := exec.Command(ffmpegCmd[0], ffmpegCmd[1:]...)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
log.Printf("FFMPEG执行命令失败,错误信息:%s,命令:【%s】", stderr.String(), strings.Join(ffmpegCmd, " "))
|
||||
return false, err
|
||||
}
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(1 * time.Minute):
|
||||
log.Printf("FFMPEG执行命令没有在1分钟内退出,命令:【%s】", strings.Join(ffmpegCmd, " "))
|
||||
return false, fmt.Errorf("ffmpeg command timed out")
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
log.Printf("FFMPEG执行命令失败,错误信息:%s,命令:【%s】", stderr.String(), strings.Join(ffmpegCmd, " "))
|
||||
return false, err
|
||||
}
|
||||
endTime := time.Now()
|
||||
log.Printf("FFMPEG执行命令结束,耗费时间:【%dms】,命令:【%s】", endTime.Sub(startTime).Milliseconds(), strings.Join(ffmpegCmd, " "))
|
||||
return true, nil
|
||||
}
|
||||
}
|
41
util/file_filter.go
Normal file
41
util/file_filter.go
Normal file
@ -0,0 +1,41 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
func FilterAndSortFiles(fileList []dto.File, beginDt, endDt time.Time) []dto.File {
|
||||
var filteredFiles []dto.File
|
||||
|
||||
for _, file := range fileList {
|
||||
fileStartTime := file.StartTime
|
||||
nextFileStartTime := file.EndTime
|
||||
|
||||
// 如果当前文件还没有开始
|
||||
if beginDt.After(fileStartTime) {
|
||||
// 没有下一个文件的情况下,就是最后一个文件
|
||||
if nextFileStartTime.IsZero() {
|
||||
continue
|
||||
}
|
||||
// 但是下一个文件已经开始
|
||||
if beginDt.Before(nextFileStartTime) {
|
||||
filteredFiles = append(filteredFiles, file)
|
||||
}
|
||||
// 已经开始,但是也已经结束了
|
||||
} else if fileStartTime.After(endDt) {
|
||||
continue
|
||||
// 已经开始,但未结束
|
||||
} else {
|
||||
filteredFiles = append(filteredFiles, file)
|
||||
}
|
||||
}
|
||||
|
||||
// 按照 GetDiffMs 的值降序排序
|
||||
sort.Slice(filteredFiles, func(i, j int) bool {
|
||||
return filteredFiles[i].GetDiffMs() > filteredFiles[j].GetDiffMs()
|
||||
})
|
||||
|
||||
return filteredFiles
|
||||
}
|
26
util/file_filter_test.go
Normal file
26
util/file_filter_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/dto"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// 示例数据
|
||||
fileList := []dto.File{
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 1, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 1, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 2, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 2, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 3, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 3, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 4, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 4, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 5, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 5, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 6, 0, 0, time.UTC)},
|
||||
{StartTime: time.Date(2023, 1, 1, 0, 6, 0, 0, time.UTC), EndTime: time.Date(2023, 1, 1, 0, 7, 0, 0, time.UTC)},
|
||||
}
|
||||
beginDt := time.Date(2023, 1, 1, 0, 2, 10, 0, time.UTC)
|
||||
endDt := time.Date(2023, 1, 1, 0, 2, 20, 0, time.UTC)
|
||||
filteredFiles := FilterAndSortFiles(fileList, beginDt, endDt)
|
||||
for _, file := range filteredFiles {
|
||||
println(file.StartTime.String(), file.EndTime.String())
|
||||
}
|
||||
}
|
10
util/file_recognizer.go
Normal file
10
util/file_recognizer.go
Normal file
@ -0,0 +1,10 @@
|
||||
package util
|
||||
|
||||
import "ZhenTuLocalPassiveAdapter/config"
|
||||
|
||||
func IsVideoFile(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
return name[len(name)-len(config.Config.FileName.FileExt):] == config.Config.FileName.FileExt || name[len(name)-len(config.Config.FileName.UnfinishedFileExt):] == config.Config.FileName.UnfinishedFileExt
|
||||
}
|
16
util/parse_date.go
Normal file
16
util/parse_date.go
Normal file
@ -0,0 +1,16 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ParseDate(filePath string) (time.Time, error) {
|
||||
re := regexp.MustCompile(`(\d{4}).?(\d{2}).?(\d{2})`)
|
||||
matches := re.FindStringSubmatch(filePath)
|
||||
if len(matches) != 4 {
|
||||
return time.Time{}, fmt.Errorf("无法解析时间范围")
|
||||
}
|
||||
return time.Parse("2006.01.02", fmt.Sprintf("%s.%s.%s", matches[1], matches[2], matches[3]))
|
||||
}
|
39
util/parse_time.go
Normal file
39
util/parse_time.go
Normal file
@ -0,0 +1,39 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"ZhenTuLocalPassiveAdapter/config"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ParseStartStopTime(filePath string, relativeDate time.Time) (time.Time, time.Time, error) {
|
||||
split := strings.Split(filePath, config.Config.FileName.TimeSplit)
|
||||
if len(split) != 2 {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("无法解析时间范围")
|
||||
}
|
||||
startTime, err := ParseTime(split[0], relativeDate)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, err
|
||||
}
|
||||
stopTime, err := ParseTime(split[1], relativeDate)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, err
|
||||
}
|
||||
return startTime, stopTime, nil
|
||||
}
|
||||
|
||||
func ParseTime(s string, relativeDate time.Time) (time.Time, error) {
|
||||
re := regexp.MustCompile(`(\d{2}).?(\d{2}).?(\d{2})`)
|
||||
matches := re.FindStringSubmatch(s)
|
||||
if len(matches) != 4 {
|
||||
return time.Time{}, fmt.Errorf("无法解析时间范围")
|
||||
}
|
||||
tm, err := time.Parse("15.04.05", fmt.Sprintf("%s.%s.%s", matches[1], matches[2], matches[3]))
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return time.Date(relativeDate.Year(), relativeDate.Month(), relativeDate.Day(),
|
||||
tm.Hour(), tm.Minute(), tm.Second(), 0, time.Local), nil
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user