2025-01-15 12:48:20 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io/fs"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.tordarus.net/tordarus/channel"
|
|
|
|
"git.tordarus.net/tordarus/envvars"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
|
|
)
|
|
|
|
|
|
|
|
var ( // flags
|
2025-01-15 15:22:52 +01:00
|
|
|
FlagSourceDir = envvars.String("SOURCE_PATH", "/video")
|
|
|
|
FlagTargetDir = envvars.String("TARGET_PATH", "/audio")
|
|
|
|
FlagCacheFile = envvars.String("CACHE_FILE", "/hashes.bin")
|
2025-01-15 21:06:45 +01:00
|
|
|
FlagExcludeFiles = envvars.StringSlice("EXCLUDE_FILES", ":", []string{".songs"})
|
2025-01-15 15:22:52 +01:00
|
|
|
FlagUpdateCacheFileInterval = envvars.Duration("UPDATE_CACHE_FILE_INTERVAL", 10*time.Second)
|
2025-01-15 12:48:20 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
if _, err := exec.LookPath("ffmpeg"); err != nil {
|
|
|
|
fmt.Println("ffmpeg not found in PATH")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
FlagSourceDir = AbsPath(FlagSourceDir)
|
|
|
|
FlagTargetDir = AbsPath(FlagTargetDir)
|
|
|
|
FlagCacheFile = AbsPath(FlagCacheFile)
|
|
|
|
|
2025-01-15 21:06:45 +01:00
|
|
|
for i := 0; i < len(FlagExcludeFiles); i++ {
|
|
|
|
FlagExcludeFiles[i] = filepath.Join(FlagSourceDir, FlagExcludeFiles[i])
|
|
|
|
}
|
|
|
|
|
2025-01-15 12:48:20 +01:00
|
|
|
InitCache()
|
|
|
|
|
|
|
|
ops := []fsnotify.Op{
|
|
|
|
fsnotify.Create,
|
|
|
|
fsnotify.Remove,
|
|
|
|
fsnotify.Write,
|
|
|
|
fsnotify.Rename,
|
|
|
|
}
|
|
|
|
|
|
|
|
files, err := WatchDirectory(context.Background(), FlagSourceDir, ops...)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
groupedFiles := channel.GroupByTime(files, time.Second, func(events map[string]struct{}, event string) map[string]struct{} {
|
|
|
|
if events == nil {
|
|
|
|
events = map[string]struct{}{}
|
|
|
|
}
|
|
|
|
events[event] = struct{}{}
|
|
|
|
return events
|
|
|
|
})
|
|
|
|
|
|
|
|
filesByGroup := channel.FlatMap(groupedFiles, func(key string, _ struct{}) string { return key })
|
|
|
|
filteredFiles := channel.Filter(filesByGroup, Or(Not(Exists), And(SymlinkFilesOnly, VideosOnly)))
|
|
|
|
|
|
|
|
for file := range filteredFiles {
|
|
|
|
if err := TranscodeFile(file); err != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, fmt.Errorf("could not transcode file '%s': %w", file, err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TranscodeFile(file string) error {
|
2025-01-15 15:01:28 +01:00
|
|
|
relPath := strings.TrimPrefix(file[:len(file)-len(filepath.Ext(file))], FlagSourceDir)
|
|
|
|
newPath := filepath.Join(FlagTargetDir, relPath+".mp3")
|
2025-01-15 12:48:20 +01:00
|
|
|
newDir := filepath.Dir(newPath)
|
|
|
|
|
|
|
|
fmt.Printf("check '%s'\n", relPath)
|
|
|
|
|
|
|
|
// file was deleted in source directory.
|
|
|
|
// delete corresponding file in target directory as well
|
|
|
|
if _, err := os.Stat(file); errors.Is(err, fs.ErrNotExist) {
|
|
|
|
fmt.Printf("remove '%s'\n", relPath)
|
|
|
|
return os.Remove(newPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
oldFileHash, err := HashFile(file)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := os.MkdirAll(newDir, 0755); err != nil {
|
|
|
|
return fmt.Errorf("creation of playlist failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// file does exist in target directory.
|
|
|
|
// compare hashsums to determine if file should be re-transcoded
|
|
|
|
if _, err := os.Stat(newPath); !errors.Is(err, fs.ErrNotExist) {
|
|
|
|
if expectedFileHash, exists := HashCache.GetHas(oldFileHash); exists {
|
|
|
|
currentFileHash, err := HashFile(newPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if currentFileHash == expectedFileHash {
|
|
|
|
fmt.Printf("skip '%s'. hashes match\n", relPath)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("convert '%s'\n", relPath)
|
|
|
|
if err := ConvertToAudio(context.Background(), file, newPath); err != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, fmt.Errorf("encoding of video '%s' failed: %w", file, err))
|
|
|
|
}
|
|
|
|
|
|
|
|
newFileHash, err := HashFile(newPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
HashCache.Put(oldFileHash, newFileHash)
|
|
|
|
return nil
|
|
|
|
}
|