diff --git a/cli/cmd/enrich.go b/cli/cmd/enrich.go index 564855e..1fbf6c5 100644 --- a/cli/cmd/enrich.go +++ b/cli/cmd/enrich.go @@ -7,14 +7,12 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/spf13/cobra" "tangled.org/hectorsector.com/books/cli/internal/openlibrary" "tangled.org/hectorsector.com/books/cli/internal/store" ) -// enrichCmd represents the enrich command var enrichCmd = &cobra.Command{ Use: "enrich", Short: "looks up OL IDs for each book", @@ -38,12 +36,11 @@ func runEnrich(storePath string) error { return fmt.Errorf("Couldn't load books from store at %s: %w", absPath, err) } + books = store.Filter(books, store.NeedsEnrichment()) for i := range books { - if books[i].OLWorkID == "" && books[i].ISBN != "" && !strings.HasPrefix(books[i].ISBN, "B") { - books[i].OLWorkID, err = openlibrary.LookupWorkID(books[i].ISBN) - if err != nil { - fmt.Fprintf(os.Stderr, "warning: Couldn't resolve ISBN %s to a OLWorkID\n", books[i].ISBN) - } + books[i].OLWorkID, err = openlibrary.LookupWorkID(books[i].ISBN) + if err != nil { + fmt.Fprintf(os.Stderr, "warning: Couldn't resolve ISBN %s to a OLWorkID\n", books[i].ISBN) } } @@ -56,14 +53,4 @@ func runEnrich(storePath string) error { func init() { rootCmd.AddCommand(enrichCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // enrichCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // enrichCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } diff --git a/cli/cmd/filters.go b/cli/cmd/filters.go new file mode 100644 index 0000000..ec5cdaa --- /dev/null +++ b/cli/cmd/filters.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "tangled.org/hectorsector.com/books/cli/internal/store" +) + +func filtersFromFlags(cmd *cobra.Command) []store.Predicate { + + var predicates []store.Predicate + + title, _ := cmd.Flags().GetString("title") + if title != "" { + predicates = append(predicates, store.HasTitle(title)) + } + + author, _ := cmd.Flags().GetString("author") + if author != "" { + predicates = append(predicates, store.HasAuthor(author)) + } + + isbn, _ := cmd.Flags().GetString("isbn") + if isbn != "" { + predicates = append(predicates, store.HasISBN(isbn)) + } + + noIsbn, _ := cmd.Flags().GetBool("no-isbn") + if noIsbn { + predicates = append(predicates, store.Not(store.HasISBN(""))) + } + + status, _ := cmd.Flags().GetString("status") + if status != "" { + predicates = append(predicates, store.HasStatus(status)) + } + + noStatus, _ := cmd.Flags().GetBool("no-status") + if noStatus { + predicates = append(predicates, store.Not(store.HasLexiconStatus())) + } + + olWorkID, _ := cmd.Flags().GetString("olworkid") + if olWorkID != "" { + predicates = append(predicates, store.HasOLWorkID(olWorkID)) + } + + noOlWorkID, _ := cmd.Flags().GetBool("no-olworkid") + if noOlWorkID { + predicates = append(predicates, store.Not(store.HasOLWorkID(""))) + } + + return predicates + +} diff --git a/cli/cmd/list.go b/cli/cmd/list.go index 56fa044..2a56b93 100644 --- a/cli/cmd/list.go +++ b/cli/cmd/list.go @@ -11,7 +11,6 @@ import ( "tangled.org/hectorsector.com/books/cli/internal/store" ) -// listCmd represents the list command var listCmd = &cobra.Command{ Use: "list", Short: "List the books in our local record.", @@ -23,20 +22,29 @@ var listCmd = &cobra.Command{ return fmt.Errorf("Can't load the file: %s", storePath) } - statusFilter, _ := cmd.Flags().GetString("status") - books = store.Filter(books, statusFilter) - + books = store.Filter(books, filtersFromFlags(cmd)...) n, _ := cmd.Flags().GetInt("count") if n > 0 && n < len(books) { books = books[:n] } - printSummary(books) + full, _ := cmd.Flags().GetBool("full") + if full { + printFull(books) + } else { + printSummary(books) + } return nil }, } +func printFull(books []store.Book) { + for _, b := range books { + fmt.Printf("%-40.40s %-25.25s %-12.12s %-13.13s %s\n", b.Title, b.Author, lexicon.DisplayStatus(b.LexiconStatus), b.ISBN, b.OLWorkID) + } +} + func printSummary(books []store.Book) { for _, b := range books { fmt.Printf("%s by %s [%s]\n", b.Title, b.Author, lexicon.DisplayStatus(b.LexiconStatus)) @@ -46,15 +54,16 @@ func printSummary(books []store.Book) { func init() { rootCmd.AddCommand(listCmd) - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // listCmd.PersistentFlags().String("foo", "", "A help for foo") + listCmd.Flags().Bool("full", false, "show all fields") - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // listCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") - listCmd.Flags().String("status", "", "filter by lexicon status (options: wantToRead, reading, finished, didNotFinish, paused)") + // filters listCmd.Flags().IntP("count", "n", 0, "limit results, (0=all)") + listCmd.Flags().String("title", "", "filter by title (partial match)") + listCmd.Flags().String("author", "", "filter by author (partial match)") + listCmd.Flags().String("isbn", "", "filter by ISBN") + listCmd.Flags().Bool("no-isbn", false, "filter books with no ISBN") + listCmd.Flags().String("status", "", "filter by lexicon status (wantToRead, reading, finished, didNotFinish, paused)") + listCmd.Flags().Bool("no-status", false, "filter books with no status") + listCmd.Flags().String("olworkid", "", "filter by OL work ID") + listCmd.Flags().Bool("no-olworkid", false, "filter books with no OL work ID") } diff --git a/cli/internal/store/store.go b/cli/internal/store/store.go index 2b45330..e2f1cba 100644 --- a/cli/internal/store/store.go +++ b/cli/internal/store/store.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "tangled.org/hectorsector.com/books/cli/internal/lexicon" "tangled.org/hectorsector.com/books/cli/internal/storygraph" @@ -21,6 +22,8 @@ type Book struct { StatusURI string } +type Predicate func(Book) bool + // loads book data from the store func Load(path string) ([]Book, error) { absPath, _ := filepath.Abs(path) @@ -38,7 +41,7 @@ func Load(path string) ([]Book, error) { return books, nil } -// writes pretty JSON via temp file and then rename (for crash safety?) +// writes pretty JSON via temp file and then rename (for crash safety) func Save(path string, books []Book) error { // marshall json data, err := json.MarshalIndent(books, "", " ") @@ -74,17 +77,26 @@ func Save(path string, books []Book) error { return nil } -func Filter(books []Book, status string) []Book { +func Filter(books []Book, predicates ...Predicate) []Book { - if status == "" { + // nothing to filter, return the list + if len(predicates) == 0 { return books } - statusFilter := lexicon.NormalizeStatus(status) - var result []Book for _, book := range books { - if book.LexiconStatus == statusFilter { + // handles multiple predicates + match := true + + for _, applyFilter := range predicates { + // no match on this predicate + if !applyFilter(book) { + match = false + break + } + } + if match { result = append(result, book) } } @@ -92,6 +104,82 @@ func Filter(books []Book, status string) []Book { return result } +func HasTitle(title string) Predicate { + return func(book Book) bool { + return strings.Contains(book.Title, title) + } +} + +func HasAuthor(author string) Predicate { + return func(book Book) bool { + return strings.Contains(book.Author, author) + } +} + +func HasStatus(status string) Predicate { + return func(book Book) bool { + if status == "" { + return true + } + return book.LexiconStatus == lexicon.NormalizeStatus(status) + } +} + +func HasLexiconStatus() Predicate { + return func(book Book) bool { + return book.LexiconStatus != "" + } +} + +func NeedsEnrichment() Predicate { + return All( + HasISBN(""), + Not(HasOLWorkID("")), + Not(HasASIN()), + ) +} + +func HasISBN(isbn string) Predicate { + return func(book Book) bool { + if isbn == "" { + return book.ISBN != "" + } + return book.ISBN == isbn + } +} + +func HasASIN() Predicate { + return func(book Book) bool { + return strings.HasPrefix(book.ISBN, "B") + } +} + +func HasOLWorkID(olWorkID string) Predicate { + return func(book Book) bool { + if olWorkID == "" { + return book.OLWorkID != "" + } + return book.OLWorkID == olWorkID + } +} + +func Not(p Predicate) Predicate { + return func(book Book) bool { + return !p(book) + } +} + +func All(predicates ...Predicate) Predicate { + return func(book Book) bool { + for _, p := range predicates { + if !p(book) { + return false + } + } + return true + } +} + // converts []storygraph.Book to store-compatible []Book (with LexiconStatus built in) func FromStorygraph(books []storygraph.Book) []Book { var result []Book