gobbler/cmd/categorize.go

125 lines
3.6 KiB
Go

package main
import (
"errors"
"fmt"
"os"
"sort"
"github.com/ollien/gobbler/categorize"
"github.com/ollien/gobbler/categorize/match"
"github.com/ollien/gobbler/categorize/tv"
)
type fileLocation struct {
filename string
location tv.Location
}
// categorizationResult is the result of an individual categorization; it's effectively what a return value would be
// for a single operation, but it's returned through a callback
type categorizationResult struct {
// the location of the categorized file. This should be ignored if the error is non-nil, or "skipped" is true
location fileLocation
err error
}
type categorizationStats struct {
filesScanned int
filesSkipped int
}
// Performs the categorization process for the given source (i.e. where the downloaded files are stored)
// and library (i.e. where they will be sorted into). Errors that halt the program proceeding will be returned. Results
// and non-fatal errors will be returned via resultCallback. If this callback returns false, categorization stops.
func categorizeFolder(sourceFolder, libraryFolder string, resultCallback func(categorizationResult) bool) (categorizationStats, error) {
sourceFolderContents, err := getDirEntries(sourceFolder, func(entries []os.FileInfo) {
// Sort from newest to oldest
sort.Slice(entries, func(i, j int) bool {
iModTime := entries[i].ModTime()
jModTime := entries[j].ModTime()
return !iModTime.Before(jModTime)
})
})
if err != nil {
return categorizationStats{}, fmt.Errorf("failed to read source folder: %w", err)
}
categorizer, err := categorize.NewCategorizer(libraryFolder)
if err != nil {
return categorizationStats{}, fmt.Errorf("failed to make categorizer: %w", err)
}
stats := categorizationStats{}
performCategorization(categorizer, sourceFolderContents, func(res categorizationResult) bool {
stats.filesScanned++
if errors.Is(err, match.ErrNoMatches) {
stats.filesSkipped++
return true
} else if res.err != nil {
stats.filesSkipped++
return true
}
return resultCallback(res)
})
return stats, nil
}
// performCategorization categorizes the given files through the given categorizer, converting them to a
// slice of FileLocations, ordered as they were provided in sourceFolderContents. Results and non-fatal errors will be
// returned via resultCallback. If this callback returns false, categorization stops.
func performCategorization(categorizer categorize.Categorizer, sourceFolderContents []string, resultCallback func(categorizationResult) bool) {
for _, entry := range sourceFolderContents {
location, err := categorizer.CategorizeFile(entry)
var wrappedErr error
if err != nil {
wrappedErr = fmt.Errorf("failed to categorize '%s': %w", entry, err)
}
shouldContinue := resultCallback(categorizationResult{
location: fileLocation{entry, location},
err: wrappedErr,
})
if shouldContinue {
continue
} else {
break
}
}
}
// getDirEntries gets filenames in a directory, ordered by the given sort function.
func getDirEntries(path string, performSort func([]os.FileInfo)) ([]string, error) {
fd, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open dir: %w", err)
}
defer fd.Close()
entries, err := fd.Readdir(-1)
if err != nil {
return nil, fmt.Errorf("failed to read dir contents: %w", err)
}
performSort(entries)
return getFileInfoNames(entries), nil
}
// getFileInfoNames maps FileInfos to the names in each FileInfo
func getFileInfoNames(fileInfos []os.FileInfo) []string {
names := make([]string, len(fileInfos))
for i, fileInfo := range fileInfos {
names[i] = fileInfo.Name()
}
return names
}