Repository navigation
Developer Providers
Otakase separates what you watch (AniList / local history) from where streams come from (providers). Providers are compile-time modules that implement a small interface and register themselves at startup. The host application handles menus, tracking, mpv playback, and provider stack fallback — providers only answer search, episode list, and stream URL questions.
This document explains the intent of the design, how to add a provider, how users enable or disable providers, and what to do when a new provider needs something the current contract does not cover.
-
Adding a provider should be mechanical — new package, implement three methods, register in
init(), add one import line. No edits to central maps, name switches, orotakase.goprovider-specific branches. -
Disabling a broken provider should not require a release — users can turn providers off in
otakase.confwithout rebuilding. -
Keep one binary — providers ship inside the otakase repo (or as Go packages imported at build time). We are not using runtime plugin binaries or
.soloading. -
Host owns orchestration — ordered fallback, sub/dub prompts, AniList → provider matching, and mpv IPC stay in
internal/. Providers return data; they do not drive the watch loop.
- Installable third-party plugin binaries
- Providers replacing AniList search or the TUI/Rofi selector
- Providers running arbitrary host code or reading OAuth tokens
┌─────────────────────────────────────────────────────────┐
│ internal/ (host) │
│ Setup · provider stack · mpv · tracking · config │
│ │ uses providers.New() + adapter │
│ │ wires providerhost.* hooks for HTTP, log, storage │
└───────┼─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ internal/providers/ │
│ registry · types · Provider interface │
├─────────────────────────────────────────────────────────┤
│ anikoto/ anipub/ yourprovider/ │
│ search · episodes · streams · register.go │
└─────────────────────────────────────────────────────────┘
Providers must not import internal (that would create an import cycle). They use providerhost for shared services and providers for types and registration.
Defined in internal/providers/types.go:
type Provider interface {
Name() string
SearchAnime(query, mode string) ([]SelectionOption, error)
EpisodesList(showID, mode string) ([]string, error)
GetEpisodeURL(config PlaybackConfig, id string, epNo int) ([]string, error)
}| Method | Purpose |
|---|---|
Name() |
Canonical provider id (e.g. anikoto). Must match Meta.Name from registration. |
SearchAnime |
Find shows on the streaming site. mode is sub or dub where relevant. |
EpisodesList |
Return episode numbers as strings for a provider show id. |
GetEpisodeURL |
Resolve playable URLs for episode epNo (1-based, aligned with AniList progress). |
type SelectionOption struct {
Key string // provider show id (stored in history / ProviderId)
Label string // menu line
Title string // plain title for matching
Thumbnail string // optional cover URL
ExtraData any // optional provider-specific payload (e.g. metadata for matching)
}
type PlaybackConfig struct {
SubOrDub string // "sub" or "dub"
}Use providers.NormalizeTranslationType(mode) and providers.AlternateTranslationType(mode) from internal/providers/lang.go for sub/dub handling.
Implement only when the default behavior is not enough:
| Interface | When to implement |
|---|---|
ModeResolver |
Sub and dub use different APIs or URLs; host calls GetEpisodeURLForMode explicitly during fallback. |
HintResolver |
Streams need per-URL mpv metadata (Referer header, external subtitle URL). Used by AllAnime. |
IDResolver |
Stored show ids go stale and must be refreshed via search (Animepahe sessions). |
The host discovers these via type assertion on the registered provider — no extra registration step.
- Configured stack:
Provider = ["anikoto", "anipub"]— tried left to right for search and playback. - Qualified ids in menus when multiple providers are active:
anikoto::ShowIdHere. - Host qualifies ids with
providername::idwhen merging stacked search results.
Your Key in SearchAnime should be the raw provider id. The host adds the provider:: prefix when needed.
internal/providers/yoursite/
register.go # providers.Register in init()
provider.go # Provider struct + interface methods
search.go # HTTP / API search (optional split)
episodes.go
streams.go
provider_test.go
Use anikoto as a reference for a JSON/REST provider and anipub for one that has to follow an embed chain.
package yoursite
import "github.com/thexykril/otakase/internal/providers"
type Provider struct{}
func (p *Provider) Name() string { return "yoursite" }
func (p *Provider) SearchAnime(query, mode string) ([]providers.SelectionOption, error) {
// ...
}
func (p *Provider) EpisodesList(showID, mode string) ([]string, error) {
// ...
}
func (p *Provider) GetEpisodeURL(config providers.PlaybackConfig, id string, epNo int) ([]string, error) {
// ...
}func init() {
providers.Register(providers.Meta{
Name: "yoursite",
Aliases: []string{"ys"}, // optional config/menu aliases
Referrer: "https://yoursite.example/", // mpv Referer for streams
}, func() providers.Provider {
return &Provider{}
})
}Meta fields:
| Field | Meaning |
|---|---|
Name |
Canonical name (lowercase, no spaces; normalized to compact form e.g. anikoto). |
Aliases |
Alternate names accepted in config. |
Referrer |
Default HTTP Referer for mpv when playing this provider's links. |
DefaultDisabled |
If true, provider is off until user enables it (see Animepahe). |
DisableReason |
Shown when a disabled provider is requested. |
FallbackPrompt |
Reserved for host fallback UX (Animepahe chromium warning). |
Add a blank import in internal/loadproviders/load.go:
import (
_ "github.com/thexykril/otakase/internal/providers/anikoto"
_ "github.com/thexykril/otakase/internal/providers/anipub"
_ "github.com/thexykril/otakase/internal/providers/yoursite"
)internal already imports loadproviders from provider_bridge.go, so registration runs on startup.
Providers must not call internal helpers directly. Use hooks in internal/providerhost/host.go:
| Hook | Use |
|---|---|
providerhost.HTTPClient() |
Shared cookie jar HTTP client |
providerhost.Log(string) |
Debug log (otakase-debug.log in storage path) |
providerhost.Out(string) |
User-visible terminal message |
providerhost.StoragePath() |
~/.local/share/otakase (or configured path) |
providerhost.AnimeNameLanguage() |
"english" or "romaji" for search result labels |
providerhost.HTTPStatusOK / HTTPStatusError
|
Consistent HTTP error formatting |
Example:
resp, err := providerhost.HTTPClient().Do(req)
if err != nil {
return nil, err
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if !providerhost.HTTPStatusOK(resp.StatusCode) {
return nil, providerhost.HTTPStatusError("yoursite search", resp.StatusCode, body)
}- Unit tests live in the provider package (
httptesttransport, no live site). - Use
providerhosthooks in test setup (seeanineko/provider_test.go). - Host-level stack tests use
providers.SetFactoryForTestviawithProviderFactoriesinprovider_stack_test.go. - Live tests: gate behind env vars (e.g.
CURD_LIVE_ALLANIME_TEST=1).
Users add the provider to their stack in ~/.config/otakase/otakase.conf:
Provider=["yoursite"]
# or fallback stack:
Provider=["yoursite","anikoto"]Disable order (first match wins):
-
Test override —
withAllProvidersEnabledForTestin tests only. -
DisabledProvidersin config — runtime kill switch, no rebuild:DisabledProviders=["anidb","yoursite"]
-
DefaultDisabledinMeta— compile-time default (Animepahe ships disabled with a reason).
To ship a provider that is off by default but opt-in capable, set DefaultDisabled: true and a clear DisableReason. Users remove it from DisabledProviders or we can add a menu entry later to toggle it.
Follow this order — prefer extending the contract over special-casing in otakase.go.
| Need | Action |
|---|---|
| Explicit sub/dub resolution | Implement ModeResolver
|
| Per-stream Referer / subtitles | Implement HintResolver
|
| Stale show id refresh | Implement IDResolver
|
No registry or host changes required beyond what the interface already supports.
Examples: capability flags (RequiresBrowser: true), max quality, consent text.
- Add the field to
providers.Meta. - Teach the host to read it via
providers.MetaFor(name)(config UI, fallback prompts, doctor command). - Do not hardcode the provider name in
otakase.go.
Examples: headless browser factory, shared rate limiter, proxy setting.
- Add a function variable to
internal/providerhost/host.go. - Wire it in
internal/provider_bridge.goinit()from existinginternalinfrastructure. - Document it in this file.
- Use it only from provider packages that need it.
Avoid importing internal from providers.
If every provider needs a new playback preference (e.g. subtitle language on resolve):
- Add the field to
providers.PlaybackConfig. - Map it in
toPlaybackConfig()inprovider_bridge.go. - Update existing providers if the field is required; otherwise ignore in providers that do not care.
Prefer putting provider-specific match data in SelectionOption.ExtraData and scoring in the host (see scoreProviderSearchOption in provider.go).
If matching is truly provider-specific and complex, implement IDResolver or export small helpers from your provider package (as anipub does for its MegaPlay ids) and call them from the host through interfaces — never if providerName == "yoursite" in otakase.go.
That is outside the current model. Options for a future iteration:
- External JSON/script providers invoked via subprocess
- Separate Go module imported at build time (
go installwith build tags)
Document the decision in a short ADR before building. The compile-time registry is intentional for reliability and packaging.
- Package under
internal/providers/<name>/ -
register.gowithproviders.Registerand completeMeta - Blank import added in
internal/loadproviders/load.go - Uses
providerhostonly (nointernalimport) -
provider_test.gowith HTTP mocks - Episode numbers compatible with AniList 1-based progress
- Stream URLs return formats mpv can play (m3u8, mp4, etc.)
-
Referrerset correctly if the CDN checks Referer - No provider-specific branches added to
otakase.go - README or this doc updated if new config keys or hooks were added
| Provider | Package | Notes |
|---|---|---|
| Anikoto | internal/providers/anikoto |
AniList media ids as show ids, HLS manifest + subtitle + headers + skip ranges in two requests, SkipRange
|
| AniPub | internal/providers/anipub |
JSON search/info/details APIs, MegaPlay embed resolution via /stream/getSources, MAL id in ExtraData for tracker matching |
| AniNeko | internal/providers/anineko |
AJAX search, HTML scrape, bibiemb/vibeplayer embed resolution, SubStyle / HintResolver
|
| KickAssAnime | internal/providers/kickassanime |
JSON API, whole seasons rather than only recent episodes, dubs indexed separately |
| Nyaa | internal/providers/nyaa |
Torrent RSS + streaming through a torrent client rather than an HTTP host |
Key host files:
| File | Role |
|---|---|
internal/providers/registry.go |
Registration and lookup |
internal/providers/types.go |
Interfaces and DTOs |
internal/loadproviders/load.go |
Built-in provider imports |
internal/provider_bridge.go |
Host hooks + internal adapter |
internal/provider.go |
Stack, search, resolve orchestration |
internal/provider_disabled.go |
Enable/disable logic |
Why not dynamic plugins?
Otakase is a single-session CLI binary. Compile-time modules give fast startup, simple packaging (AUR, releases), and easy debugging. Config-based disable covers “site is broken right now” without a plugin marketplace.
Can providers live in another repo?
Yes, as a Go module that imports github.com/thexykril/otakase/internal/providers and providerhost, calls Register in init(), and is blank-imported from a fork or custom loadproviders package. Same binary model, different import path.
What if episode URL resolution is slow?
That is expected for some sites. Do heavy work inside the provider (parallel HTTP, caching cookies on disk under providerhost.StoragePath()). The host already prefetches the next episode in a goroutine during playback.
Who owns breaking site changes?
The provider package. Fix the provider, release otakase. Users can disable a broken provider with DisabledProviders until a fix ships.