Добавил обновление библиотеки jellyfin после добавления медиа
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// Package jellyfin — минимальный клиент Jellyfin для пересканирования
|
||||
// медиатеки после успешной раскладки. Единственная задача: дёрнуть скан
|
||||
// всех библиотек (POST /Library/Refresh), чтобы новые хардлинки быстрее
|
||||
// появились в проигрывателе. В духе сервиса — без зоопарка вызовов.
|
||||
package jellyfin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultTimeout = 10 * time.Second
|
||||
|
||||
// Config — подключение к Jellyfin.
|
||||
type Config struct {
|
||||
URL string
|
||||
APIKey string
|
||||
Proxy string // опц. HTTP-прокси
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// Client — клиент Jellyfin API.
|
||||
type Client struct {
|
||||
base string
|
||||
apiKey string
|
||||
hc *http.Client
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// New собирает клиент с опц. прокси. logger nil → slog.Default().
|
||||
func New(cfg Config, logger *slog.Logger) (*Client, error) {
|
||||
base, err := url.Parse(strings.TrimRight(cfg.URL, "/"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("jellyfin: parse url %q: %w", cfg.URL, err)
|
||||
}
|
||||
timeout := cfg.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultTimeout
|
||||
}
|
||||
transport := http.DefaultTransport
|
||||
if cfg.Proxy != "" {
|
||||
pu, perr := url.Parse(cfg.Proxy)
|
||||
if perr != nil {
|
||||
return nil, fmt.Errorf("jellyfin: parse proxy %q: %w", cfg.Proxy, perr)
|
||||
}
|
||||
// Клонируем дефолтный транспорт (dial/TLS-таймауты, keep-alive), а не
|
||||
// собираем голый — как в metadata-клиенте.
|
||||
t := http.DefaultTransport.(*http.Transport).Clone()
|
||||
t.Proxy = http.ProxyURL(pu)
|
||||
transport = t
|
||||
}
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
return &Client{
|
||||
base: base.String(),
|
||||
apiKey: cfg.APIKey,
|
||||
hc: &http.Client{Timeout: timeout, Transport: transport},
|
||||
log: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshLibraries запускает скан всех библиотек Jellyfin
|
||||
// (POST /Library/Refresh). Скан инкрементальный — полный дёшев, поэтому
|
||||
// точечный скан конкретной папки не делаем (сложнее, не в духе сервиса).
|
||||
// Ответ при успехе — 204 No Content.
|
||||
func (c *Client) RefreshLibraries(ctx context.Context) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.base+"/Library/Refresh", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jellyfin: build request: %w", err)
|
||||
}
|
||||
req.Header.Set("X-Emby-Token", c.apiKey)
|
||||
|
||||
start := time.Now()
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("jellyfin: refresh: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<10))
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("jellyfin: refresh: status %d body %q",
|
||||
resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
c.log.Info("jellyfin: library refresh triggered", "duration", time.Since(start))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package jellyfin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRefreshLibraries_OK(t *testing.T) {
|
||||
var gotPath, gotToken, gotMethod string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod = r.Method
|
||||
gotPath = r.URL.Path
|
||||
gotToken = r.Header.Get("X-Emby-Token")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c, err := New(Config{URL: srv.URL, APIKey: "secret"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if err := c.RefreshLibraries(context.Background()); err != nil {
|
||||
t.Fatalf("RefreshLibraries: %v", err)
|
||||
}
|
||||
if gotMethod != http.MethodPost {
|
||||
t.Errorf("method = %q, want POST", gotMethod)
|
||||
}
|
||||
if gotPath != "/Library/Refresh" {
|
||||
t.Errorf("path = %q, want /Library/Refresh", gotPath)
|
||||
}
|
||||
if gotToken != "secret" {
|
||||
t.Errorf("token = %q, want secret", gotToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshLibraries_TrimsTrailingSlash(t *testing.T) {
|
||||
var gotPath string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c, err := New(Config{URL: srv.URL + "/", APIKey: "k"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if err := c.RefreshLibraries(context.Background()); err != nil {
|
||||
t.Fatalf("RefreshLibraries: %v", err)
|
||||
}
|
||||
if gotPath != "/Library/Refresh" {
|
||||
t.Errorf("path = %q, want /Library/Refresh (без двойного слеша)", gotPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshLibraries_ErrorStatus(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c, err := New(Config{URL: srv.URL, APIKey: "bad"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
if err := c.RefreshLibraries(context.Background()); err == nil {
|
||||
t.Fatal("ожидали ошибку на 401, получили nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user