diff --git a/cmd/liber/liber.go b/cmd/liber/liber.go
index c5db157d740da7417587237e0e11ff03bb999b78..0fdbbe7940da18ce4f39ef261d78cc178c9244a2 100644
--- a/cmd/liber/liber.go
+++ b/cmd/liber/liber.go
@@ -37,6 +37,7 @@ func main() {
 	subcommands.Register(&dumpCommand{}, "Maintenance")
 	subcommands.Register(&restoreCommand{}, "Maintenance")
 	subcommands.Register(&reindexCommand{}, "Maintenance")
+	subcommands.Register(&listCommand{}, "Maintenance")
 
 	log.SetFlags(0)
 	flag.Parse()
diff --git a/cmd/liber/list.go b/cmd/liber/list.go
new file mode 100644
index 0000000000000000000000000000000000000000..5f352f897f969aaa01b3161da45729d763216d4b
--- /dev/null
+++ b/cmd/liber/list.go
@@ -0,0 +1,98 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"strings"
+
+	"git.autistici.org/ale/liber"
+
+	"github.com/google/subcommands"
+)
+
+type listCommand struct{}
+
+func (c *listCommand) Name() string             { return "list-books" }
+func (c *listCommand) Synopsis() string         { return "List books that match certain criteria" }
+func (c *listCommand) SetFlags(f *flag.FlagSet) {}
+func (c *listCommand) Usage() string {
+	return `list-books <CRITERIA>
+  List books that match certain criteria. Known tags you can use:
+
+    +all
+    +incomplete
+    +invalid
+    +source:<SOURCE>
+
+`
+}
+
+func matchSource(source string) func(*liber.Book) bool {
+	return func(book *liber.Book) bool {
+		if book.Metadata == nil {
+			return false
+		}
+		for _, msrc := range book.Metadata.Sources {
+			if msrc.Name == source {
+				return true
+			}
+		}
+		return false
+	}
+}
+
+func matchIncomplete(book *liber.Book) bool {
+	return (book.Metadata != nil && book.Metadata.Complete())
+}
+
+func matchInvalid(_ *liber.Book) bool {
+	return false
+}
+
+func matchAll(_ *liber.Book) bool {
+	return true
+}
+
+func (c *listCommand) parseTag(tag string) (func(*liber.Book) bool, error) {
+	if strings.HasPrefix(tag, "+source:") {
+		return matchSource(tag[7:]), nil
+	}
+
+	switch tag {
+	case "+all":
+		return matchAll, nil
+	case "+incomplete":
+		return matchIncomplete, nil
+	case "+invalid":
+		return matchInvalid, nil
+	default:
+		return nil, fmt.Errorf("unknown tag '%s'", tag)
+	}
+}
+
+func (c *listCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
+	if f.NArg() == 0 {
+		log.Printf("Not enough arguments")
+		return subcommands.ExitUsageError
+	}
+
+	db := openDB()
+	defer db.Close()
+
+	var matchFuncs []func(*liber.Book) bool
+	for _, arg := range f.Args() {
+		f, err := c.parseTag(arg)
+		if err != nil {
+			log.Printf("error: %v", err)
+			return subcommands.ExitUsageError
+		}
+		matchFuncs = append(matchFuncs, f)
+	}
+
+	db.ListBooks(os.Stdout, matchFuncs...)
+
+	return subcommands.ExitSuccess
+}
diff --git a/cmd/liber/refine.go b/cmd/liber/refine.go
new file mode 100644
index 0000000000000000000000000000000000000000..02a66add3d3f3f61a3c03905813292907391c3ec
--- /dev/null
+++ b/cmd/liber/refine.go
@@ -0,0 +1,47 @@
+package main
+
+import (
+	"context"
+	"flag"
+	"log"
+	"os"
+	"path/filepath"
+
+	"github.com/google/subcommands"
+
+	"git.autistici.org/ale/liber/util"
+)
+
+type refineCommand struct{}
+
+func (c *refineCommand) Name() string             { return "update" }
+func (c *refineCommand) Synopsis() string         { return "Add books to the local db" }
+func (c *refineCommand) SetFlags(f *flag.FlagSet) {}
+func (c *refineCommand) Usage() string {
+	return `update [<OPTIONS>]
+  Add books to the local database.
+
+`
+}
+
+func (c *refineCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
+	if f.NArg() > 0 {
+		log.Printf("Too many arguments")
+		return subcommands.ExitUsageError
+	}
+
+	db := openDB()
+	defer db.Close()
+
+	// Redirect logging to dbdir/refine.log.
+	logf, err := os.OpenFile(filepath.Join(*databaseDir, "refine.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
+	if err == nil {
+		defer logf.Close()
+		log.SetOutput(logf)
+		log.SetFlags(log.Ldate | log.Ltime)
+	}
+
+	db.Refine(util.ExpandTilde(*bookDir))
+
+	return subcommands.ExitSuccess
+}
diff --git a/database.go b/database.go
index 33a2fa224ca76be15c82633dd48e84056dda2848..47b4bb821740350edbf7d7cd9f76e649b0b01ac4 100644
--- a/database.go
+++ b/database.go
@@ -478,15 +478,25 @@ func (db *Database) Reindex() error {
 	}
 
 	// Scan the database and re-index everything.
-	i := db.Scan(BookBucket)
-	for i.Next() {
+	return db.onAllBooks(func(book *Book) error {
+		db.index.Index(book.Id.String(), flatten(book))
+		return nil
+	})
+}
+
+// Call a function for all books in the database.
+func (db *Database) onAllBooks(f func(*Book) error) error {
+	it := db.Scan(BookBucket)
+	for it.Next() {
 		var book Book
-		if err := i.Value(&book); err != nil {
+		if err := it.Value(&book); err != nil {
 			continue
 		}
-		db.index.Index(i.Id().String(), flatten(&book))
+		if err := f(&book); err != nil {
+			return err
+		}
 	}
-	return i.Close()
+	return it.Close()
 }
 
 type SearchResult struct {
diff --git a/refine.go b/refine.go
new file mode 100644
index 0000000000000000000000000000000000000000..d1f2c0049cf351f32468e307044be0e8cd72aab8
--- /dev/null
+++ b/refine.go
@@ -0,0 +1,31 @@
+package liber
+
+import (
+	"fmt"
+	"io"
+)
+
+func (db *Database) Refine(dir string) error {
+	//storage := NewFileStorage(dir)
+	return db.onAllBooks(func(book *Book) error {
+		return nil
+	})
+}
+
+// ListBooks writes IDs of books that match any of a series of
+// functions to an io.Writer.
+func (db *Database) ListBooks(w io.Writer, matchFuncs ...func(book *Book) bool) error {
+	return db.onAllBooks(func(book *Book) error {
+		ok := false
+		for _, f := range matchFuncs {
+			if f(book) {
+				ok = true
+				break
+			}
+		}
+		if ok {
+			fmt.Fprintf(w, "%s\n", book.Id)
+		}
+		return nil
+	})
+}