stash

package module
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: GPL-3.0 Imports: 18 Imported by: 0

README

stash-go

CI codecov Go Reference Go Report Card

A Go client for the GraphQL API of a running Stash server.

Stash ships a Go client for its plugins (pkg/plugin/util), but it is wired to localhost and shaped for that use. Anything talking to a Stash instance from outside ends up hand-rolling the transport, auth and error handling. This is that layer, written once.

import "github.com/Anastylosis/stash-go"

c := stash.NewClient("http://localhost:9999", stash.WithAPIKey(key))

scenes, err := c.FindAllScenes(ctx, stash.SceneFilter{StudioName: "Example"}, nil)

What it gives you

  • No dependencies. Standard library only.
  • Your HTTP client. WithHTTPClient takes whatever retry, backoff, proxy and timeout behaviour your program already uses. The library imposes none.
  • Typed errors. *APIError carries the GraphQL messages, *HTTPError the status — so you can branch on a schema mismatch versus a bad key without matching on error text.
  • The API key never reaches an error string. Some GraphQL middlewares echo the request back on auth failure, and those messages get logged.
  • Sentinel errors where Stash answers with silence. A filter naming a performer that does not exist returns an empty result set, so a typo looks exactly like a genuine no-match. ErrPerformerNotFound, ErrStudioNotFound and ErrTagNotFound make the difference visible.
  • Partial updates that cannot clear a field by accident — and separate calls for when clearing is what you meant.

What it covers

Scenes, performers, studios and tags; the files and fingerprints behind a scene; the merge, delete and move calls behind deduplication; saved filters; plugins and the package manager; the metadata tasks and the jobs they run; database backup; submitting to a stash-box; and administering the server itself — status, logs, general and interface settings, the API key, the database migrations and DLNA.

That is 32 of Stash's 62 queries and 60 of its 125 mutations.

The goal is the whole API. What is left is not a boundary, it is a to-do list, and it is mostly one shape: galleries, images, groups and markers account for 16 of the 30 remaining queries and 33 of the 65 remaining mutations — object types nothing has needed yet, so nothing has been written and tested for them. The rest is the URL scrapers, a handful of per-object bulk updates, the playback and o-counter calls, and whole-library import/export.

Two things are deliberate rather than pending. Raw SQL (querySQL, execSQL) will not be wrapped: arbitrary SQL against somebody's library is a footgun with no matching benefit. Neither will setup, which configures a server that has none, naming directories a client cannot see or validate.

Until a call is wrapped, Execute reaches it with the same transport, auth and error handling as the typed methods.

Which Stash it works against

Stash 0.31.1 (schema 85). That is what this targets, what the live suite runs against, and the only version anything has been checked on.

Older servers are not supported. This is a deliberate simplification rather than an untested hedge: the selection sets name fields directly, and GraphQL fails the whole query when asked for one the schema lacks, so a renamed field costs the entire response rather than itself. Shapes do drift — Stash's date criterion requires a value even when the modifier ignores it, and career_start is declared String although it holds a year — and chasing them across releases bought less than pinning to one did.

Where a field is known to vary, ask first:

if ok, _ := c.Supports(ctx, "groups"); ok {
    // safe to name it in your own Execute query
}

Introspection runs once per client and is cached. It is there for a consumer reaching past the wrapped surface with Execute, not for the wrapped calls themselves — those name what the target server has. Scene.captions used to be gated behind it and an opt-in option; both are gone, and captions are in the shared selection set with everything else.

Three things that surprise people

A partial update cannot empty a field. SceneUpdate sends only what you set, so an unset Title leaves the stored one alone — which is what makes it safe, and what makes Title: "" mean "leave it" rather than "clear it".

title := "Corrected"
err := c.UpdateScene(ctx, stash.SceneUpdate{ID: id, Title: &title})
err = c.ClearSceneFields(ctx, id, "title")   // actually empties it

ClearPerformerFields, ClearStudioFields and ClearTagFields do the same for the others, sending the empty value each field's type wants.

List fields replace rather than add. SceneUpdate.TagIDs overwrites a scene's tags, so adding one through it means a read-modify-write that loses whatever arrived in between. AddSceneTags and AddScenePerformers use Stash's own ADD mode instead, for many scenes in one request.

Captions are read-only. A subtitle is attached by writing a sidecar next to the video and making Stash scan for it; there is no mutation that attaches one.

job, err := c.MetadataScan(ctx, stash.ScanOptions{Paths: []string{dir}})

Scanning and generating are background jobs, so they return an id rather than waiting:

for {
    j, found, err := c.FindJob(ctx, job)
    if err != nil || !found || j.Status.Done() {
        break
    }
    time.Sleep(2 * time.Second)
}

Status.Done() covers all three terminal states — treating CANCELLED as still-running turns that loop into a hang.

Every generate flag defaults to off, which is not Stash's own default. A generate across a library is hours of work and gigabytes of output, so ask for what you want:

job, err := c.MetadataGenerate(ctx, stash.GenerateOptions{
    Sprites: true, Phashes: true, SceneIDs: []string{id},
})

Documentation

  • docs/usage.md — the API, call by call
  • docs/design.md — why it is shaped this way, and what it deliberately does not do
  • CONTRIBUTING.md — cutting a release, and the constraints any addition has to respect

Tests

go test ./... needs no network — every test drives an httptest stub, and CI enforces that nothing reaches out.

Two properties are asserted across every method that talks to the server, so anything added later is covered the moment it joins the list: that it reports a problem rather than returning a zero value and no error, against a GraphQL error, an HTTP 500, an HTTP 401 and a body that is not JSON; and that it never lets the API key into an error string.

A second suite runs against a real server. It is read-only by contract: it queries, and never creates, updates or deletes.

STASH_URL=http://your-server:9999 STASH_API_KEY=… go test -tags integration ./...

Without a reachable server the whole suite skips.

The calls that write are therefore not exercised live — scans, generates, backups, package installs, the entity mutations, and the administration calls that migrate, anonymise or replace the API key are pinned by unit tests, and the live suite checks the shapes they send against the server's own schema instead. A scan is an hours-long mutation against somebody's real library; a test should not start one, and nothing should replace a running server's credential to prove it can.

Status

Useful and in production against one library, not finished.

Solid in the sense that matters: the surface it has is tested, the error paths with it, and three real bugs found by using it are fixed. Incomplete in that half the API is not here, and only one Stash version has ever been checked.

Versions follow vMAJOR.MINOR.PATCH. Everything since v0.7.0 has been additive — no call has changed its signature or its behaviour.

License

Copyright (C) 2026 Wasylq

GPL-3.0-only.

Documentation

Overview

Package stash is a Go client for the GraphQL API of a running Stash server (https://stashapp.cc).

It has no dependencies outside the standard library, and it does not impose an HTTP client: pass your own with WithHTTPClient to get whatever retry, timeout and transport behaviour your program already uses.

c := stash.NewClient("http://localhost:9999", stash.WithAPIKey(key))
scenes, _, err := c.FindScenes(ctx, stash.SceneFilter{}, 1, 100)

The server's schema varies by version. Ask before relying on a field that older releases lack — see Client.Supports.

Index

Examples

Constants

View Source
const DefaultMaxResponseBytes = 50 << 20 // 50 MiB

DefaultMaxResponseBytes caps how much of a response is read. A full page of scenes with metadata is large, but a runaway server should not be able to exhaust the caller's memory.

View Source
const FileFields = `` /* 201-byte string literal not displayed */

FileFields is the selection set File decodes from, exported for the same reason as SceneFields.

View Source
const ManifestSuffix = ".manifest.json"

ManifestSuffix is appended to a backup's filename to name its manifest.

View Source
const PerformerFields = `` /* 293-byte string literal not displayed */

PerformerFields is the selection the performer queries use. Exported for the same reason SceneFields is: a caller writing its own query through Client.Execute can drop it in and decode the result into Performer.

View Source
const SQLiteMagic = "SQLite format 3\x00"

SQLiteMagic opens every SQLite 3 database file.

View Source
const SceneFields = `` /* 588-byte string literal not displayed */

SceneFields is the selection set Scene decodes from, exported so a caller writing its own query with Client.Execute fills the same type completely rather than maintaining a parallel field list that drifts.

query := `query { sceneWall(q: "beach") { ` + stash.SceneFields + ` } }`

Every field here must exist on the oldest supported server — see docs/design.md.

View Source
const StudioFields = `` /* 132-byte string literal not displayed */

StudioFields is the selection the studio queries use. Exported for the same reason SceneFields is.

child_studios is deliberately absent: a studio with many children would carry them all on every query, and a caller that wants the tree can ask for it. parent_studio is one level deep for the same reason.

View Source
const TagFields = `` /* 147-byte string literal not displayed */

TagFields is the selection the tag queries use.

Variables

View Source
var (
	// ErrNotSQLite means the file does not begin with [SQLiteMagic]:
	// whatever was downloaded, it is not a database. A login page or a
	// proxy's error page answering the download URL looks like this.
	ErrNotSQLite = errors.New("stash: not a SQLite database")
	// ErrTruncatedBackup means the file is a SQLite database whose header
	// describes more bytes than the file has. A transfer that dropped
	// mid-stream looks like this, and the prefix it leaves opens fine.
	ErrTruncatedBackup = errors.New("stash: truncated backup")
	// ErrBlobsNotVerifiable means [BackupOptions.IncludeBlobs] was set: a
	// server keeping blobs on disk then answers with a zip of database and
	// blobs, which is not a SQLite file and cannot be checked.
	ErrBlobsNotVerifiable = errors.New("stash: a backup with blobs cannot be verified")
)
View Source
var (
	// ErrPerformerNotFound means no performer has the requested name. Stash
	// matches names exactly, so this is usually a typo or stray whitespace.
	ErrPerformerNotFound = errors.New("stash: no such performer")
	// ErrStudioNotFound means no studio has the requested name.
	ErrStudioNotFound = errors.New("stash: no such studio")
	// ErrTagNotFound means no tag has the requested name.
	ErrTagNotFound = errors.New("stash: no such tag")
)

Sentinel errors. Stash answers "no such performer" with an empty result set rather than an error, so a typo in a filter is otherwise indistinguishable from a filter that legitimately matched nothing.

View Source
var ErrNotFound = errors.New("stash: not found")

ErrNotFound is what Client.Fetch returns for a 404, so a caller can tell it from the other failures without inspecting a status code. Stash generates sprites, previews and covers lazily, which makes a missing one an ordinary state of a scene rather than a reason to stop.

Functions

func Union added in v0.10.0

func Union(dst Scene, srcs []Scene, p UnionPolicy) (SceneUpdate, []Conflict)

Union computes the values a merge into dst needs so that nothing the sources know is lost, since sceneMerge on its own keeps only dst's metadata. The result is partial: only fields that would change are set, and it is the zero SceneUpdate when nothing would. Its ID is left empty; Client.MergeScenes addresses it at the destination.

Lists deduplicate by ID, stash IDs by (endpoint, id), and dst's order comes first. When the scenes disagree on the ID for one endpoint, dst wins over a source and an earlier source over a later one; every ID dropped that way is returned as a Conflict.

The function is pure: it neither reads nor writes Stash, and does not modify its arguments.

func VerifySQLite added in v0.10.0

func VerifySQLite(r io.ReaderAt, size int64) error

VerifySQLite reports whether r, of exactly size bytes, is a whole SQLite database: it has to begin with SQLiteMagic, and the page count in its header has to agree with size.

The page-count comparison is skipped, rather than failed, when the header's count is not authoritative. SQLite only trusts it while the change counter at offset 24 matches the version-valid-for number at offset 92, and a database last written by a very old library leaves it zero. The magic is still required in that case; the length simply cannot be confirmed from the header alone.

Types

type APIError

type APIError struct {
	Errors []GraphQLError
}

APIError is one or more errors returned in a GraphQL response body. The request itself succeeded at the HTTP level.

Inspect Errors to distinguish a schema mismatch ("Cannot query field …") from an authentication failure, rather than matching on error text.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Messages

func (e *APIError) Messages() []string

Messages returns just the message strings, for the common case where the structure is not needed.

type AutoTagOptions added in v0.8.0

type AutoTagOptions struct {
	Paths      []string
	Performers []string
	Studios    []string
	Tags       []string
}

AutoTagOptions configures Client.MetadataAutoTag.

Each list names what to match against, and "*" means all of that kind — which is what the UI's button sends. Empty lists match nothing, so a call with none set is a job that does nothing.

type BackupManifest added in v0.10.0

type BackupManifest struct {
	// File is the server's name for the backup, and the filename it was
	// saved under.
	File   string       `json:"file"`
	Bytes  int64        `json:"bytes"`
	SHA256 string       `json:"sha256"`
	Server BackupServer `json:"server"`
}

BackupManifest records what Client.DownloadVerifiedBackup wrote and which server it came from. It is written beside the backup as JSON.

type BackupOptions added in v0.7.0

type BackupOptions struct {
	// IncludeBlobs asks for blob data — covers, and the rest of what Stash
	// stores as blobs — to be included.
	//
	// It changes nothing on a server that keeps blobs in the database, which
	// is what an empty blobsPath in the configuration means: there they are
	// part of the file whatever this is set to. It matters on a server
	// storing blobs on the filesystem, where a backup without them is not a
	// backup of everything.
	IncludeBlobs bool
}

BackupOptions configures a database backup.

type BackupServer added in v0.10.0

type BackupServer struct {
	Version      string `json:"version"`
	Schema       int    `json:"schema"`
	OS           string `json:"os"`
	DatabasePath string `json:"database_path"`
	SceneCount   int    `json:"scene_count"`
}

BackupServer is what a manifest records about the server a backup came from. The database path in particular: a file called local.sqlite says nothing about which machine or which Stash it belongs to.

type BatchTagOptions added in v0.9.0

type BatchTagOptions struct {
	// Endpoint is the stash-box GraphQL URL to match against. Required —
	// Stash will not guess even when only one box is configured.
	Endpoint string

	// IDs restricts the job to these entities. Empty means every one of that
	// kind, which on a large library is a long job.
	IDs []string
	// Names asks the box about these names instead of about entities.
	Names []string

	// Refresh re-queries entities that already carry a stash id for this
	// endpoint. Without it Stash only visits the ones with none, which is
	// what makes a repeat run cheap.
	Refresh bool

	// ExcludeFields names the fields the job must not write, so a library
	// that trusts its own data more than the box's can keep it. The names are
	// the box's, not Stash's: "name", "aliases", "description", "image".
	ExcludeFields []string

	// CreateParent creates a missing parent studio rather than leaving the
	// studio unparented. Only meaningful for [BatchTagStudios].
	CreateParent bool
}

BatchTagOptions selects what a batch tag job covers and how much of it it is allowed to change.

Either IDs or Names, not both: ids name entities the library already has, where names ask the box to find something the library only knows by name.

type BatchTagTarget added in v0.9.0

type BatchTagTarget string

BatchTagTarget is what a stash-box batch tag job works on.

const (
	// BatchTagPerformers matches performers against the box.
	BatchTagPerformers BatchTagTarget = "stashBoxBatchPerformerTag"
	// BatchTagStudios matches studios.
	BatchTagStudios BatchTagTarget = "stashBoxBatchStudioTag"
	// BatchTagTags matches tags.
	BatchTagTags BatchTagTarget = "stashBoxBatchTagTag"
)

type Caption added in v0.6.0

type Caption struct {
	LanguageCode string `json:"language_code"`
	CaptionType  string `json:"caption_type"`
}

Caption is one subtitle track Stash has attached to a scene. Stash discovers these by scanning for sidecar files next to the video; they are read-only in GraphQL, so a caption cannot be attached over the API — only written to disk and picked up by Client.MetadataScan.

LanguageCode is the bare ISO 639 subtag Stash parsed off the filename (`clip.pt.srt` gives "pt"). Stash parses it with language.ParseBase and silently attaches nothing for anything it cannot parse, so a regional tag never appears here — a file named `clip.pt-BR.srt` is simply not a caption as far as Stash is concerned.

type CleanOptions added in v0.8.0

type CleanOptions struct {
	// Paths restricts the clean to these library paths.
	Paths []string
	// DryRun makes Stash report what it would remove without removing it.
	// Worth using first: clean deletes database records for files it cannot
	// find, and a library on a disconnected drive looks exactly like a
	// library whose files were deleted.
	DryRun bool
	// IgnoreZipFileContents skips files inside zips.
	IgnoreZipFileContents bool
}

CleanOptions configures Client.MetadataClean.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to one Stash server. It is safe for concurrent use.

func NewClient

func NewClient(baseURL string, opts ...Option) *Client

NewClient returns a client for the Stash server at baseURL, which is the server root ("http://localhost:9999") — "/graphql" is appended.

func (*Client) AddScenePerformers added in v0.7.0

func (c *Client) AddScenePerformers(ctx context.Context, performerIDs []string, sceneIDs ...string) error

AddScenePerformers adds performers to scenes, leaving the ones they already have alone — the same reason Client.AddSceneTags exists.

func (*Client) AddSceneTags added in v0.7.0

func (c *Client) AddSceneTags(ctx context.Context, tagIDs []string, sceneIDs ...string) error

AddSceneTags adds tags to scenes, leaving the tags they already have alone.

This is not what SceneUpdate.TagIDs does. That field *replaces* a scene's tags, so adding one through it means reading the current list, appending, and writing it back — which drops any tag added in between, and drops all of them if the read is skipped. Stash's bulk update takes an ADD mode instead, and applies it to every scene named in one request.

func (*Client) AllowDLNAIP added in v0.9.0

func (c *Client) AllowDLNAIP(ctx context.Context, address string, d time.Duration) error

AllowDLNAIP lets one address use the DLNA service for the given duration, or until the server restarts when d is zero. It is a temporary grant on top of the configured whitelist, not an addition to it.

The address is a bare IP — the form Client.DLNAStatus reports in RecentIPAddresses, which is where a device that has just tried and been refused shows up.

func (*Client) AnonymiseDatabase added in v0.9.0

func (c *Client) AnonymiseDatabase(ctx context.Context) (serverPath string, err error)

AnonymiseDatabase writes a copy of the database with every name, path, URL and free-text field stripped, and returns the path it wrote.

This is what a bug report attaches: it keeps the shape of a library — the counts, the relationships, the schema — and none of what it is a library of. The path is on the *server's* filesystem; Client.DownloadAnonymisedDatabase fetches the copy instead, which is usually the point.

It is a copy. Nothing about the live database changes.

func (*Client) AssignFile added in v0.9.0

func (c *Client) AssignFile(ctx context.Context, sceneID, fileID string) error

AssignFile attaches an existing file to a scene, moving it from whatever scene held it.

This is how a file that Stash matched to the wrong scene is put right without deleting anything.

func (*Client) AvailablePackages added in v0.7.0

func (c *Client) AvailablePackages(ctx context.Context, t PackageType, sourceURL string) ([]Package, error)

AvailablePackages returns what the index at sourceURL offers.

The server fetches that index over the internet when the call is made, so this is slower than it looks and fails when the server is offline — not when this program is.

func (*Client) BackupDatabase added in v0.7.0

func (c *Client) BackupDatabase(ctx context.Context, opts BackupOptions) (serverPath string, err error)

BackupDatabase asks the server to write a backup of its database and returns the path it wrote.

That path is on the *server's* filesystem, in the server's own notation: a Windows-hosted Stash answers with something like `C:\Users\you\.stash\local.sqlite.85.20260101_000000`, which the calling machine has no way to open. Where it lands is the server's backupDirectoryPath setting, or the database's own directory when that is unset.

A backup that stays on the machine being backed up is a limited kind of insurance. Client.DownloadBackup fetches one instead.

func (*Client) ClearAPIKey added in v0.9.0

func (c *Client) ClearAPIKey(ctx context.Context) error

ClearAPIKey removes the server's API key without issuing a new one. Clients authenticating with it — this one included — stop working; a session login still does.

func (*Client) ClearPerformerFields added in v0.8.0

func (c *Client) ClearPerformerFields(ctx context.Context, id string, fields ...string) error

ClearPerformerFields empties the named fields, which PerformerUpdate cannot: it omits what is unset so that a partial update is safe, and that makes "" indistinguishable from "leave it".

Names are the PerformerUpdateInput field names — "birthdate", "details", "alias_list", "measurements". A name Stash does not know fails the whole mutation rather than being ignored.

func (*Client) ClearSceneFields added in v0.8.0

func (c *Client) ClearSceneFields(ctx context.Context, id string, fields ...string) error

ClearSceneFields empties the named fields, which SceneUpdate cannot: it omits what is unset so that a partial update is safe, and that makes "" and "leave it alone" the same request.

Names are the SceneUpdateInput field names — "title", "details", "date", "code". A name Stash does not know fails the whole mutation rather than being ignored.

func (*Client) ClearStudioFields added in v0.8.0

func (c *Client) ClearStudioFields(ctx context.Context, id string, fields ...string) error

ClearStudioFields empties the named fields, which StudioInput cannot.

func (*Client) ClearTagFields added in v0.8.0

func (c *Client) ClearTagFields(ctx context.Context, id string, fields ...string) error

ClearTagFields empties the named fields, which TagInput cannot.

func (*Client) Configuration added in v0.6.0

func (c *Client) Configuration(ctx context.Context) (map[string]any, error)

Configuration returns the server's whole configuration tree as decoded JSON. Stash's ConfigResult is a large, version-dependent shape whose sections gain and lose fields between releases, so this deliberately does not model it: a typed struct would fail the entire query the first time a field it names is dropped, which is the failure mode this library exists to spare callers.

Prefer Client.PluginSettings when that is what you want — it asks for one section rather than the whole tree.

func (*Client) ConfigureGeneral added in v0.9.0

func (c *Client) ConfigureGeneral(ctx context.Context, settings map[string]any) error

ConfigureGeneral writes the given general settings and leaves the rest alone: Stash applies only the keys present in the input.

Keys are the ConfigGeneralInput field names ("logLevel", "maxSessionAge", "previewSegments", …), unmodelled here for the reason Client.Configuration gives.

Two things this cannot do gently. A list-valued field is *replaced* rather than extended — sending one entry of "stashes" makes it the only library path Stash has, and Client.SetStashBoxes exists because that same trap costs API keys. And several of these are how the server reaches its own data: point databasePath or generatedPath somewhere new and Stash starts afresh there rather than moving anything. Read a field before writing it.

func (*Client) ConfigureInterface added in v0.7.0

func (c *Client) ConfigureInterface(ctx context.Context, settings map[string]any) error

ConfigureInterface writes the given interface settings and leaves the rest alone, the same way Client.UpdateScene does — Stash applies only the keys present in the input.

Keys are the ConfigInterfaceInput field names ("javascript", "javascriptEnabled", "css", "cssEnabled", …). They are not modelled here for the reason Client.Configuration gives: the section gains and loses fields between releases, and a struct naming them all would fail the whole mutation the first time one went away.

Custom JavaScript in particular runs in every browser that opens this Stash. Read it before writing it, and preserve what is there.

func (*Client) CreatePerformer

func (c *Client) CreatePerformer(ctx context.Context, name string) (string, error)

CreatePerformer creates a performer and returns its ID.

func (*Client) CreatePerformerFrom added in v0.7.0

func (c *Client) CreatePerformerFrom(ctx context.Context, in PerformerInput) (string, error)

CreatePerformerFrom creates a performer with full details and returns its ID. Client.CreatePerformer is the same call for a name alone.

func (*Client) CreateStudio

func (c *Client) CreateStudio(ctx context.Context, name string) (string, error)

CreateStudio creates a studio and returns its ID.

func (*Client) CreateStudioFrom added in v0.8.0

func (c *Client) CreateStudioFrom(ctx context.Context, in StudioInput) (string, error)

CreateStudioFrom creates a studio with full details and returns its ID. Client.CreateStudio is the same call for a name alone.

func (*Client) CreateTag

func (c *Client) CreateTag(ctx context.Context, name string) (string, error)

CreateTag creates a tag and returns its ID.

func (*Client) CreateTagFrom added in v0.8.0

func (c *Client) CreateTagFrom(ctx context.Context, in TagInput) (string, error)

CreateTagFrom creates a tag with full details and returns its ID. Client.CreateTag is the same call for a name alone.

func (*Client) DLNAStatus added in v0.9.0

func (c *Client) DLNAStatus(ctx context.Context) (DLNAStatus, error)

DLNAStatus reports whether the DLNA service is running, which addresses have been asking for it, and which are allowed to.

func (*Client) DeleteFiles added in v0.9.0

func (c *Client) DeleteFiles(ctx context.Context, fileIDs ...string) error

DeleteFiles deletes the files from disk and removes their records.

Permanent, and it says so plainly because the name does not distinguish itself from Client.DestroyFiles: this one is "delete these videos". Stash offers both mutations and they are not synonyms — the other keeps the file.

func (*Client) DeletePerformer added in v0.8.0

func (c *Client) DeletePerformer(ctx context.Context, id string) error

DeletePerformer removes a performer.

The performer's scenes are not touched; they simply lose the credit. There is no undo, and no confirmation — Stash deletes on being asked.

func (*Client) DeletePerformers added in v0.8.0

func (c *Client) DeletePerformers(ctx context.Context, ids ...string) error

DeletePerformers removes several performers in one request.

All or nothing: Stash checks every id first, and one that does not exist fails the whole call with nothing deleted. That matters after a merge, which has already removed its sources — passing them again deletes none of the rest.

func (*Client) DeleteScene added in v0.9.0

func (c *Client) DeleteScene(ctx context.Context, id string, opts DeleteOptions) error

DeleteScene removes one scene.

With a zero DeleteOptions this removes the database record only and the video stays on disk. Set DeleteFile and the video is deleted, permanently.

func (*Client) DeleteScenes added in v0.9.0

func (c *Client) DeleteScenes(ctx context.Context, ids []string, opts DeleteOptions) error

DeleteScenes removes several scenes in one request, on the same terms as Client.DeleteScene.

func (*Client) DeleteStudio added in v0.8.0

func (c *Client) DeleteStudio(ctx context.Context, id string) error

DeleteStudio removes a studio. Its scenes are not touched; they lose the studio.

func (*Client) DeleteStudios added in v0.8.0

func (c *Client) DeleteStudios(ctx context.Context, ids ...string) error

DeleteStudios removes several studios in one request. All or nothing: one id that does not exist fails the call with nothing deleted.

func (*Client) DeleteTag added in v0.8.0

func (c *Client) DeleteTag(ctx context.Context, id string) error

DeleteTag removes a tag. Scenes carrying it simply lose it.

func (*Client) DeleteTags added in v0.8.0

func (c *Client) DeleteTags(ctx context.Context, ids ...string) error

DeleteTags removes several tags in one request. All or nothing: one id that does not exist fails the call with nothing deleted.

func (*Client) DestroyFiles added in v0.9.0

func (c *Client) DestroyFiles(ctx context.Context, fileIDs ...string) error

DestroyFiles removes the files' records from the database and leaves the videos on disk.

This is the reversible one, and the name is the opposite way round from what that suggests: Stash's own description is "deletes file entries from the database without deleting the files from the filesystem". A later scan finds the files again and re-adds them, so this un-does itself unless the files are moved out of a library path first.

Client.DeleteFiles is the one that deletes the video.

func (*Client) DestroySavedFilter added in v0.7.0

func (c *Client) DestroySavedFilter(ctx context.Context, id string) error

DestroySavedFilter deletes a saved filter by id.

func (*Client) DisableDLNA added in v0.9.0

func (c *Client) DisableDLNA(ctx context.Context, d time.Duration) error

DisableDLNA stops the DLNA service for the given duration, or until it is enabled again when d is zero.

func (*Client) DisallowDLNAIP added in v0.9.0

func (c *Client) DisallowDLNAIP(ctx context.Context, address string) error

DisallowDLNAIP revokes a grant made by Client.AllowDLNAIP. It says nothing about an address in the configured whitelist, which this cannot reach.

func (*Client) DownloadAnonymisedDatabase added in v0.9.0

func (c *Client) DownloadAnonymisedDatabase(ctx context.Context, w io.Writer) (name string, written int64, err error)

DownloadAnonymisedDatabase anonymises the database and streams the copy to w, returning the server's name for it and the number of bytes written.

The caveats are Client.DownloadBackup's, for the same reasons: the transfer is not bounded by WithMaxResponseBytes, the HTTP client's timeout covers the whole of it, and the server leaves its temporary copy behind until it restarts.

func (*Client) DownloadBackup added in v0.7.0

func (c *Client) DownloadBackup(ctx context.Context, opts BackupOptions, w io.Writer) (name string, written int64, err error)

DownloadBackup backs up the server's database and streams it to w, returning the server's name for the backup and the number of bytes written.

The server writes the backup to a temporary file and serves that over HTTP. It does not delete the temporary file once the download finishes — it clears that directory on restart — so a program backing up on a schedule leaves copies on the server's temp volume.

Nothing here is bounded by WithMaxResponseBytes. That cap protects a caller decoding a GraphQL response into memory; this is a stream to a writer the caller chose, of a database that is hundreds of megabytes on a real library. For the same reason the HTTP client's timeout matters more than usual: it covers the whole transfer, not just the response headers, and the default client's is 30 seconds. Pass one with no timeout (see WithHTTPClient) and bound the transfer with ctx instead.

A short write to w aborts the download and is returned as-is, so a full disk does not leave a truncated file looking like a complete one.

func (*Client) DownloadFFMpeg added in v0.9.0

func (c *Client) DownloadFFMpeg(ctx context.Context) (jobID string, err error)

DownloadFFMpeg has the server fetch ffmpeg and ffprobe for its own platform and put them beside its configuration, returning the id of the job doing it.

The download is the *server's*, from the internet, and it is what a Stash with no system ffmpeg needs before it can generate anything. A server that already found ffmpeg on its PATH does not need this, and running it anyway gives Stash its own copy to prefer.

func (*Client) DownloadVerifiedBackup added in v0.10.0

func (c *Client) DownloadVerifiedBackup(ctx context.Context, opts BackupOptions, dir string) (BackupManifest, error)

DownloadVerifiedBackup backs up the server's database into dir, checks that what arrived is a whole SQLite database, and writes a manifest beside it.

The download lands in `<name>.part`, where name is the server's name for the backup, and is renamed once VerifySQLite accepts it; the manifest goes to `<name>.manifest.json`. Any failure removes the partial file and returns the error, so the directory never holds a half-backup under a usable name. ErrNotSQLite and ErrTruncatedBackup are the two ways the transfer itself can be bad.

The server is described before the backup is taken, and one that is not SystemStatus.Ready is refused: a database mid-migration is the one moment the file on disk is least worth having. Everything Client.DownloadBackup says about timeouts applies here too. Blobs cannot be included — see ErrBlobsNotVerifiable.

func (*Client) EnableDLNA added in v0.9.0

func (c *Client) EnableDLNA(ctx context.Context, d time.Duration) error

EnableDLNA starts the DLNA service for the given duration, or until it is disabled again when d is zero.

None of this touches the configuration: a temporary enable is forgotten on restart, when the dlnaEnabled setting decides again.

func (*Client) EnsurePerformer

func (c *Client) EnsurePerformer(ctx context.Context, name string) (string, error)

EnsurePerformer returns the ID of a performer with this name, creating it if absent.

func (*Client) EnsureStudio

func (c *Client) EnsureStudio(ctx context.Context, name string) (string, error)

EnsureStudio returns the ID of a studio with this name, creating it if absent.

func (*Client) EnsureTag

func (c *Client) EnsureTag(ctx context.Context, name string) (string, error)

EnsureTag returns the ID of a tag with this name, creating it if neither the name nor an alias matches.

func (*Client) Execute

func (c *Client) Execute(ctx context.Context, query string, variables map[string]any) (json.RawMessage, error)

Execute runs a raw GraphQL query or mutation and returns the `data` object.

Exported so callers can reach parts of the schema this package does not wrap, without hand-rolling the transport, auth and error handling again.

func (*Client) Fetch added in v0.7.0

func (c *Client) Fetch(ctx context.Context, url string, w io.Writer) (contentType string, n int64, err error)

Fetch streams one of the server's plain HTTP resources to w and returns its content type and length.

Scenes carry URLs to things GraphQL will not hand over as data — the sprite sheet, its WebVTT, the cover, the stream — and those routes want the same credential as /graphql. This applies it, and re-roots the URL the way Client.DownloadBackup does, so a URL the server built from a proxied request still resolves to the address this client was given.

url may be absolute or a path. As with the backup download, neither WithMaxResponseBytes nor a short HTTP client timeout suits a stream; bound it with ctx.

func (*Client) FindAllScenes

func (c *Client) FindAllScenes(ctx context.Context, filter SceneFilter, progress Progress) ([]Scene, error)

FindAllScenes pages through every scene matching the filter.

On a large library this is a long operation — a 61k-scene instance takes several minutes and hundreds of requests. Pass a non-nil progress to report it, and a cancellable context: cancellation returns what was collected so far alongside ctx.Err().

Example
c := NewClient("http://localhost:9999", WithAPIKey("key"))

scenes, err := c.FindAllScenes(context.Background(), SceneFilter{StudioName: "Example"},
	func(fetched, total int) { fmt.Printf("\r%d/%d", fetched, total) })
if err != nil {
	return
}
fmt.Println(len(scenes))

func (*Client) FindDuplicateScenes added in v0.9.0

func (c *Client) FindDuplicateScenes(ctx context.Context, distance int, durationDiff float64) ([][]Scene, error)

FindDuplicateScenes returns groups of scenes Stash considers the same content, matched on the perceptual hash of the video rather than on any metadata. Each group holds two or more scenes; a library with no duplicates returns none.

distance is the hamming distance allowed between two phashes. 0 demands identical hashes, which finds the same encode stored twice. 4 is the useful setting and catches re-encodes and resolution changes. Past 8 the matches stop being trustworthy.

durationDiff bounds how far apart two scenes' runtimes may be, in seconds, and is the strong filter of the two: phash collides across unrelated videos often enough to matter, but a collision that also agrees on length to within a second rarely does. Pass 0 to demand equal durations, or a negative value to leave duration out of it.

The whole result arrives in one response, and every scene in it carries the full selection set — on a large library that is megabytes. There is no paged form of this query: Stash computes the grouping in one pass and has nowhere to hold it between calls.

func (*Client) FindFile added in v0.9.0

func (c *Client) FindFile(ctx context.Context, id string) (file *File, found bool, err error)

FindFile looks up one file by id.

func (*Client) FindFileByPath added in v0.9.0

func (c *Client) FindFileByPath(ctx context.Context, path string) (file *File, found bool, err error)

FindFileByPath looks up one file by its path on disk.

The path must be exactly as Stash stored it, separators included — on a Windows server that means backslashes. found is false when no such file is known, which is not an error.

func (*Client) FindJob added in v0.6.0

func (c *Client) FindJob(ctx context.Context, id string) (job *Job, found bool, err error)

FindJob returns one job by id. found is false when the job has aged out of the queue, which is not an error — Stash drops finished jobs after a while, so a poll that starts too late sees the same thing as a poll for a job that never existed.

func (*Client) FindPerformer

func (c *Client) FindPerformer(ctx context.Context, name string) (id string, found bool, err error)

FindPerformer returns the ID of the performer with this exact name.

func (*Client) FindPerformerByID added in v0.8.0

func (c *Client) FindPerformerByID(ctx context.Context, id string) (performer *Performer, found bool, err error)

FindPerformerByID returns one performer with everything Stash stores about them. found is false when there is no such performer, which is not an error.

func (*Client) FindPerformerByStashID added in v0.7.0

func (c *Client) FindPerformerByStashID(ctx context.Context, endpoint, stashID string) (id string, found bool, err error)

FindPerformerByStashID returns the ID of the performer carrying this stash-box id.

This is the identity check worth making before creating anything. Client.FindPerformer matches on a name, and a name is neither unique nor stable — two performers share one, one performer changes theirs, and a scraper writes it with different punctuation. A stash-box id is the same string forever.

func (*Client) FindPerformers added in v0.8.0

func (c *Client) FindPerformers(ctx context.Context, filter PerformerFilter, page, perPage int) ([]Performer, int, error)

FindPerformers returns one page of performers plus the total count. Pages are 1-based and sorted by name, so paging is stable.

func (*Client) FindSavedFilter added in v0.7.0

func (c *Client) FindSavedFilter(ctx context.Context, mode FilterMode, name string) (filter *SavedFilter, found bool, err error)

FindSavedFilter returns the saved filter with this name in this list.

Stash does not require names to be unique, so this returns the first match and a caller creating filters should check before creating a second one of the same name — which is what Client.SaveSceneFilter does.

func (*Client) FindScene

func (c *Client) FindScene(ctx context.Context, id string) (scene *Scene, found bool, err error)

FindScene returns one scene by ID. found is false when no scene has that ID, which is not an error.

func (*Client) FindSceneByHash added in v0.9.0

func (c *Client) FindSceneByHash(ctx context.Context, algorithm, hash string) (scene *Scene, found bool, err error)

FindSceneByHash finds the scene holding a file with this fingerprint.

Exact, unlike matching on a title or a path: the hash names one file. Which algorithm to pass depends on what the library has — "oshash" is computed on every scan, "phash" only when generated.

func (*Client) FindScenes

func (c *Client) FindScenes(ctx context.Context, filter SceneFilter, page, perPage int) ([]Scene, int, error)

FindScenes returns one page of scenes plus the total count matching the filter. Pages are 1-based and sorted by path, so paging is stable.

A filter naming a performer or studio that does not exist returns ErrPerformerNotFound or ErrStudioNotFound rather than an empty page — otherwise a typo is indistinguishable from a genuine zero-result query.

func (*Client) FindScenesByPathRegex added in v0.9.0

func (c *Client) FindScenesByPathRegex(ctx context.Context, pattern string, page, perPage int) ([]Scene, int, error)

FindScenesByPathRegex finds scenes whose file path matches a regular expression, one page at a time, and reports the total match count.

The pattern is evaluated by the server, in Go's regexp syntax, against the full path. This is the call for questions a filter cannot ask — "which scenes still live under the old naming scheme" — where SceneFilter's PathContains only does a substring.

func (*Client) FindStudio

func (c *Client) FindStudio(ctx context.Context, name string) (id string, found bool, err error)

FindStudio returns the ID of the studio with this exact name.

func (*Client) FindStudioByID added in v0.8.0

func (c *Client) FindStudioByID(ctx context.Context, id string) (studio *Studio, found bool, err error)

FindStudioByID returns one studio with everything Stash stores about it.

func (*Client) FindTag

func (c *Client) FindTag(ctx context.Context, name string) (id string, found bool, err error)

FindTag returns the ID of the tag with this exact name.

func (*Client) FindTagByAlias

func (c *Client) FindTagByAlias(ctx context.Context, alias string) (id string, found bool, err error)

FindTagByAlias returns the ID of a tag carrying this alias. Stash treats aliases as first-class, so a tag can be present under a name the caller does not know — checking aliases before creating avoids duplicates.

func (*Client) FindTagByID added in v0.8.0

func (c *Client) FindTagByID(ctx context.Context, id string) (tag *Tag, found bool, err error)

FindTagByID returns one tag with everything Stash stores about it.

func (*Client) GeneralConfig added in v0.9.0

func (c *Client) GeneralConfig(ctx context.Context, fields ...string) (map[string]any, error)

GeneralConfig returns the named fields of the server's general configuration — the section holding library paths, the database and blob locations, ffmpeg settings, the log file and the rest of Settings > System.

The caller names the fields for the reason Client.InterfaceConfig gives: the section is large, it changes between releases, and one field the schema lacks fails the whole query rather than just that field.

cfg, err := c.GeneralConfig(ctx, "databasePath", "blobsPath", "logFile")

Client.StashBoxConfigs is the way to read stashBoxes: it is the one field in here that carries credentials, and it has a type of its own.

func (*Client) GenerateAPIKey added in v0.9.0

func (c *Client) GenerateAPIKey(ctx context.Context) (string, error)

GenerateAPIKey replaces the server's API key and returns the new one.

There is exactly one key per Stash, so this invalidates the old one — and that includes the key this client is authenticating with, which stops working the moment the mutation returns. The new key does not apply itself: build a fresh client with it.

key, err := c.GenerateAPIKey(ctx)
c = stash.NewClient(url, stash.WithAPIKey(key))

The returned key is a credential in a variable rather than in an error string, so nothing redacts it for you — WithAPIKey's scrubbing covers the key a client was built with, not one it has just been handed.

A server with authentication disabled — no username and password configured — refuses this: Stash will not hand out a key that would then be the only thing standing in front of the library, or not stand there at all.

func (*Client) InstallPackages added in v0.7.0

func (c *Client) InstallPackages(ctx context.Context, t PackageType, specs ...PackageSpec) (jobID string, err error)

InstallPackages installs packages and returns the id of the job doing it. It does not wait; follow the job with Client.FindJob.

Stash downloads each package from its source and unpacks it into the server's plugin or scraper directory. A package already installed is reinstalled at the index's current version, so this is also how an update is forced.

Requirements are not resolved: a package whose Requires names something absent installs anyway, and fails when it runs. Check Package.Requires first.

func (*Client) InstalledPackages added in v0.7.0

func (c *Client) InstalledPackages(ctx context.Context, t PackageType) ([]Package, error)

InstalledPackages returns what is installed, whether or not its source is still configured.

func (*Client) InterfaceConfig added in v0.7.0

func (c *Client) InterfaceConfig(ctx context.Context, fields ...string) (map[string]any, error)

InterfaceConfig returns the named fields of the server's interface configuration.

The caller names the fields because the section is large and changes between releases, and one field the schema lacks fails the whole query. A caller asking for what it is about to write cannot be broken by a field it does not use.

func (*Client) JobQueue added in v0.6.0

func (c *Client) JobQueue(ctx context.Context) ([]Job, error)

JobQueue returns every job Stash currently knows about, queued or running. Empty when the server is idle.

func (*Client) LatestVersion added in v0.9.0

func (c *Client) LatestVersion(ctx context.Context) (shortHash, url string, err error)

LatestVersion returns the newest release Stash knows of, as a short commit hash and the URL to it.

The *server* fetches this from GitHub when the call is made. It therefore fails when the server has no route to the internet — which is not the same thing as this program having none — and it is slower than a local query. Nothing here compares it to Client.ServerVersion: the two are a tag and a hash, and only the server knows whether it is behind.

func (*Client) LibraryStats added in v0.9.0

func (c *Client) LibraryStats(ctx context.Context) (LibraryStats, error)

LibraryStats reports the server's own counts for the library.

This is the cheap way to size a library before doing anything with it: one query, answered from the database, where counting the same thing through Client.FindScenes would page through every scene.

func (*Client) Logs added in v0.9.0

func (c *Client) Logs(ctx context.Context) ([]LogEntry, error)

Logs returns the server's recent log entries, newest first.

This is not the log file. Stash keeps a bounded in-memory ring of the last few hundred entries and serves that, so a server restarted since the event has nothing to say about it, and a busy one has already dropped it. For anything that must not be missed, read the file the server's logFile setting names — Client.GeneralConfig reports where that is.

There is no way to follow the log from here: Stash streams new entries over a GraphQL subscription, which is a websocket this package does not open.

func (*Client) MergePerformers added in v0.8.0

func (c *Client) MergePerformers(ctx context.Context, destinationID string, sourceIDs []string, values *PerformerUpdate) error

MergePerformers folds the source performers into the destination and deletes them, moving their scenes across.

values, when set, is applied to the destination as part of the merge — the place to keep a source's better name or birthdate, since the destination's own fields otherwise win and the sources are gone afterwards.

This is not reversible.

func (*Client) MergeScenes added in v0.9.0

func (c *Client) MergeScenes(ctx context.Context, destinationID string, sourceIDs []string, values *SceneUpdate, opts MergeOptions) error

MergeScenes folds the source scenes into the destination and deletes them, moving their files across.

The destination keeps its own metadata; values is applied to it as part of the merge, which is where a source's better title or date goes — afterwards the sources are gone and there is nothing left to copy from. Stash does not union metadata by itself, so values is the whole of what survives from a source: compute it before calling.

This deletes database records, not files on disk: the sources' files are reattached to the destination. Deleting a file is Client.DeleteScene with DeleteFile set, or Client.DeleteFiles.

Not reversible.

func (*Client) MergeTags added in v0.8.0

func (c *Client) MergeTags(ctx context.Context, destinationID string, sourceIDs []string, values *TagInput) error

MergeTags folds the source tags into the destination and deletes them, moving everything they were on across.

values, when set, is applied to the destination as part of the merge — the place to keep a source's better name or description, since the sources are gone afterwards. This is not reversible.

func (*Client) MetadataAutoTag added in v0.8.0

func (c *Client) MetadataAutoTag(ctx context.Context, opts AutoTagOptions) (jobID string, err error)

MetadataAutoTag starts an auto-tag job and returns its id.

Auto-tag attaches performers, studios and tags to scenes whose *path* contains their name. That is a guess about filenames, and it writes: on a library whose files are named after their content it is useful, and on one where a performer is called "Angel" it is not.

func (*Client) MetadataClean added in v0.8.0

func (c *Client) MetadataClean(ctx context.Context, opts CleanOptions) (jobID string, err error)

MetadataClean starts a clean job and returns its id.

Clean removes the records of files that are no longer on disk. That is destructive and depends on the disk being readable at the time: an unmounted drive presents as a library whose files have all been deleted. Use DryRun first.

func (*Client) MetadataGenerate added in v0.8.0

func (c *Client) MetadataGenerate(ctx context.Context, opts GenerateOptions) (jobID string, err error)

MetadataGenerate starts a generate job and returns the id of the job doing it. It does not wait; follow it with Client.FindJob.

This is how a scene gets the sprite, cover or perceptual hash that other work needs and a plain scan does not produce.

func (*Client) MetadataIdentify added in v0.8.0

func (c *Client) MetadataIdentify(ctx context.Context, opts IdentifyOptions) (jobID string, err error)

MetadataIdentify starts an identify job and returns its id.

Identify is a *writing* task: it matches scenes against a stash-box and applies what it finds, according to the field rules configured on the server. Those rules decide whether it overwrites what is already there, and this call cannot see them — check them before starting one on a library whose metadata you care about.

func (*Client) MetadataScan added in v0.6.0

func (c *Client) MetadataScan(ctx context.Context, opts ScanOptions) (jobID string, err error)

MetadataScan starts a library scan and returns the id of the job doing it. It does not wait: scanning is a long-running background task, and the id is how you follow it with Client.FindJob.

This is the only way to make Stash notice a file that appeared on disk. Captions in particular are read-only in GraphQL, so writing a sidecar next to a video and calling this is the entire mechanism for attaching a subtitle.

func (*Client) Migrate added in v0.9.0

func (c *Client) Migrate(ctx context.Context, backupPath string) error

Migrate runs the database schema migration a server in SystemNeedsMigration is waiting for, writing a copy of the old database to backupPath first. An empty backupPath skips that copy.

Three things make this unlike the rest of the package. It is *irreversible* — a migrated database cannot be opened by the older Stash that wrote it, which is what the backup is for. It runs synchronously rather than as a job, so it holds the request open for as long as the migration takes; on a large library that is minutes, and the default HTTP client gives up after thirty seconds while the server carries on regardless (pass one without a timeout via WithHTTPClient and bound it with ctx). And a server in this state answers almost nothing else: Client.SystemStatus and this are what work.

backupPath is a path on the *server's* filesystem, in the server's own notation.

func (*Client) MigrateBlobs added in v0.9.0

func (c *Client) MigrateBlobs(ctx context.Context, deleteOld bool) (jobID string, err error)

MigrateBlobs moves blob data — covers, images, and the rest — between the database and the filesystem, following whatever the blobsPath setting now says. It returns the id of the job doing it; follow it with Client.FindJob.

deleteOld removes each blob from where it came from once it has been written to where it is going. With it false the data exists in both places afterwards, which is the safe way round and the reason it is not the default: a migration that turns out to have gone wrong is then still undoable by putting the setting back.

func (*Client) MigrateHashNaming added in v0.9.0

func (c *Client) MigrateHashNaming(ctx context.Context) (jobID string, err error)

MigrateHashNaming renames generated files — sprites, previews, covers — from the MD5 naming an old Stash used to the oshash naming it uses now, and returns the id of the job doing it.

Only a library that predates the change has anything to rename; on everything else the job runs and finds nothing. It is not reversible, and while it runs the generated files it has not reached yet are the ones the UI cannot find.

func (*Client) MigrateSceneScreenshots added in v0.9.0

func (c *Client) MigrateSceneScreenshots(ctx context.Context, opts ScreenshotMigration) (jobID string, err error)

MigrateSceneScreenshots reads the loose screenshot files an older Stash wrote next to its generated content and stores them as scene covers, returning the id of the job doing it.

This is the one-off that follows an upgrade past the release where covers stopped being files. A library that never had those files has nothing to migrate.

func (*Client) MoveFiles added in v0.9.0

func (c *Client) MoveFiles(ctx context.Context, fileIDs []string, to MoveTarget) error

MoveFiles moves files on disk and updates Stash to match.

This is a real move: the video changes place in the filesystem. Stash needs the destination to be inside a configured library path, or it will refuse rather than move a file somewhere it cannot see.

func (*Client) OptimiseDatabase added in v0.8.0

func (c *Client) OptimiseDatabase(ctx context.Context) (jobID string, err error)

OptimiseDatabase starts a database optimisation job and returns its id.

func (*Client) PackageSources added in v0.7.0

func (c *Client) PackageSources(ctx context.Context, t PackageType) ([]PackageSource, error)

PackageSources returns the indexes the server is configured to install from. A PackageSpec needs one of these URLs.

func (*Client) Ping

func (c *Client) Ping(ctx context.Context) error

Ping checks that the server is reachable and answering GraphQL.

func (*Client) PluginSettings added in v0.6.0

func (c *Client) PluginSettings(ctx context.Context, pluginID string) (map[string]any, error)

PluginSettings returns the stored settings for one plugin, keyed by the setting name its YAML declares. The plugin id is what Stash derives from the config filename — `moansubs.yml` gives "moansubs".

An unconfigured plugin, or one Stash has never heard of, returns an empty map rather than an error: a plugin whose settings have all been left at their defaults is indistinguishable from one that is not installed, and both mean "nothing has been set".

Values are whatever JSON Stash stored, so a caller asserting a type must tolerate what the UI actually writes. In particular a boolean setting the user has never touched can come back as nil rather than false, and a task's declared default arrives as a string.

func (*Client) Plugins added in v0.7.0

func (c *Client) Plugins(ctx context.Context) ([]Plugin, error)

Plugins returns every plugin the server has loaded, enabled or not.

This is not the same list as Client.InstalledPackages: a plugin installed from a source appears in both, one dropped into the plugins directory by hand appears only here, and one whose files are present but unparseable appears in neither.

func (*Client) ReloadPlugins added in v0.7.0

func (c *Client) ReloadPlugins(ctx context.Context) error

ReloadPlugins makes the server re-read its plugin directory. Needed after files change on disk; an install through the package manager does it itself.

func (*Client) RemoveScenePerformers added in v0.7.0

func (c *Client) RemoveScenePerformers(ctx context.Context, performerIDs []string, sceneIDs ...string) error

RemoveScenePerformers removes performers from scenes, leaving their others alone.

func (*Client) RemoveSceneTags added in v0.7.0

func (c *Client) RemoveSceneTags(ctx context.Context, tagIDs []string, sceneIDs ...string) error

RemoveSceneTags removes tags from scenes, leaving their other tags alone. Removing a tag a scene does not have is not an error.

func (*Client) SaveFilter added in v0.7.0

func (c *Client) SaveFilter(ctx context.Context, filter SavedFilter) (string, error)

SaveFilter creates a saved filter, or updates the one whose ID is set, and returns its id.

func (*Client) SaveSceneFilter added in v0.7.0

func (c *Client) SaveSceneFilter(ctx context.Context, name string, filter SceneFilter, find *FindFilter) (string, error)

SaveSceneFilter saves a scene filter under a name, replacing one of that name if it exists.

The criteria come from a SceneFilter, so a filter that selects scenes in a program and a filter the user clicks in the sidebar are the same thing written once — including the translation into the notation saved filters use, which is not the one queries use. find may be nil, in which case Stash's own defaults apply.

func (*Client) SavedFilters added in v0.7.0

func (c *Client) SavedFilters(ctx context.Context, mode FilterMode) ([]SavedFilter, error)

SavedFilters returns the saved filters for one list, in the order Stash holds them.

func (*Client) SceneFilterCriteria added in v0.7.0

func (c *Client) SceneFilterCriteria(ctx context.Context, filter SceneFilter) (map[string]any, error)

SceneFilterCriteria renders a SceneFilter as the criterion map Stash speaks, resolving performer, studio and tag names to ids on the way.

Exported because it is what a saved filter stores: the same filter that selects scenes here can be handed to Client.SaveSceneFilter and appear in the UI, rather than being described twice in two notations.

func (*Client) ScenePaths added in v0.7.0

func (c *Client) ScenePaths(ctx context.Context, id string) (ScenePaths, error)

ScenePaths returns the media URLs for one scene.

This is a call of its own rather than a field on Scene because SceneFields is shared by every scene query, and a field that turns out to be missing on an older server costs all of them their whole response. A caller that wants paths asks for paths.

func (*Client) ScrapeMultiScenes added in v0.9.0

func (c *Client) ScrapeMultiScenes(ctx context.Context, endpoint string, sceneIDs []string) ([][]ScrapedScene, error)

ScrapeMultiScenes asks a stash-box about many scenes at once, each matched on its files' fingerprints the way Client.ScrapeSceneByID matches one.

The result is parallel to sceneIDs: one entry per scene, in the order given, holding that scene's candidates. A scene the stash-box does not recognise gets an empty entry rather than being dropped, so the two slices can be walked together.

This is the call to reach for when identifying a whole library. Stash queries the stash-box once per scene either way, but a batch costs one HTTP round trip instead of one per scene, and Stash's own max_requests_per_minute paces the upstream calls — so the batch size is a question of how much work to lose if the request fails, not of politeness. Twenty or so is comfortable.

func (*Client) ScrapePerformers added in v0.7.0

func (c *Client) ScrapePerformers(ctx context.Context, endpoint, query string) ([]ScrapedPerformer, error)

ScrapePerformers searches a stash-box through the server and returns what it found.

query is matched against names, and a name search returns everything close to it — ten results for a common first name is normal, so a caller picking blindly will pick wrong. Passing a stash id instead returns the one performer it belongs to, which is the reliable way to use this when the id is already known.

The server does the scraping, using the API key configured for that stash-box. A stash-box with no key configured returns nothing rather than an error.

func (*Client) ScrapeSceneByID added in v0.7.0

func (c *Client) ScrapeSceneByID(ctx context.Context, endpoint, sceneID string) ([]ScrapedScene, error)

ScrapeSceneByID asks a stash-box about a scene Stash already has, matched on the file's fingerprints rather than on any text.

An empty result is the ordinary answer for a library the stash-box does not cover, not a failure.

func (*Client) ScrapeScenes added in v0.7.0

func (c *Client) ScrapeScenes(ctx context.Context, endpoint, query string) ([]ScrapedScene, error)

ScrapeScenes searches a stash-box for scenes through the server.

Passing a scene id Stash already knows — see Client.ScrapeSceneByID — matches on the file's fingerprints, which is exact. A text query matches on whatever the stash-box searches, so it returns near-misses too: check something about each result before believing it, because "one result" is not the same as "the right one".

func (*Client) ServerVersion added in v0.9.0

func (c *Client) ServerVersion(ctx context.Context) (ServerVersion, error)

ServerVersion returns the build the server is running, hash and build time included. Client.Version is the same call when only the version string is wanted.

func (*Client) SetFingerprints added in v0.9.0

func (c *Client) SetFingerprints(ctx context.Context, fileID string, fingerprints []Fingerprint) error

SetFingerprints replaces a file's fingerprints.

Replaces, not merges: a hash the file had and this call omits is dropped. Read the file first and append if you mean to add one.

func (*Client) SetPluginsEnabled added in v0.7.0

func (c *Client) SetPluginsEnabled(ctx context.Context, enabled map[string]bool) error

SetPluginsEnabled enables and disables plugins by id. Plugins not named are left as they are.

func (*Client) SetPrimaryFile added in v0.9.0

func (c *Client) SetPrimaryFile(ctx context.Context, sceneID, fileID string) error

SetPrimaryFile chooses which of a scene's files is the primary one — the file Stash streams, and the one whose resolution and codec the scene reports as its own.

This is not Client.AssignFile: that moves a file between scenes, while this reorders the files a scene already has. A scene left with several files after a merge is the usual reason to call it, picking the best of them before the rest are destroyed.

The file must already belong to the scene; Stash rejects the update otherwise.

func (*Client) SetSceneStashIDs added in v0.7.0

func (c *Client) SetSceneStashIDs(ctx context.Context, sceneID string, ids []StashID) error

SetSceneStashIDs replaces a scene's stash-box ids, and can clear them.

SceneUpdate cannot. Its fields are omitted when empty so that an unset one leaves the stored value alone, which is what makes partial updates safe — and it means an empty StashIDs slice is indistinguishable from "do not touch the stash ids". Removing the last one therefore needs a call that always sends the field.

Passing nil clears them.

func (*Client) SetStashBoxes added in v0.8.0

func (c *Client) SetStashBoxes(ctx context.Context, boxes []StashBoxConfig) error

SetStashBoxes replaces the configured stash-boxes with exactly this list.

It replaces; it does not add. Passing one box removes every other, along with its API key, and Stash asks nothing before doing so. Read the current list with Client.StashBoxConfigs, append to it, and send the result.

Passing an empty list removes them all, which is a legitimate thing to want and so is not refused.

func (*Client) StashBoxBatchTag added in v0.9.0

func (c *Client) StashBoxBatchTag(ctx context.Context, target BatchTagTarget, opts BatchTagOptions) (jobID string, err error)

StashBoxBatchTag starts Stash's own batch tagger and returns the id of the job doing it. It does not wait; follow it with Client.FindJob.

This is the server-side version of matching a library against a stash-box: Stash queries the box for each entity, and writes back the stash id plus whatever fields are not excluded. Doing the same thing client-side means one round trip per entity and no access to Stash's own matching, which is why this exists even though Client.ScrapePerformers can reach the same data.

What it writes is not reviewable in advance. ExcludeFields is the only control over that, so on a library whose own metadata is better than the box's, exclude everything except the stash id.

func (*Client) StashBoxConfigs added in v0.8.0

func (c *Client) StashBoxConfigs(ctx context.Context) ([]StashBoxConfig, error)

StashBoxConfigs returns the configured stash-boxes with their API keys.

Prefer Client.StashBoxes for anything that only needs to know which boxes exist. This one hands back credentials, and is here for the one job that needs them: rewriting the list without destroying the entries it keeps.

func (*Client) StashBoxes added in v0.7.0

func (c *Client) StashBoxes(ctx context.Context) ([]StashBox, error)

StashBoxes returns the stash-boxes the server is configured against, in the order Stash holds them. A scrape needs one of these endpoints, and an empty result means nothing can be scraped from a stash-box at all.

func (*Client) StopAllJobs added in v0.8.0

func (c *Client) StopAllJobs(ctx context.Context) error

StopAllJobs asks Stash to stop everything queued and running.

func (*Client) StopJob added in v0.8.0

func (c *Client) StopJob(ctx context.Context, jobID string) error

StopJob asks Stash to stop one running job. It returns without waiting: the job moves to STOPPING and reaches a terminal state in its own time, which Client.FindJob reports.

func (*Client) Studios added in v0.8.0

func (c *Client) Studios(ctx context.Context, page, perPage int) ([]Studio, int, error)

Studios returns one page of studios plus the total count, sorted by name.

func (*Client) SubmitFingerprints added in v0.8.0

func (c *Client) SubmitFingerprints(ctx context.Context, endpoint string, sceneIDs ...string) (ok bool, err error)

SubmitFingerprints sends the scenes' file fingerprints to a stash-box, against whatever entries they are already linked to there.

Only linked scenes contribute: the stash id is what says which upstream scene the hashes belong to, so a scene with none is silently nothing to submit. ok is what the server reports.

func (*Client) SubmitPerformerDraft added in v0.8.0

func (c *Client) SubmitPerformerDraft(ctx context.Context, performerID, endpoint string) (draftID string, err error)

SubmitPerformerDraft sends a performer to a stash-box as a draft.

func (*Client) SubmitSceneDraft added in v0.8.0

func (c *Client) SubmitSceneDraft(ctx context.Context, sceneID, endpoint string) (draftID string, err error)

SubmitSceneDraft sends a scene to a stash-box as a draft and returns the draft's id there.

A draft is not an edit. It lands in the stash-box as a proposal that someone — usually the submitter — then turns into a create or a modify through the stash-box's own interface. Nothing changes upstream until they do.

func (*Client) Supports

func (c *Client) Supports(ctx context.Context, field string) (bool, error)

Supports reports whether the server's schema has a named field on Scene.

This exists because GraphQL fails the *whole* query when it is asked for a field the schema lacks — one unknown field costs the entire response, not just that field. Probing once is cheaper than discovering it mid-import against an older server.

if ok, _ := c.Supports(ctx, "captions"); ok { ... }

The schema is fetched once per client and cached.

func (*Client) SystemStatus added in v0.9.0

func (c *Client) SystemStatus(ctx context.Context) (SystemStatus, error)

SystemStatus asks the server what state it is in.

Worth a call before a long unattended run: Client.Ping succeeds against a server that is showing its setup wizard or refusing to open an unmigrated database, because answering "SETUP" is itself a successful answer. This is how the two are told apart.

func (*Client) Tags added in v0.8.0

func (c *Client) Tags(ctx context.Context, page, perPage int) ([]Tag, int, error)

Tags returns one page of tags plus the total count, sorted by name.

func (*Client) UninstallPackages added in v0.7.0

func (c *Client) UninstallPackages(ctx context.Context, t PackageType, specs ...PackageSpec) (jobID string, err error)

UninstallPackages removes packages and returns the id of the job doing it.

It deletes the package's directory on the server. Anything a plugin wrote inside its own directory goes with it.

func (*Client) UpdatePackages added in v0.7.0

func (c *Client) UpdatePackages(ctx context.Context, t PackageType, specs ...PackageSpec) (jobID string, err error)

UpdatePackages updates packages, or every installed package of that type when no spec is given, and returns the id of the job doing it.

func (*Client) UpdatePerformer added in v0.8.0

func (c *Client) UpdatePerformer(ctx context.Context, update PerformerUpdate) error

UpdatePerformer writes the fields that are set and leaves the rest alone.

func (*Client) UpdateScene

func (c *Client) UpdateScene(ctx context.Context, update SceneUpdate) error

UpdateScene writes the fields set on update, leaving the rest untouched.

func (*Client) UpdateStudio added in v0.8.0

func (c *Client) UpdateStudio(ctx context.Context, in StudioInput) error

UpdateStudio writes the fields that are set and leaves the rest alone.

func (*Client) UpdateTag added in v0.8.0

func (c *Client) UpdateTag(ctx context.Context, in TagInput) error

UpdateTag writes the fields that are set and leaves the rest alone.

func (*Client) ValidateStashBox added in v0.8.0

func (c *Client) ValidateStashBox(ctx context.Context, endpoint, apiKey string) (valid bool, status string, err error)

ValidateStashBox asks the server whether it can reach a stash-box with the given credential, and returns what it says.

The request is made by the *server*, so this tests the server's route to the box rather than this program's — which is the whole point when the two are on different machines.

func (*Client) Version

func (c *Client) Version(ctx context.Context) (string, error)

Version reports the server's version string.

type Conflict added in v0.10.0

type Conflict struct {
	Endpoint string
	Kept     string
	Dropped  string
}

Conflict is a stash ID Union dropped because the scene already had a different one for the same endpoint. A scene holds one ID per stash-box, so two IDs for one endpoint means two different remote scenes claim the same local one — which is for a person to settle, not this function.

type DLNAIP added in v0.9.0

type DLNAIP struct {
	Address string `json:"ipAddress"`
	// Until is when the grant lapses, or nil for one made without a
	// duration, which lasts until the server restarts.
	Until *string `json:"until"`
}

DLNAIP is one temporarily allowed address.

type DLNAStatus added in v0.9.0

type DLNAStatus struct {
	Running bool `json:"running"`
	// Until is when the current state ends, and reads in whichever
	// direction Running points: while running it is when the service will
	// stop, while stopped it is when it will start. nil means the state has
	// no end — which is the usual case, since only a timed
	// [Client.EnableDLNA] or [Client.DisableDLNA] sets one.
	Until *string `json:"until"`
	// RecentIPAddresses are the addresses that have asked the service for
	// something lately, whether or not it answered. This is where the
	// address for [Client.AllowDLNAIP] comes from: a device is identified
	// by having just tried.
	RecentIPAddresses []string `json:"recentIPAddresses"`
	// AllowedIPAddresses are the temporary grants — the permanent list
	// lives in the configuration, under dlnaInterfaces and dlnaWhitelist.
	AllowedIPAddresses []DLNAIP `json:"allowedIPAddresses"`
}

DLNAStatus is what the server's DLNA service is currently doing.

type DeleteOptions added in v0.9.0

type DeleteOptions struct {
	// DeleteFile removes the video from disk. There is no undo, and Stash
	// does not move it to a wastebasket.
	DeleteFile bool
	// DeleteGenerated removes the sprites, previews and covers Stash made
	// for it. Those can be regenerated from the video, so this is only
	// destructive alongside DeleteFile.
	DeleteGenerated bool
	// DestroyFileEntry removes the file's row as well as the scene's. Without
	// it Stash remembers the file and will not re-add it on the next scan,
	// which is what you want when deleting a duplicate whose video is still
	// on disk under another scene — and what you do not want when the file
	// is gone and you may restore it later.
	DestroyFileEntry bool
}

DeleteOptions says how far a scene deletion reaches.

Every field defaults to false, which removes only the scene record and leaves the video where it is. That is the recoverable choice: a scan finds the file again. The others are not.

type FieldPolicy added in v0.10.0

type FieldPolicy int

FieldPolicy says how Union combines one scene field.

The zero value is Keep, so a field left out of a UnionPolicy is never written.

const (
	// Keep never touches the field.
	Keep FieldPolicy = iota

	// FillEmpty keeps the destination's value unless it is empty, and then
	// takes the first source that has one. This is what a merge wants: the
	// scene being kept is right, the ones being folded away fill its gaps.
	FillEmpty

	// PreferSource takes the first source that has a value, over whatever the
	// destination has. An empty source never clears the destination.
	PreferSource

	// Combine takes everything: list fields get every element from every
	// scene, the destination's first, deduplicated; Rating100 takes the
	// highest; Organized is true if any scene is. On a plain string field it
	// behaves as FillEmpty, there being nothing to combine.
	Combine
)

type File

type File struct {
	ID           string        `json:"id"`
	Basename     string        `json:"basename"`
	Path         string        `json:"path"`
	Size         int64         `json:"size"`
	ModTime      string        `json:"mod_time"`
	Format       string        `json:"format"`
	Width        int           `json:"width"`
	Height       int           `json:"height"`
	Duration     float64       `json:"duration"`
	VideoCodec   string        `json:"video_codec"`
	AudioCodec   string        `json:"audio_codec"`
	FrameRate    float64       `json:"frame_rate"`
	BitRate      int64         `json:"bit_rate"`
	Fingerprints []Fingerprint `json:"fingerprints"`
}

File is one video file backing a scene. A scene has several when Stash has attached re-detected duplicates to it.

func (*File) Fingerprint added in v0.4.0

func (f *File) Fingerprint(kind string) (string, bool)

Fingerprint returns the value of the named hash ("oshash", "phash", "md5").

func (*File) Tier added in v0.10.0

func (f *File) Tier() Tier

Tier classifies the file by its own width and height — see TierOf.

type FilterMode added in v0.7.0

type FilterMode string

FilterMode is which list a saved filter belongs to.

const (
	FilterScenes       FilterMode = "SCENES"
	FilterPerformers   FilterMode = "PERFORMERS"
	FilterStudios      FilterMode = "STUDIOS"
	FilterGalleries    FilterMode = "GALLERIES"
	FilterSceneMarkers FilterMode = "SCENE_MARKERS"
	FilterGroups       FilterMode = "GROUPS"
	FilterTags         FilterMode = "TAGS"
	FilterImages       FilterMode = "IMAGES"
)

FilterMode values, one per list a saved filter can belong to.

type FindFilter added in v0.7.0

type FindFilter struct {
	// Query is the free-text box.
	Query string `json:"q,omitempty"`
	// Sort is a field name ("date", "path", "title"), and Direction is
	// "ASC" or "DESC".
	Sort      string `json:"sort,omitempty"`
	Direction string `json:"direction,omitempty"`
	PerPage   int    `json:"per_page,omitempty"`
}

FindFilter is the sorting and paging half of a filter — what the UI puts above the list rather than in the sidebar.

type Fingerprint added in v0.4.0

type Fingerprint struct {
	Type  string `json:"type"`
	Value string `json:"value"`
}

Fingerprint is one content hash of a file.

type Gallery struct {
	ID    string `json:"id"`
	Title string `json:"title"`
}

Gallery attached to a scene.

type GenerateOptions added in v0.8.0

type GenerateOptions struct {
	// Covers, Sprites and Phashes are the three that other work depends on.
	// A scene with no sprite cannot be read for a title card; a scene with
	// no phash cannot be matched against a stash-box.
	Covers  bool
	Sprites bool
	Phashes bool

	Previews      bool
	ImagePreviews bool
	Markers       bool
	Transcodes    bool
	// ForceTranscodes re-encodes even where a transcode already exists.
	ForceTranscodes           bool
	InteractiveHeatmapsSpeeds bool
	ImagePhashes              bool
	ImageThumbnails           bool
	ClipPreviews              bool

	// SceneIDs restricts the job to these scenes; Paths to these library
	// paths. Both empty means the whole library.
	SceneIDs []string
	Paths    []string

	// Overwrite regenerates what is already there. Without it Stash skips
	// anything it has, which is what makes a second run cheap.
	Overwrite bool
}

GenerateOptions selects what Client.MetadataGenerate produces.

Every flag defaults to off, for the reason ScanOptions gives: generating across a library is hours of work and gigabytes of output, and a library call that quietly started doing it would be an expensive surprise. Ask for what you want.

type GraphQLError added in v0.2.0

type GraphQLError struct {
	Message    string         `json:"message"`
	Path       []any          `json:"path,omitempty"`
	Extensions map[string]any `json:"extensions,omitempty"`
}

GraphQLError is one entry from a GraphQL `errors` array.

Path and Extensions are kept because they are where a server says something useful: Path names the field that failed, and Stash puts its own error codes in Extensions. Flattening these to a string loses the machine-readable part of the only structured error the API offers.

type Group added in v0.9.0

type Group struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

Group is a Stash group, as a scene reports its membership of one. Only the fields the scene selection asks for.

type HTTPError

type HTTPError struct {
	StatusCode int
	Status     string
	Body       string
}

HTTPError is a non-2xx response from the server.

Body carries what the server actually said, truncated. Stash returns useful text on an auth failure or a bad endpoint, and a bare status code sends the reader to the server logs for something that was already in the response.

func (*HTTPError) Error

func (e *HTTPError) Error() string

type IdentifyOptions added in v0.8.0

type IdentifyOptions struct {
	// Sources are the stash-box endpoints or scraper ids to try, in order.
	// Empty uses whatever the server has configured as its defaults.
	Sources []string
	// SceneIDs restricts the job to these scenes; Paths to these library
	// paths. Both empty means everything.
	SceneIDs []string
	Paths    []string
}

IdentifyOptions configures Client.MetadataIdentify, Stash's own matching of scenes against a stash-box.

type Job added in v0.6.0

type Job struct {
	ID          string    `json:"id"`
	Status      JobStatus `json:"status"`
	Description string    `json:"description"`
	// Progress is 0..1 while running, and negative when Stash has not
	// worked out a total yet — not 0, which would render as "just started"
	// rather than "unknown".
	Progress  *float64 `json:"progress"`
	Error     *string  `json:"error"`
	AddTime   string   `json:"addTime"`
	StartTime *string  `json:"startTime"`
	EndTime   *string  `json:"endTime"`
}

Job is one entry in Stash's task queue.

type JobStatus added in v0.6.0

type JobStatus string

JobStatus is where a job has got to. Stash's own vocabulary, not a normalisation of it.

const (
	JobReady     JobStatus = "READY"
	JobRunning   JobStatus = "RUNNING"
	JobFinished  JobStatus = "FINISHED"
	JobStopping  JobStatus = "STOPPING"
	JobCancelled JobStatus = "CANCELLED"
	JobFailed    JobStatus = "FAILED"
)

JobStatus values, Stash's own vocabulary for a job's progress.

func (JobStatus) Done added in v0.6.0

func (s JobStatus) Done() bool

Done reports whether the job has stopped, however it stopped. Useful as a poll condition: the three terminal states are easy to enumerate incompletely by hand, and treating CANCELLED as still-running is a hang.

type LibraryStats added in v0.9.0

type LibraryStats struct {
	SceneCount int `json:"scene_count"`
	// ScenesSize is the total size of every scene's files, in bytes.
	ScenesSize float64 `json:"scenes_size"`
	// ScenesDuration is the total runtime of every scene, in seconds.
	ScenesDuration float64 `json:"scenes_duration"`
	ImageCount     int     `json:"image_count"`
	ImagesSize     float64 `json:"images_size"`
	GalleryCount   int     `json:"gallery_count"`
	PerformerCount int     `json:"performer_count"`
	StudioCount    int     `json:"studio_count"`
	GroupCount     int     `json:"group_count"`
	TagCount       int     `json:"tag_count"`

	// TotalPlayDuration is how long scenes have been watched for in total,
	// in seconds, and ScenesPlayed how many have been watched at all.
	TotalOCount       int     `json:"total_o_count"`
	TotalPlayCount    int     `json:"total_play_count"`
	TotalPlayDuration float64 `json:"total_play_duration"`
	ScenesPlayed      int     `json:"scenes_played"`
}

LibraryStats is what the server counts about the library as a whole. Every field is the server's own tally, computed from the database rather than by walking the scenes.

The counts cover what Stash has indexed, which is not the same as what is on disk: a file the last scan did not reach is not here.

type LogEntry added in v0.9.0

type LogEntry struct {
	Time string `json:"time"`
	// Level is Stash's own word for it: "Trace", "Debug", "Info",
	// "Progress", "Warning" or "Error".
	Level   string `json:"level"`
	Message string `json:"message"`
}

LogEntry is one line from the server's log.

type MergeOptions added in v0.9.0

type MergeOptions struct {
	// PlayHistory folds the sources' play timestamps into the
	// destination's.
	PlayHistory bool
	// OHistory folds the sources' o timestamps into the destination's.
	OHistory bool
}

MergeOptions says what a merge carries across besides the files.

Both default to false, which is Stash's own default and discards the sources' watch history. A deduplicating tool usually wants both: the copy being kept is the same content, so the times it was watched belong to it.

type MoveTarget added in v0.9.0

type MoveTarget struct {
	FolderID string
	Folder   string
	Basename string
}

MoveTarget says where Client.MoveFiles should put them: a folder Stash already knows by id, or a path, and optionally a new name.

Renaming one file at a time is what Basename is for; moving several with a Basename set would give them all the same name, so it is refused.

type Option

type Option func(*Client)

Option configures a Client.

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey authenticates as the given API key. Omit it for a Stash instance with authentication disabled.

func WithCaptions deprecated added in v0.6.0

func WithCaptions() Option

WithCaptions once asked the scene queries to include Scene.Captions, which they now always do.

It survives as a no-op so that callers written against the option still compile. There is nothing to opt into: the supported server has the field, so it is in SceneFields with everything else.

Deprecated: captions are always selected. Remove the option.

func WithCookie added in v0.3.0

func WithCookie(cookie *http.Cookie) Option

WithCookie authenticates with a session cookie.

Stash hands its plugin processes a session cookie in server_connection, and a plugin has no API key unless the operator configured one. An API key takes precedence when both are set: session cookies expire mid-run, which on a long task fails partway through rather than at startup.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient supplies the HTTP client used for every request, so retry, backoff, proxying and timeouts stay under the caller's control.

The default is a plain client with a 30s timeout and no retry.

func WithMaxResponseBytes

func WithMaxResponseBytes(n int64) Option

WithMaxResponseBytes overrides DefaultMaxResponseBytes.

type Package added in v0.7.0

type Package struct {
	ID        string `json:"package_id"`
	Name      string `json:"name"`
	Version   string `json:"version"`
	Date      string `json:"date"`
	SourceURL string `json:"sourceURL"`
	// Requires names the packages this one needs. Stash does not install
	// them for you — an install that leaves a requirement unmet succeeds and
	// the plugin then fails at runtime.
	Requires []Package `json:"requires"`
	// Metadata is the index's free-form block, which in practice carries a
	// "description". Free-form is why it is not modelled.
	Metadata map[string]any `json:"metadata"`
}

Package is one entry in a package index, or one thing already installed.

func (Package) Description added in v0.7.0

func (p Package) Description() string

Description returns the package's description, or "" when the index gives none.

func (Package) Spec added in v0.7.0

func (p Package) Spec() PackageSpec

Spec returns the PackageSpec that names this package for an install or uninstall.

type PackageSource added in v0.7.0

type PackageSource struct {
	Name string `json:"name"`
	// URL of the index. It is also the sourceURL a [PackageSpec] has to
	// carry: Stash identifies a package by id *and* source, because two
	// indexes may both offer an id.
	URL       string `json:"url"`
	LocalPath string `json:"local_path"`
}

PackageSource is one index a package manager installs from, as configured in the server's settings.

type PackageSpec added in v0.7.0

type PackageSpec struct {
	ID        string `json:"id"`
	SourceURL string `json:"sourceURL"`
}

PackageSpec names one package to install or uninstall. Both fields are required: an id alone is ambiguous across sources.

type PackageType added in v0.7.0

type PackageType string

PackageType selects which of Stash's two package managers a call talks to.

const (
	PackagePlugin  PackageType = "Plugin"
	PackageScraper PackageType = "Scraper"
)

PackageType values, one per package manager Stash exposes.

type ParsedFilename added in v0.10.0

type ParsedFilename struct {
	Date       string   // "2024-12-15" — normalized to YYYY-MM-DD
	Title      string   // "A Long Scene Title With Words"
	Performers []string // ["Some Performer"]
}

ParsedFilename is what ParseFilename extracted from a structured basename.

func ParseFilename added in v0.10.0

func ParseFilename(basename string) (ParsedFilename, bool)

ParseFilename extracts a date, title and performer names from a basename following the convention YYYY-MM-DD_Performers-Title_Resolution.ext. It reports false when the name doesn't match that convention at all — most filenames simply aren't structured this way, so that is not an error.

Stash has its own scan-time filename parser; this one exists alongside it because it additionally handles multiple underscore-separated performers and a dashed or dotted title the way one library is actually named.

type Performer

type Performer struct {
	ID             string `json:"id"`
	Name           string `json:"name"`
	Disambiguation string `json:"disambiguation"`
	Gender         string `json:"gender"`
	Birthdate      string `json:"birthdate"`
	DeathDate      string `json:"death_date"`
	Country        string `json:"country"`
	Ethnicity      string `json:"ethnicity"`
	EyeColor       string `json:"eye_color"`
	HairColor      string `json:"hair_color"`
	HeightCM       int    `json:"height_cm"`
	Weight         int    `json:"weight"`
	Measurements   string `json:"measurements"`
	FakeTits       string `json:"fake_tits"`
	// CareerStart and CareerEnd are strings on the wire, not numbers: Stash
	// stores them as years but declares them String, and decoding them as
	// ints fails every performer query.
	CareerStart string    `json:"career_start"`
	CareerEnd   string    `json:"career_end"`
	Tattoos     string    `json:"tattoos"`
	Piercings   string    `json:"piercings"`
	Aliases     []string  `json:"alias_list"`
	URLs        []string  `json:"urls"`
	Details     string    `json:"details"`
	Favorite    bool      `json:"favorite"`
	Rating100   *int      `json:"rating100"`
	ImagePath   string    `json:"image_path"`
	SceneCount  int       `json:"scene_count"`
	Tags        []Tag     `json:"tags"`
	StashIDs    []StashID `json:"stash_ids"`
}

Performer attached to a scene. Performer as returned by the performer queries.

A performer reached through a scene carries only ID and Name: the shared scene selection asks for nothing more, because a page of scenes would otherwise drag a full performer record along for every credit. Client.FindPerformerByID and Client.FindPerformers fill the rest in.

type PerformerFilter added in v0.8.0

type PerformerFilter struct {
	// NameContains matches anywhere in the name, case-insensitively.
	NameContains string
	// Gender is Stash's own vocabulary — "FEMALE", "MALE",
	// "TRANSGENDER_FEMALE" and the rest.
	Gender string
	// Favorite selects favourites (true) or everything else (false).
	Favorite *bool
	// HasStashID selects performers that do (true) or do not (false) carry
	// stash-box metadata.
	HasStashID *bool
	// HasScenes selects performers with at least one scene (true) or none
	// (false). Performers with none are usually leftovers.
	HasScenes *bool
}

PerformerFilter selects performers. An unset field does not filter.

type PerformerInput added in v0.7.0

type PerformerInput struct {
	Name           string
	Disambiguation string
	Gender         string
	Birthdate      string
	DeathDate      string
	Country        string
	Ethnicity      string
	EyeColor       string
	HairColor      string
	HeightCM       int
	Weight         int
	Measurements   string
	FakeTits       string
	CareerLength   string
	Tattoos        string
	Piercings      string
	Aliases        []string
	URLs           []string
	Details        string
	// Image is either a URL or a data: URI. Given a URL, Stash fetches it
	// itself — which means the fetch happens from the server, and a URL only
	// this machine can reach will not work.
	Image string
	// StashIDs ties the performer to its entry in a stash-box. Worth setting
	// whenever it is known: it is the only stable identity a performer has,
	// and it is what stops the same person being created twice under two
	// spellings.
	StashIDs []StashID
}

PerformerInput is a performer to create, with everything Stash will accept about one.

Every field but Name is optional and omitted when empty, so a caller that knows only a name sends the same request Client.CreatePerformer does.

type PerformerUpdate added in v0.8.0

type PerformerUpdate struct {
	ID string

	Name           *string
	Disambiguation *string
	Gender         *string
	Birthdate      *string
	DeathDate      *string
	Country        *string
	Ethnicity      *string
	EyeColor       *string
	HairColor      *string
	HeightCM       *int
	Weight         *int
	Measurements   *string
	FakeTits       *string
	CareerLength   *string
	Tattoos        *string
	Piercings      *string
	Details        *string
	Favorite       *bool
	Rating100      *int

	// Aliases, URLs, TagIDs and StashIDs replace what is stored rather than
	// adding to it. Read the performer first and send the union if adding is
	// what you meant.
	Aliases  []string
	URLs     []string
	TagIDs   []string
	StashIDs []StashID

	// Image is a URL or a data: URI. Given a URL, the server fetches it.
	Image *string
}

PerformerUpdate is the payload for Client.UpdatePerformer.

Only the fields you set are sent, so an unset one leaves the stored value alone — the same shape as SceneUpdate, and with the same limitation: it cannot clear a field, because empty and absent look identical on the wire. Client.ClearPerformerFields is the way to empty one.

type Plugin added in v0.7.0

type Plugin struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	URL         string `json:"url"`
	Version     string `json:"version"`
	Enabled     bool   `json:"enabled"`
}

Plugin is one plugin the server has loaded.

type Progress

type Progress func(fetched, total int)

Progress reports pagination advancing. total is the count reported by the first page.

type SavedFilter added in v0.7.0

type SavedFilter struct {
	ID   string     `json:"id"`
	Mode FilterMode `json:"mode"`
	Name string     `json:"name"`
	// FindFilter is the sort and page size.
	FindFilter *FindFilter `json:"find_filter"`
	// ObjectFilter is the criteria, in Stash's own notation. Build one for
	// scenes with [Client.SceneFilterCriteria] rather than by hand.
	ObjectFilter map[string]any `json:"object_filter"`
	// UIOptions is what the UI remembers about how to display the list —
	// card size, zoom, which columns. Carried so an update does not discard
	// it.
	UIOptions map[string]any `json:"ui_options"`
}

SavedFilter is one of the filters that appear in Stash's sidebar.

type ScanOptions added in v0.6.0

type ScanOptions struct {
	// Paths restricts the scan to these library paths. Empty scans every
	// configured library path, which on a large library is an hours-long
	// job — pass the directory you actually changed.
	Paths []string
	// Rescan re-reads files Stash has already indexed rather than only
	// picking up new ones.
	Rescan bool
	// GeneratePhashes computes perceptual hashes for newly scanned files.
	// Worth setting for anything that matches on content rather than
	// filename: without a phash, only byte-identical files can be matched.
	GeneratePhashes    bool
	GenerateCovers     bool
	GeneratePreviews   bool
	GenerateSprites    bool
	GenerateThumbnails bool
}

ScanOptions selects what Client.MetadataScan scans and what it generates while it does.

Every generate flag defaults to off. That is not Stash's own default — the UI remembers whatever was ticked last — but a library call that quietly started generating covers, previews and sprites across a library would be a very expensive surprise. Ask for what you want.

type Scene

type Scene struct {
	ID         string       `json:"id"`
	Title      string       `json:"title"`
	Code       string       `json:"code"`
	Date       string       `json:"date"`
	Details    string       `json:"details"`
	Director   string       `json:"director"`
	URLs       []string     `json:"urls"`
	Rating100  *int         `json:"rating100"`
	Organized  bool         `json:"organized"`
	OCounter   int          `json:"o_counter"`
	Files      []File       `json:"files"`
	Tags       []Tag        `json:"tags"`
	Performers []Performer  `json:"performers"`
	Studio     *Studio      `json:"studio"`
	StashIDs   []StashID    `json:"stash_ids"`
	Galleries  []Gallery    `json:"galleries"`
	Captions   []Caption    `json:"captions"`
	Groups     []SceneGroup `json:"groups"`

	// PlayDuration is how long this scene has been watched for in total,
	// in seconds, and ResumeTime where playback left off.
	PlayCount    int     `json:"play_count"`
	PlayDuration float64 `json:"play_duration"`
	LastPlayedAt *string `json:"last_played_at"`
	ResumeTime   float64 `json:"resume_time"`

	// CreatedAt and UpdatedAt are the record's timestamps, as the server
	// formats them; pass UpdatedAt back as [SceneFilter.UpdatedAfter] to
	// mirror incrementally.
	CreatedAt string `json:"created_at"`
	UpdatedAt string `json:"updated_at"`
}

Scene is a scene as returned by findScene / findScenes.

func (*Scene) HasStashID added in v0.5.0

func (s *Scene) HasStashID() bool

HasStashID reports whether the scene carries stash-box metadata.

func (*Scene) PrimaryFile added in v0.4.0

func (s *Scene) PrimaryFile() *File

PrimaryFile returns the file Stash treats as canonical, or nil when the scene has none.

type SceneFilter

type SceneFilter struct {
	Organized     *bool  `json:"organized,omitempty"`
	PerformerName string `json:"-"`
	StudioName    string `json:"-"`

	// HasStashID selects scenes that do (true) or do not (false) carry
	// stash-box metadata. Nil means "either".
	HasStashID *bool `json:"-"`

	// PathContains matches scenes whose file path contains this substring.
	PathContains string `json:"-"`

	// HasDate selects scenes that do (true) or do not (false) carry a date.
	// Nil means "either".
	HasDate *bool `json:"-"`

	// MultiFile selects scenes with more than one file attached (true) or
	// with exactly one (false). Nil means "either".
	//
	// Stash attaches a re-detected file to the scene that already has its
	// hash rather than creating a second scene, so true is how the
	// duplicates that never became separate scenes are found.
	MultiFile *bool `json:"-"`

	// TagNames selects scenes carrying every one of these tags, and
	// ExcludeTagNames scenes carrying none of them. Both are resolved to
	// ids first, and a name no tag has is an error rather than an empty
	// result — the same reason [ErrPerformerNotFound] exists.
	TagNames        []string `json:"-"`
	ExcludeTagNames []string `json:"-"`

	// DateBefore and DateAfter bound the date, exclusive at both ends, in
	// Stash's own "2006-01-02" notation. A scene with no date matches
	// neither: an absent date is not an early one.
	DateBefore string `json:"-"`
	DateAfter  string `json:"-"`

	// UpdatedAfter selects scenes whose record changed after this
	// timestamp, exclusive, in RFC 3339 or Stash's "2006-01-02 15:04:05".
	// Remember the newest updated_at seen and pass it next time to mirror
	// a library incrementally.
	UpdatedAfter string `json:"-"`
}

SceneFilter narrows FindScenes.

Fields tagged `json:"-"` are resolved client-side into the server's filter shape — PerformerName and StudioName each cost an extra lookup to turn a name into an ID.

type SceneGroup added in v0.9.0

type SceneGroup struct {
	Group      Group `json:"group"`
	SceneIndex *int  `json:"scene_index"`
}

SceneGroup is a scene's membership of a group — what Stash called a movie before 0.28. SceneIndex is the scene's place in it, and is nil for a group that does not order its scenes.

type ScenePaths added in v0.7.0

type ScenePaths struct {
	// Screenshot is the scene's cover, at the video's own resolution.
	Screenshot string `json:"screenshot"`
	// Preview is a short video, Webp a short animation.
	Preview string `json:"preview"`
	Webp    string `json:"webp"`
	// Stream serves the video itself, and honours range requests — which is
	// what lets a frame be pulled from the middle of a file without
	// downloading it.
	Stream string `json:"stream"`
	Sprite string `json:"sprite"`
	VTT    string `json:"vtt"`
}

ScenePaths are the URLs of a scene's generated media — the things GraphQL will not return as data. Fetch one with Client.Fetch.

An empty field means Stash has not generated that piece for this scene. Sprite and VTT go together: the sheet is a grid of frames and the WebVTT is what says which frame is which moment.

type SceneUpdate

type SceneUpdate struct {
	ID           string    `json:"id"`
	Title        *string   `json:"title,omitempty"`
	Code         *string   `json:"code,omitempty"`
	Details      *string   `json:"details,omitempty"`
	Director     *string   `json:"director,omitempty"`
	Date         *string   `json:"date,omitempty"`
	Rating100    *int      `json:"rating100,omitempty"`
	URLs         []string  `json:"urls,omitempty"`
	TagIDs       []string  `json:"tag_ids,omitempty"`
	PerformerIDs []string  `json:"performer_ids,omitempty"`
	StudioID     *string   `json:"studio_id,omitempty"`
	GalleryIDs   []string  `json:"gallery_ids,omitempty"`
	Organized    *bool     `json:"organized,omitempty"`
	StashIDs     []StashID `json:"stash_ids,omitempty"`

	// PrimaryFileID picks which of the scene's files it streams from, and
	// which one's resolution and codec it reports as its own. The file must
	// already belong to the scene. [Client.SetPrimaryFile] is this field on
	// its own.
	PrimaryFileID *string `json:"primary_file_id,omitempty"`

	// CoverImage is a data URI ("data:image/jpeg;base64,…").
	//
	// This package deliberately does not fetch it for you: downloading an
	// arbitrary scraped URL needs SSRF validation, a size cap and an expiry
	// policy, all of which belong to the calling program rather than to a
	// Stash client.
	CoverImage *string `json:"cover_image,omitempty"`
}

SceneUpdate is the payload for a scene update.

Pointer and slice fields are omitted when nil, so only what you set is written. That is what makes a partial metadata push non-destructive — an unset Title leaves the existing title alone rather than clearing it.

type ScrapedPerformer added in v0.7.0

type ScrapedPerformer struct {
	Name           string   `json:"name"`
	Disambiguation string   `json:"disambiguation"`
	Gender         string   `json:"gender"`
	Birthdate      string   `json:"birthdate"`
	DeathDate      string   `json:"death_date"`
	Country        string   `json:"country"`
	Ethnicity      string   `json:"ethnicity"`
	EyeColor       string   `json:"eye_color"`
	HairColor      string   `json:"hair_color"`
	Height         string   `json:"height"`
	Weight         string   `json:"weight"`
	Measurements   string   `json:"measurements"`
	FakeTits       string   `json:"fake_tits"`
	CareerLength   string   `json:"career_length"`
	Tattoos        string   `json:"tattoos"`
	Piercings      string   `json:"piercings"`
	Details        string   `json:"details"`
	URLs           []string `json:"urls"`
	// Aliases is one comma-separated string, not a list. Stash's own input
	// wants a list, which is half of what Input is for.
	Aliases string `json:"aliases"`
	// Images are URLs, best first.
	Images []string `json:"images"`
	// RemoteSiteID is the performer's id at the source. For a stash-box that
	// is the stash id, which is what makes the result worth keeping.
	RemoteSiteID string `json:"remote_site_id"`
	// StoredID is set when Stash already has this performer, which saves
	// creating a second one under a name that differs by punctuation.
	StoredID string `json:"stored_id"`
}

ScrapedPerformer is a performer as a scraper describes one. Its fields are strings because that is what scrapers return, including the numeric ones — ScrapedPerformer.Input does the converting.

func (ScrapedPerformer) Input added in v0.7.0

func (p ScrapedPerformer) Input(endpoint string) PerformerInput

Input converts a scraped performer into something Client.CreatePerformerFrom can take, with endpoint naming the stash-box it came from so the stash id is recorded against the right one.

The conversions are the fiddly part and the reason this exists: heights and weights arrive as strings and sometimes with a unit attached, aliases arrive comma-separated where the input wants a list, and the first image is the one to keep.

type ScrapedScene added in v0.7.0

type ScrapedScene struct {
	Title    string   `json:"title"`
	Code     string   `json:"code"`
	Date     string   `json:"date"`
	Details  string   `json:"details"`
	Director string   `json:"director"`
	URLs     []string `json:"urls"`
	// Image is a URL to the scene's cover.
	Image string `json:"image"`
	// Duration is in seconds, and is the check worth making before
	// believing a match: two scenes with the same code and wildly different
	// lengths are not the same scene.
	Duration int `json:"duration"`
	// RemoteSiteID is the scene's stash id.
	RemoteSiteID string             `json:"remote_site_id"`
	Studio       *ScrapedStudio     `json:"studio"`
	Performers   []ScrapedPerformer `json:"performers"`
	Tags         []ScrapedTag       `json:"tags"`
}

ScrapedScene is a scene as a stash-box describes one.

type ScrapedStudio added in v0.7.0

type ScrapedStudio struct {
	Name         string `json:"name"`
	StoredID     string `json:"stored_id"`
	RemoteSiteID string `json:"remote_site_id"`
	URL          string `json:"url"`
}

ScrapedStudio is a studio as a scraper describes one. StoredID is set when Stash already has it, which saves looking it up by a name that may differ.

type ScrapedTag added in v0.7.0

type ScrapedTag struct {
	Name     string `json:"name"`
	StoredID string `json:"stored_id"`
}

ScrapedTag is a tag as a scraper describes one.

type ScreenshotMigration added in v0.9.0

type ScreenshotMigration struct {
	// DeleteFiles removes each screenshot file once it has been read into
	// the database as a blob.
	DeleteFiles bool
	// OverwriteExisting replaces a cover the scene already has. Off, a
	// scene with a cover keeps it and the file on disk is ignored.
	OverwriteExisting bool
}

ScreenshotMigration configures Client.MigrateSceneScreenshots.

type ServerVersion added in v0.9.0

type ServerVersion struct {
	// Version is the release tag ("v0.31.1"), and is empty on a binary
	// built from source outside a release — where Hash is what identifies
	// it.
	Version   string `json:"version"`
	Hash      string `json:"hash"`
	BuildTime string `json:"build_time"`
}

ServerVersion is the running build.

type StashBox added in v0.7.0

type StashBox struct {
	Endpoint string `json:"endpoint"`
	Name     string `json:"name"`
}

StashBox is one stash-box the server is configured against — stashdb.org and its siblings, the shared metadata databases Stash matches against.

The API key is deliberately not carried here. It is the server's credential for a third party, and a library handing it back invites it into a log line.

type StashBoxConfig added in v0.8.0

type StashBoxConfig struct {
	Name     string `json:"name"`
	Endpoint string `json:"endpoint"`
	APIKey   string `json:"api_key"`
	// MaxRequestsPerMinute throttles the server's calls to this box. Zero
	// means the server's default rather than "no requests".
	MaxRequestsPerMinute int `json:"max_requests_per_minute"`
}

StashBoxConfig is a stash-box as it is configured, credential included.

Separate from StashBox on purpose: that type is what a caller reads and deliberately has no API key, because it is the server's credential for a third party. This one exists because configuring a stash-box means sending the key, and rewriting the list means sending back the keys of the entries being kept.

type StashID

type StashID struct {
	Endpoint string `json:"endpoint"`
	ID       string `json:"stash_id"`
}

StashID links a scene to its entry in an external stash-box instance. Endpoint is the stash-box GraphQL URL; ID is the remote UUID.

type Studio

type Studio struct {
	ID           string    `json:"id"`
	Name         string    `json:"name"`
	URLs         []string  `json:"urls"`
	Details      string    `json:"details"`
	Aliases      []string  `json:"aliases"`
	Rating100    *int      `json:"rating100"`
	Favorite     bool      `json:"favorite"`
	ImagePath    string    `json:"image_path"`
	SceneCount   int       `json:"scene_count"`
	StashIDs     []StashID `json:"stash_ids"`
	ParentStudio *Studio   `json:"parent_studio"`
}

Studio a scene belongs to, and as the studio queries return one.

A studio reached through a scene carries only ID and Name, for the reason Performer does: the shared scene selection asks for no more, because a page of scenes should not drag a full record along for each one.

type StudioInput added in v0.8.0

type StudioInput struct {
	// ID is set on an update and empty on a create.
	ID   string
	Name string

	Details  string
	ParentID string
	Aliases  []string
	URLs     []string
	TagIDs   []string
	StashIDs []StashID
	// Image is a URL or a data: URI. Given a URL the server fetches it.
	Image     string
	Rating100 *int
	Favorite  *bool
}

StudioInput is a studio to create or update. Every field but Name is optional and omitted when empty, so an unset one leaves the stored value alone — the same shape as SceneUpdate, with the same limitation: Client.ClearStudioFields is what empties one.

type SystemState added in v0.9.0

type SystemState string

SystemState is what the server says about its own readiness. Stash's own vocabulary, not a normalisation of it.

const (
	// SystemOK means the server is set up, migrated and serving.
	SystemOK SystemState = "OK"
	// SystemSetup means Stash has no configuration file yet and is showing
	// its setup wizard. Nothing in the library API answers usefully.
	SystemSetup SystemState = "SETUP"
	// SystemNeedsMigration means the database on disk is older than the
	// binary reading it. Stash refuses to touch a library in that state
	// until [Client.Migrate] runs.
	SystemNeedsMigration SystemState = "NEEDS_MIGRATION"
)

type SystemStatus added in v0.9.0

type SystemStatus struct {
	Status SystemState `json:"status"`
	// DatabaseSchema is the version of the database on disk, and is nil on
	// a server that has none yet — which is what SETUP means.
	DatabaseSchema *int `json:"databaseSchema"`
	// AppSchema is the version the running binary expects. It being ahead
	// of DatabaseSchema is exactly the NEEDS_MIGRATION condition.
	AppSchema    int    `json:"appSchema"`
	DatabasePath string `json:"databasePath"`
	ConfigPath   string `json:"configPath"`
}

SystemStatus is the server's account of itself: what state it is in, which schema version its database is at, and where it keeps both.

The fields are the ones every supported server has. Stash has added others since (the operating system, the working and home directories, the resolved ffmpeg and ffprobe paths), and naming one here would fail the whole query against a server that lacks it — reach those through Client.Execute.

func (SystemStatus) Ready added in v0.9.0

func (s SystemStatus) Ready() bool

Ready reports whether the server is in a state where the rest of this package will work. A server mid-setup or awaiting migration answers queries, but answers them with errors.

type Tag

type Tag struct {
	ID   string `json:"id"`
	Name string `json:"name"`

	SortName    string    `json:"sort_name"`
	Description string    `json:"description"`
	Aliases     []string  `json:"aliases"`
	Favorite    bool      `json:"favorite"`
	ImagePath   string    `json:"image_path"`
	SceneCount  int       `json:"scene_count"`
	StashIDs    []StashID `json:"stash_ids"`
	// Parents and Children are one level deep: a hierarchy queried in full
	// would carry the whole tree on every tag in it.
	Parents  []Tag `json:"parents"`
	Children []Tag `json:"children"`
}

Tag attached to a scene, and as the tag queries return one.

A tag reached through a scene carries only ID and Name — the shared scene selection asks for no more.

type TagInput added in v0.8.0

type TagInput struct {
	// ID is set on an update and empty on a create.
	ID   string
	Name string

	SortName    string
	Description string
	Aliases     []string
	// ParentIDs and ChildIDs replace the tag's place in the hierarchy rather
	// than adding to it. Read the tag first and send the union if adding is
	// what you meant.
	ParentIDs []string
	ChildIDs  []string
	StashIDs  []StashID
	Image     string
	Favorite  *bool
}

TagInput is a tag to create or update. Every field but Name is optional and omitted when empty, so an unset one leaves the stored value alone.

type Tier added in v0.10.0

type Tier int

Tier is the resolution class a file belongs to, judged from its pixel dimensions. Stash's own `resolution` label is not to be trusted for this: libraries carry 720x404 files labelled HD because someone read "720 wide" as 720p, and square cover art labelled 8K.

The tiers are bands on the longer side, so 4K is not also 1080-tier and a portrait file is judged by its height.

const (
	TierUnknown Tier = iota // no dimensions, or a side of zero
	TierSD                  // longer side under 1280
	Tier720                 // 1280 up to 1920
	Tier1080                // 1920 up to 2560
	Tier1440                // 2560 up to 3840
	Tier4K                  // 3840 up to 7680
	Tier8K                  // 7680 and beyond
)

The bands, on the longer side; each line gives its lower bound.

func TierOf added in v0.10.0

func TierOf(width, height int) Tier

TierOf classifies a frame of the given pixel dimensions. Orientation does not matter: the longer side is taken as the width, so 1080x1920 is Tier1080 like 1920x1080. Either side at or below zero is TierUnknown.

func (Tier) String added in v0.10.0

func (t Tier) String() string

type UnionPolicy added in v0.10.0

type UnionPolicy struct {
	Title    FieldPolicy
	Code     FieldPolicy
	Date     FieldPolicy
	Details  FieldPolicy
	Director FieldPolicy
	Studio   FieldPolicy

	URLs       FieldPolicy
	Tags       FieldPolicy
	Performers FieldPolicy
	Galleries  FieldPolicy
	StashIDs   FieldPolicy

	Rating100 FieldPolicy
	Organized FieldPolicy
}

UnionPolicy is a FieldPolicy per scene field Union knows how to combine. Fields Stash's sceneMerge already handles — files, markers, play history — are not here.

func DefaultUnionPolicy added in v0.10.0

func DefaultUnionPolicy() UnionPolicy

DefaultUnionPolicy is the policy a merge wants: lists are unioned, scalars are kept unless the destination has none, the rating is the highest and the scene is organized if any copy was.

Jump to

Keyboard shortcuts

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