Help us shape the future of Go! Take the 2026 Go Developer Survey

simkl

package
v0.2.49 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: GPL-3.0 Imports: 20 Imported by: 0

README

Simkl integration

Ether uses Simkl AUTH V2. The owner registers an application and supplies its client ID; server-app registrations also require a client secret. Device/PIN authorization requests media:read media:write. The detailed device endpoint specifies a device code grant without PKCE fields, while the authentication overview says both flows require PKCE. Ether follows the endpoint-specific schema rather than inventing device parameters:

Imports run on demand and read activities before fetching changed history. The initial episode pull uses include_all_episodes=original, avoiding synthetic timestamps but omitting episodes that Simkl represents only as a bulk-completed show. Anime history is skipped because its numbering may not match TMDB. Exports resolve television type before writing and skip anime rather than guessing episode mappings. Unwatches and deletions are never propagated. Simkl's normal history endpoint does not create distinct rewatch sessions without its separate PRO/VIP feature, which Ether does not enable.

A viewer's pending device flow and tokens are sealed and bound to that viewer and the registered client ID. Shared leases serialize refresh and background export across nodes; a successful disconnect fences the grant and removes queued work. Successful remote delivery can be retried after an interrupted response; normal Simkl history writes are idempotent without rewatch mode. The owner should complete credential sealing before connecting a Simkl account.

Documentation

Overview

Package simkl implements the optional Simkl integration.

Index

Constants

This section is empty.

Variables

View Source
var ErrBusy = errors.New("simkl: account is busy")
View Source
var ErrExpired = errors.New("simkl: authorization expired")
View Source
var ErrIdentityNotFound = errors.New("simkl: history identity not found")
View Source
var ErrNoPending = errors.New("simkl: no pending authorization")
View Source
var ErrPollEarly = errors.New("simkl: poll interval has not elapsed")

Functions

func Enqueue

func Enqueue(ctx context.Context, db *sql.DB, ev state.Event) error

Enqueue records local completions only for linked viewers. Imports do not echo back to their source, and an unwatch never deletes remote history.

func Handler

func Handler(d HandlerDeps) http.Handler

func HistoryPayload

func HistoryPayload(key core.RatingKey, at time.Time) (map[string]any, error)

HistoryPayload uses only identities represented unambiguously by Ether's native movie and episode keys. Local files and show/season keys are skipped. https://api.simkl.org/api-reference/simkl/add-to-history

func ParseHistory

func ParseHistory(user int64, data []byte) ([]state.ImportRecord, int, error)

ParseHistory accepts only actual timestamps and TMDB identities; anime is deliberately skipped because its episode numbering is not necessarily TMDB's. https://api.simkl.org/api-reference/simkl/get-all-items

func RunDelivery

func RunDelivery(ctx context.Context, service func() Service, enabled func() bool, log *slog.Logger)

RunDelivery is a leader-only loop. State events are enqueued by every node, but a shared per-viewer lease fences concurrent manual sync and delivery.

Types

type APIError

type APIError struct {
	Status     int
	Code       string
	RetryAfter string
}

APIError deliberately excludes response descriptions, which can contain secrets.

func (*APIError) Error

func (e *APIError) Error() string

type AuthClient

type AuthClient struct {
	BaseURL, ClientID, ClientSecret string
	HTTP                            *http.Client
}

func (AuthClient) IsOrdinaryTV

func (c AuthClient) IsOrdinaryTV(ctx context.Context, tmdb int64) (bool, error)

IsOrdinaryTV uses Simkl's documented redirect classification without following it. Anime is intentionally not assigned TMDB episode numbers. https://api.simkl.org/api-reference/simkl/redirect

func (AuthClient) Poll

func (c AuthClient) Poll(ctx context.Context, deviceCode string) (Token, error)

func (AuthClient) PullHistory

func (c AuthClient) PullHistory(ctx context.Context, user int64, token, snapshot string) ([]state.ImportRecord, string, int, error)

PullHistory reads activities before the sequential movie/show pulls. The returned snapshot must only be saved after all imported records commit. https://api.simkl.org/guides/sync

func (AuthClient) PushHistory

func (c AuthClient) PushHistory(ctx context.Context, token string, key core.RatingKey, at time.Time, tvNumberingVerified bool) error

PushHistory sends one completion; episode numbering must first be verified against Simkl's TV mapping by the caller. Anime often uses a different numbering scheme and is not safe to infer from an Ether key alone.

func (AuthClient) Refresh

func (c AuthClient) Refresh(ctx context.Context, refresh string) (Token, error)

func (AuthClient) Revoke

func (c AuthClient) Revoke(ctx context.Context, token string) error

func (AuthClient) Start

func (c AuthClient) Start(ctx context.Context) (DeviceCode, error)

type DeviceCode

type DeviceCode struct {
	DeviceCode              string `json:"device_code"`
	UserCode                string `json:"user_code"`
	VerificationURI         string `json:"verification_uri"`
	VerificationURIComplete string `json:"verification_uri_complete"`
	ExpiresIn               int    `json:"expires_in"`
	Interval                int    `json:"interval"`
}

type DeviceInfo

type DeviceInfo struct {
	UserCode                string    `json:"userCode"`
	VerificationURI         string    `json:"verificationUri"`
	VerificationURIComplete string    `json:"verificationUriComplete"`
	ExpiresAt               time.Time `json:"expiresAt"`
	Interval                int       `json:"interval"`
}

type DeviceManager

type DeviceManager struct {
	Auth       AuthClient
	AuthConfig func() AuthClient
	Tokens     TokenStore
}

DeviceManager stores each short-lived device credential sealed in shared storage. Its generation and poll claim bind it to a viewer across nodes.

func (*DeviceManager) Disconnect

func (m *DeviceManager) Disconnect(ctx context.Context, user int64) error

func (*DeviceManager) Poll

func (m *DeviceManager) Poll(ctx context.Context, user int64) (string, error)

func (*DeviceManager) Start

func (m *DeviceManager) Start(ctx context.Context, user int64) (DeviceInfo, error)

type Grant

type Grant struct {
	ClientID, Access, Refresh string
	ExpiresAt                 time.Time
}

type HTTPError

type HTTPError struct {
	Status     int
	Code       string
	RetryAfter string
}

HTTPError exposes status and Retry-After without copying upstream bodies into logs. Callers must honor Retry-After before retrying a rate limit.

func (*HTTPError) Error

func (e *HTTPError) Error() string

type HandlerDeps

type HandlerDeps struct {
	Device  *DeviceManager
	Service func() Service
	Viewer  func(context.Context, int64) bool
	Enabled func() bool
}

HandlerDeps are explicitly provided by the owner-authenticated admin API. Viewer validates that an ID identifies a current viewer, never a caller- supplied arbitrary account. Authentication is enforced by the mounting API.

type Service

type Service struct {
	Auth   AuthClient
	Tokens TokenStore
	State  *state.State
}

Service is supplied explicit state and credential dependencies. Manual sync uses a lease and a deadline shorter than the lease; it never advances the snapshot after a partial pull.

func (Service) FlushOne

func (s Service) FlushOne(ctx context.Context, user int64) error

FlushOne delivers at most one queued event under the per-viewer lease. The caller runs it only on the cluster leader. Simkl's default history write is idempotent for already-watched items, including retries after an uncertain response; rewatch creation is deliberately not enabled.

func (Service) Sync

func (s Service) Sync(ctx context.Context, user int64) (SyncResult, error)

type SyncResult

type SyncResult struct {
	Imported int `json:"imported"`
	Skipped  int `json:"skipped"`
}

type Token

type Token struct {
	ClientID     string `json:"-"`
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	ExpiresIn    int    `json:"expires_in"`
	Scope        string `json:"scope"`
}

type TokenStore

type TokenStore struct {
	DB     *sql.DB
	Sealer config.FieldSealer
}

TokenStore stores a grant per viewer, with both fields bound to their row. A functioning field sealer is mandatory: this component never stores a new grant in cleartext, including during the legacy credential upgrade.

func (TokenStore) AcquireLease

func (s TokenStore) AcquireLease(ctx context.Context, user int64, now time.Time) (string, error)

AcquireLease reserves a viewer grant across nodes for bounded remote work. Operations must end before the deadline; every update must also check the lease id, since a paused process may wake after losing its reservation.

func (TokenStore) Delete

func (s TokenStore) Delete(ctx context.Context, user int64) error

func (TokenStore) Load

func (s TokenStore) Load(ctx context.Context, user int64) (Grant, error)

func (TokenStore) Pop

func (s TokenStore) Pop(ctx context.Context, user int64) (Grant, error)

Pop removes a grant and its queue under the same viewer lock used for account replacement and enqueue, returning only the grant actually removed.

func (TokenStore) Refresh

func (s TokenStore) Refresh(ctx context.Context, user int64, lease string, auth AuthClient) (Grant, error)

Refresh under a viewer lease prevents two nodes invalidating each other's access token. The conditional update cannot recreate a disconnected grant.

func (TokenStore) ReleaseLease

func (s TokenStore) ReleaseLease(ctx context.Context, user int64, id string) error

func (TokenStore) Save

func (s TokenStore) Save(ctx context.Context, user int64, token Token, now time.Time) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL