Files
jellybit/internal/jellyfin/jellyfin.go
T
av 4d2196f41d jellyfin: авторизация заголовком Authorization вместо X-Emby-Token
- Jellyfin 12 миграцией выключает legacy-авторизацию, и X-Emby-Token
  получает 401 — пересканирование медиатеки после раскладки перестаёт работать
- схема `MediaBrowser Token="…"` принимается и 10.11, так что деплой
  можно делать до обновления Jellyfin
2026-09-18 14:31:03 +03:00

105 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package jellyfin — минимальный клиент Jellyfin для пересканирования
// медиатеки после успешной раскладки. Единственная задача: дёрнуть скан
// всех библиотек (POST /Library/Refresh), чтобы новые хардлинки быстрее
// появились в проигрывателе. В духе сервиса — без зоопарка вызовов.
package jellyfin
import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
"git.vakhrushev.me/av/jellybit/internal/logctx"
"git.vakhrushev.me/av/jellybit/internal/logging"
)
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)
}
// Схема MediaBrowser в Authorization — единственный способ авторизации,
// который Jellyfin 12 принимает по умолчанию: X-Emby-Token и прочие
// legacy-заголовки выключены (EnableLegacyAuthorization=false).
req.Header.Set("Authorization", `MediaBrowser Token="`+c.apiKey+`"`)
log := logctx.FromOr(ctx, c.log)
call := logging.StartCall(logging.ServiceJellyfin, "library/refresh")
resp, err := c.hc.Do(req)
if err != nil {
call.Failure(log, err)
return fmt.Errorf("jellyfin: refresh: %w", err)
}
defer func() { _ = resp.Body.Close() }()
call.Status = resp.StatusCode
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<10))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
err := fmt.Errorf("jellyfin: refresh: status %d body %q",
resp.StatusCode, strings.TrimSpace(string(body)))
call.Failure(log, err)
return err
}
call.Success(log)
return nil
}