125 lines
3.6 KiB
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
|
|
}
|