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 }