Добавил реализацию

This commit is contained in:
av
2026-06-28 12:20:10 +03:00
parent 4f2fb5998a
commit d8ef7063f4
12 changed files with 731 additions and 39 deletions
+21 -3
View File
@@ -26,6 +26,12 @@ type QBittorrent interface {
Add(ctx context.Context, ar qbt.AddRequest) error
}
// Namer выводит человекочитаемое отображаемое имя торрента из контекста.
// Пустой результат → имя в qBittorrent не задаём. nil → шаг пропускается.
type Namer interface {
DeriveName(ctx context.Context, contextText, hint string) string
}
// Config — параметры добавления в qBittorrent.
type Config struct {
Category string
@@ -36,13 +42,15 @@ type Config struct {
type Service struct {
store Store
qbt QBittorrent
namer Namer
cfg Config
log *slog.Logger
}
// New собирает сервис приёма.
func New(st Store, qb QBittorrent, cfg Config, log *slog.Logger) *Service {
return &Service{store: st, qbt: qb, cfg: cfg, log: log}
// New собирает сервис приёма. namer опционален (nil → отображаемое имя не
// выводится; qBittorrent оставит своё).
func New(st Store, qb QBittorrent, namer Namer, cfg Config, log *slog.Logger) *Service {
return &Service{store: st, qbt: qb, namer: namer, cfg: cfg, log: log}
}
// Request — входной запрос приёма.
@@ -82,6 +90,15 @@ func (s *Service) Ingest(ctx context.Context, req Request) (Result, error) {
}, nil
}
// Отображаемое имя для списка qBit — best-effort: не валит приём.
// Выводится синхронно (param rename действует только при добавлении) и
// ДО CreateDownload, чтобы возможный медленный вызов LLM не расширял окно
// «строка в БД есть, в qBittorrent ещё нет». Имя от строки БД не зависит.
var rename string
if s.namer != nil {
rename = s.namer.DeriveName(ctx, req.Context, info.DisplayName)
}
d := &store.Download{
SourceType: store.SourceMagnet,
SourceRef: source,
@@ -99,6 +116,7 @@ func (s *Service) Ingest(ctx context.Context, req Request) (Result, error) {
URLs: []string{source},
Category: s.cfg.Category,
SavePath: s.cfg.SavePath,
Rename: rename,
})
if addErr != nil {
s.log.Warn("ingest: qbittorrent add failed, marking download failed",
+50 -1
View File
@@ -58,8 +58,27 @@ func (f *fakeQbt) Add(_ context.Context, ar qbt.AddRequest) error {
return nil
}
// fakeNamer возвращает заранее заданное имя; фиксирует переданные аргументы.
type fakeNamer struct {
name string
gotContext string
gotHint string
called bool
}
func (f *fakeNamer) DeriveName(_ context.Context, contextText, hint string) string {
f.called = true
f.gotContext = contextText
f.gotHint = hint
return f.name
}
func newService(st Store, qb QBittorrent) *Service {
return New(st, qb, Config{Category: "jellybit", SavePath: "/srv/media/downloads"},
return newServiceWithNamer(st, qb, nil)
}
func newServiceWithNamer(st Store, qb QBittorrent, nm Namer) *Service {
return New(st, qb, nm, Config{Category: "jellybit", SavePath: "/srv/media/downloads"},
slog.New(slog.NewTextHandler(io.Discard, nil)))
}
@@ -94,6 +113,36 @@ func TestIngestHappyPath(t *testing.T) {
}
}
func TestIngestSetsDisplayName(t *testing.T) {
fs := &fakeStore{}
fq := &fakeQbt{}
nm := &fakeNamer{name: "Дюна: Часть вторая (2024)"}
_, err := newServiceWithNamer(fs, fq, nm).Ingest(context.Background(),
Request{Source: sampleMagnet, Context: "Дюна 2"})
if err != nil {
t.Fatalf("Ingest: %v", err)
}
if !nm.called || nm.gotContext != "Дюна 2" || nm.gotHint != "Dune" {
t.Errorf("namer получил context=%q hint=%q (called=%v)", nm.gotContext, nm.gotHint, nm.called)
}
if len(fq.added) != 1 || fq.added[0].Rename != "Дюна: Часть вторая (2024)" {
t.Errorf("rename = %q, want %q", fq.added[0].Rename, "Дюна: Часть вторая (2024)")
}
}
func TestIngestEmptyNameOmitsRename(t *testing.T) {
fs := &fakeStore{}
fq := &fakeQbt{}
nm := &fakeNamer{name: ""} // имя не получено
if _, err := newServiceWithNamer(fs, fq, nm).Ingest(context.Background(),
Request{Source: sampleMagnet}); err != nil {
t.Fatalf("Ingest: %v", err)
}
if len(fq.added) != 1 || fq.added[0].Rename != "" {
t.Errorf("rename = %q, want пусто", fq.added[0].Rename)
}
}
func TestIngestIdempotent(t *testing.T) {
existing := &store.Download{ID: 7, State: store.StateDownloading}
fs := &fakeStore{active: existing}