* fix(metadata): seed specialist providers off and scope chains to declared levels New library provider chains were seeded from every enabled metadata provider, ordered purely by each plugin's declared default_priority and enabled whenever that priority was > 0. Two consequences: - A specialist provider (e.g. silo.sportarr, which declares series/season/ episode) could out-rank the general providers and land at position 1, enabled, on every new TV series library. - Single-purpose providers that declare only their own level (audiobook / ebook / manga metadata) were still attached as disabled rows to series and movie libraries, cluttering the chain editor with providers that cannot serve that content. Introduce a `default_enabled` capability-metadata flag (defaults to true, so every existing plugin is unaffected). A provider sets it false to be seeded installed-but-disabled while keeping its declared priority, so a user can opt in per-library and it slots in where the manifest intends instead of jumping to the top. At the same time, seedDefaultChain and AppendProviderToAllChains now drop providers that do not declare a content level, reusing the same providerSupportsLevel rule as the chain-less fallback (issue #106). LookupSeedPlacement resolves support/priority/enabled with a single metadata fetch. buildSeededChainEntries is extracted as a pure, unit-tested helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): standardize metadata provider slug casing in library chain editor The library provider-chain editor showed the same provider differently depending on where the chain came from: a freshly defaulted chain used the capability display name ("TMDB"), while a chain loaded from the server used the capability id ("tmdb", which the API returns as provider_slug). So a provider read one way before saving and another after, and differed between library types depending on which levels already had a saved chain. Standardize on the capability id everywhere (matches the server's provider_slug and the mono/slug styling). Extract the provider mapping into a pure, unit-tested metadataProvidersFromInstallations helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): mirror server seeding rules in the library form's default chain The form builds its own default chain client-side, and any touch (including changing the library type on create, the normal path for a series library) marks it dirty and POSTs it after create — replacing the server-seeded chain. That chain still enabled every provider with a declared priority and listed unsupported providers as disabled rows, so the server-side fix evaporated on the UI create path. buildDefaultLevelChains now applies the same rules as buildSeededChainEntries: providers that don't declare the level are dropped, a declaring provider is enabled only if it doesn't opt out via default_enabled, and a legacy catch-all (no declared levels) is parked last, disabled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(web,api): serve default provider chains from the server Replace the form's client-side reimplementation of the seeding rules with a new additive endpoint, GET /api/v1/libraries/provider-defaults?library_type=X, which returns the exact chain seedDefaultChain would write for that type. The create form now renders those server-computed defaults, changing the library type just refetches them (no longer marking the chain dirty), and a create with an untouched chain lets the server-seeded chain stand instead of writing one back. Editing an existing library uses the same defaults to fill levels its saved chain doesn't cover. Types the server seeds no metadata levels for (e.g. podcasts) return an empty levels map rather than an error. This removes buildDefaultLevelChains / metadataProvidersFromInstallations and the default_priority/default_enabled manifest parsing from the frontend — one source of truth for default ordering and enablement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(web): show a loading state in the provider chain editor While the server chain (for an existing library) or the type's defaults are still in flight, the editor rendered empty provider lists for a moment. Show a spinner row instead; local edits always render immediately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
50 lines
1.6 KiB
Go
50 lines
1.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// A library type the server seeds no metadata levels for (unknown types, or
|
|
// ones like podcasts without metadata content levels) has no defaults — the
|
|
// endpoint answers with an empty levels map rather than an error, so the UI
|
|
// can treat "no defaults" and "defaults" uniformly.
|
|
func TestHandleGetLibraryProviderDefaults_NoLevelsForType(t *testing.T) {
|
|
h := &LibraryHandler{}
|
|
|
|
for _, libraryType := range []string{"podcasts", "bogus", ""} {
|
|
req := httptest.NewRequest(http.MethodGet, "/libraries/provider-defaults?library_type="+libraryType, nil)
|
|
rec := httptest.NewRecorder()
|
|
h.HandleGetLibraryProviderDefaults(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("type %q: expected 200, got %d", libraryType, rec.Code)
|
|
}
|
|
var body struct {
|
|
Levels map[string][]chainLevelEntry `json:"levels"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("type %q: decoding body: %v", libraryType, err)
|
|
}
|
|
if len(body.Levels) != 0 {
|
|
t.Errorf("type %q: expected empty levels, got %v", libraryType, body.Levels)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A seedable type still needs the chain repository; without one the endpoint
|
|
// reports unavailable like the other provider-chain handlers.
|
|
func TestHandleGetLibraryProviderDefaults_NoChainRepo(t *testing.T) {
|
|
h := &LibraryHandler{}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/libraries/provider-defaults?library_type=series", nil)
|
|
rec := httptest.NewRecorder()
|
|
h.HandleGetLibraryProviderDefaults(rec, req)
|
|
|
|
if rec.Code != http.StatusServiceUnavailable {
|
|
t.Fatalf("expected 503, got %d", rec.Code)
|
|
}
|
|
}
|