Files
pipu-music/services/api/internal/library/agent.go
T
mrleo1nidandClaude Opus 5.5 c583991d14
ci / api / api (push) Successful in 26s
ci / mobile / mobile (Metro-бандл Android) (push) Failing after 22s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 37s
ci / ts / ts (packages + web) (push) Successful in 51s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 53s
feat(api): library sources, scanner and catalog (M2)
- schema for sources, path patterns, media files, artists, albums,
  tracks, genres (migration 00003)
- path patterns (RE2 with named groups, skip), merge with tags by
  source priority, fallbacks from the file name and folder, name
  normalization and multi-artist split
- scanner: walk, skip unchanged files by size and mtime, ffprobe in 8
  workers with a single catalog writer, renames by content hash keep
  the track, duplicates fold into one track with the best file as
  primary, missing files are kept 30 days
- storage-agent mounts NFS with validated server, export and an option
  whitelist, no shell; test mount counts audio files
- admin API: sources CRUD, connection test, patterns with preview,
  scan, library status, file errors, host folder picker; scheduled
  scans and health checks; mounts are restored on start
- catalog API: artist, artist albums, album with tracks, track
- api image is alpine with ffmpeg for ffprobe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 06:48:37 +03:00

106 lines
3.0 KiB
Go

package library
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"pipu/api/internal/storageagent"
)
// Agent — клиент storage-agent (монтирование NFS).
type Agent interface {
Mount(ctx context.Context, sourceID uuid.UUID, spec storageagent.NFSSpec) (storageagent.MountResult, error)
Unmount(ctx context.Context, sourceID uuid.UUID) error
Test(ctx context.Context, spec storageagent.NFSSpec) (storageagent.MountResult, error)
}
// HTTPAgent ходит в storage-agent по внутренней сети с общим секретом.
type HTTPAgent struct {
BaseURL string
Token string
Client *http.Client
}
// NewHTTPAgent создаёт клиента агента.
func NewHTTPAgent(baseURL, token string) *HTTPAgent {
return &HTTPAgent{BaseURL: baseURL, Token: token, Client: &http.Client{Timeout: 40 * time.Second}}
}
// AgentError — агент отклонил запрос (например, недопустимые параметры NFS).
type AgentError struct {
Status int
Field string
Reason string
}
func (e *AgentError) Error() string {
return fmt.Sprintf("storage-agent: %d %s %s", e.Status, e.Field, e.Reason)
}
func (a *HTTPAgent) do(ctx context.Context, method, path string, body any, out any) error {
var rd *bytes.Reader
if body != nil {
raw, err := json.Marshal(body)
if err != nil {
return err
}
rd = bytes.NewReader(raw)
} else {
rd = bytes.NewReader(nil)
}
req, err := http.NewRequestWithContext(ctx, method, a.BaseURL+path, rd)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+a.Token)
req.Header.Set("Content-Type", "application/json")
res, err := a.Client.Do(req)
if err != nil {
return fmt.Errorf("storage-agent: %w", err)
}
defer func() { _ = res.Body.Close() }()
if res.StatusCode >= 400 {
var p struct {
Detail string `json:"detail"`
Errors []struct {
Field string `json:"field"`
} `json:"errors"`
}
_ = json.NewDecoder(res.Body).Decode(&p)
ae := &AgentError{Status: res.StatusCode, Reason: p.Detail}
if len(p.Errors) > 0 {
ae.Field = p.Errors[0].Field
}
return ae
}
if out != nil {
return json.NewDecoder(res.Body).Decode(out)
}
return nil
}
// Mount реализует Agent.
func (a *HTTPAgent) Mount(ctx context.Context, id uuid.UUID, spec storageagent.NFSSpec) (storageagent.MountResult, error) {
var res storageagent.MountResult
err := a.do(ctx, http.MethodPost, "/internal/mounts", storageagent.MountRequest{SourceID: id.String(), NFSSpec: spec}, &res)
return res, err
}
// Unmount реализует Agent.
func (a *HTTPAgent) Unmount(ctx context.Context, id uuid.UUID) error {
return a.do(ctx, http.MethodDelete, "/internal/mounts/"+id.String(), nil, nil)
}
// Test реализует Agent.
func (a *HTTPAgent) Test(ctx context.Context, spec storageagent.NFSSpec) (storageagent.MountResult, error) {
var res storageagent.MountResult
err := a.do(ctx, http.MethodPost, "/internal/mounts/test", spec, &res)
return res, err
}