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 FlagSourceDir = envvars.String("SOURCE_PATH", "/video") FlagTargetDir = envvars.String("TARGET_PATH", "/audio") FlagCacheFile = envvars.String("CACHE_FILE", "/hashes.bin") FlagUpdateCacheFileInterval = envvars.Duration("UPDATE_CACHE_FILE_INTERVAL", 10*time.Second) ) 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) 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 { relPath := strings.TrimPrefix(file[:len(file)-len(filepath.Ext(file))], FlagSourceDir) newPath := filepath.Join(FlagTargetDir, relPath+".mp3") 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 }