// 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 }