package store import ( "context" "fmt" "strings" ) // StateGroup — группа состояний для фильтра списка загрузок. Единый источник // истины «группа → состояния» для транспорта (чипы) и запроса (WHERE). type StateGroup string const ( GroupAll StateGroup = "all" // все, кроме deleted (если не IncludeDeleted) GroupReview StateGroup = "review" // ждут человека GroupActive StateGroup = "active" // в работе GroupDone StateGroup = "done" // готово GroupProblem StateGroup = "problem" // проблемы ) // statesInGroup возвращает состояния группы фильтра. nil — группа не сужает // выборку по состоянию (all/неизвестная): состояния перечисляются здесь и // нигде больше, чтобы UI и запрос не разъезжались. Терминальные cancelled/ // reverted/deleted своей группы (чипа) не имеют — под all они видны (deleted — // только при IncludeDeleted), точечно к ним не фильтруем (см. tasks 2.1). func statesInGroup(g StateGroup) []State { switch g { case GroupReview: return []State{StateReview, StateDeferred} case GroupActive: return []State{StateDownloading, StateCompleted, StateRecognizing, StateLinking} case GroupDone: return []State{StateDone} case GroupProblem: return []State{StateStuck, StateFailed, StateTargetMissing, StateOrphaned} default: return nil } } // ListFilter — параметры страницы списка загрузок. type ListFilter struct { Group StateGroup // группа состояний ("" / all — без сужения) Query string // поиск по названию/infohash/контексту (подстрока) IncludeDeleted bool // показывать состояние deleted (только под all) Limit int // размер страницы Offset int // смещение страницы } // listWhere строит условие WHERE и его аргументы по фильтру. func listWhere(f ListFilter) (string, []any) { var conds []string var args []any if states := statesInGroup(f.Group); states != nil { ph := make([]string, len(states)) for i, st := range states { ph[i] = "?" args = append(args, string(st)) } conds = append(conds, "state IN ("+strings.Join(ph, ",")+")") } else if !f.IncludeDeleted { // Группа all: скрываем удалённые по умолчанию (шум в ленте). conds = append(conds, "state <> ?") args = append(args, string(StateDeleted)) } if q := strings.TrimSpace(f.Query); q != "" { // LIKE даёт регистронезависимость только для ASCII (встроенное // case-folding SQLite), для кириллицы поиск чувствителен к регистру — // ICU в чистом modernc-драйвере нет. Достаточно для инфохэша/латиницы; // кириллические названия ищем как введены. like := "%" + q + "%" conds = append(conds, "(source_ref LIKE ? COLLATE NOCASE "+ "OR display_name LIKE ? COLLATE NOCASE "+ "OR context LIKE ? COLLATE NOCASE "+ "OR EXISTS (SELECT 1 FROM download_infohash dh "+ "WHERE dh.download_id = download.id AND dh.infohash LIKE ? COLLATE NOCASE))") args = append(args, like, like, like, like) } if len(conds) == 0 { return "", args } return " WHERE " + strings.Join(conds, " AND "), args } // ListDownloadsPage возвращает страницу загрузок под фильтром и общее число // строк, удовлетворяющих фильтру (для пагинации). Сортировка — по времени // добавления в источник (source_added_at) с фолбеком на created_at и tie-break // по id: без вторичного ключа при равных метках порядок неустойчив, и строки // задваивались бы/пропадали между страницами. RecTitle подтягивается LEFT JOIN // текущей попытки распознавания — для фолбека заголовка карточки. func (s *Store) ListDownloadsPage(ctx context.Context, f ListFilter) ([]Download, int, error) { where, args := listWhere(f) var total int if err := s.DB.GetContext(ctx, &total, `SELECT COUNT(*) FROM download`+where, args...); err != nil { return nil, 0, fmt.Errorf("list downloads page count: %w", err) } q := `SELECT download.*, r.title AS rec_title FROM download LEFT JOIN recognition r ON r.download_id = download.id AND r.is_current = 1` + where + ` ORDER BY COALESCE(download.source_added_at, download.created_at) DESC, download.id DESC LIMIT ? OFFSET ?` pageArgs := append(append([]any{}, args...), f.Limit, f.Offset) var out []Download if err := s.DB.SelectContext(ctx, &out, q, pageArgs...); err != nil { return nil, 0, fmt.Errorf("list downloads page: %w", err) } if err := s.attachInfohashes(ctx, out); err != nil { return nil, 0, fmt.Errorf("list downloads page: %w", err) } return out, total, nil }