Add CSV export of scan results
The Review tab gets an Export CSV menu: "This view" exports the current status filter and search, "Entire scan" exports every scanned slave file. Each row carries status, match type, a recommendation, slave/master paths, the link target, the match reason, sizes, codec details for both sides, the quality verdict, notes, errors and timestamps. Rows are streamed in pages of 1000 so a large export never holds the database connection a running scan needs. The file starts with a UTF-8 BOM for Excel, and cells a spreadsheet would evaluate as formulas are prefixed with an apostrophe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f895cab6a
commit
e8c534a3ae
6 files changed
+435
-6
No files matched your search
@@ -314,7 +314,8 @@ type MatchFilter struct {
|
||||
Offset int
|
||||
}
|
||||
|
||||
func (s *Store) Matches(ctx context.Context, pairID int64, f MatchFilter) ([]Match, int, error) {
|
||||
// matchWhere builds the WHERE clause shared by listing and export.
|
||||
func matchWhere(pairID int64, f MatchFilter) (string, []any) {
|
||||
where := []string{"pair_id = ?"}
|
||||
args := []any{pairID}
|
||||
switch f.Status {
|
||||
@@ -334,7 +335,11 @@ func (s *Store) Matches(ctx context.Context, pairID int64, f MatchFilter) ([]Mat
|
||||
q := "%" + strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(f.Query) + "%"
|
||||
args = append(args, q, q)
|
||||
}
|
||||
w := strings.Join(where, " AND ")
|
||||
return strings.Join(where, " AND "), args
|
||||
}
|
||||
|
||||
func (s *Store) Matches(ctx context.Context, pairID int64, f MatchFilter) ([]Match, int, error) {
|
||||
w, args := matchWhere(pairID, f)
|
||||
var total int
|
||||
if err := s.DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM matches WHERE `+w, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
@@ -359,6 +364,45 @@ func (s *Store) Matches(ctx context.Context, pairID int64, f MatchFilter) ([]Mat
|
||||
return out, total, rows.Err()
|
||||
}
|
||||
|
||||
// ExportMatches calls fn for every match of the filter, ordered by slave
|
||||
// path. Rows are read in pages so a slow download never holds the database
|
||||
// connection (scans need it) for longer than one page.
|
||||
func (s *Store) ExportMatches(ctx context.Context, pairID int64, f MatchFilter, fn func(Match) error) error {
|
||||
const page = 1000
|
||||
w, args := matchWhere(pairID, f)
|
||||
after := ""
|
||||
for {
|
||||
rows, err := s.DB.QueryContext(ctx, `SELECT `+matchCols+` FROM matches WHERE `+w+
|
||||
` AND rel_slave > ? ORDER BY rel_slave LIMIT ?`, append(append([]any{}, args...), after, page)...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
batch := make([]Match, 0, page)
|
||||
for rows.Next() {
|
||||
m, err := scanMatch(rows)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
batch = append(batch, m)
|
||||
}
|
||||
err = rows.Err()
|
||||
rows.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, m := range batch {
|
||||
if err := fn(m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(batch) < page {
|
||||
return nil
|
||||
}
|
||||
after = batch[len(batch)-1].RelSlave
|
||||
}
|
||||
}
|
||||
|
||||
// IdenticalIDs lists linkable identical matches (not ignored).
|
||||
func (s *Store) IdenticalIDs(ctx context.Context, pairID int64) ([]int64, error) {
|
||||
rows, err := s.DB.QueryContext(ctx, `SELECT id FROM matches WHERE pair_id = ? AND status = 'identical' AND ignored = 0 ORDER BY rel_slave`, pairID)
|
||||
|
||||
Reference in new issue
Block a user