msclient

package
v0.2.1 Latest Latest
Warning

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

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

Documentation

Overview

Package msclient is the plugin-side client for the moansubs server. It implements the bucketed lookup flow from PLAN.md "Lookup: bucketed by default": derive the oshash prefix and MIH blocks locally (internal/hash is the shared source of truth for both ends of the API contract), fetch the buckets in one batch request, and do all true-distance filtering client-side. Exact mode (full hash to the server) is opt-in.

Index

Constants

View Source
const MaxMetadataEntries = 25

MaxMetadataEntries mirrors the server's own per-request cap. Callers batch to it rather than discovering the 400.

View Source
const MaxResponseBytes = 4 << 20 // 4 MiB

MaxResponseBytes caps a decoded success response body: a subtitle track (internal/subtitle.MaxBytes, 2 MiB) plus JSON field overhead comfortably fits under this. A hostile or merely broken server that answers 200 with an unbounded body must fail loudly here instead of exhausting the caller's memory — the error path already caps at 4096 bytes, but the success path used to decode straight off the wire with no limit at all.

Variables

View Source
var ErrNoMatchEndpoint = errors.New("msclient: server has no /api/v1/match endpoint (older server?)")

ErrNoMatchEndpoint means the server answered 404 to POST /api/v1/match — an older server predating the v2 no-phash fallback (PLAN.md "Matching" level 5). Callers must degrade to "no fallback" silently rather than surface an error, since this is an expected compatibility case, not a failure.

Functions

This section is empty.

Types

type Client

type Client struct {
	// BaseURL is the server root, e.g. "https://subs.example".
	BaseURL string

	// Token authorizes uploads; lookups and downloads are anonymous.
	Token string

	HTTP *http.Client
}

Client talks to one moansubs server. Safe for concurrent use.

func New

func New(baseURL, token string) *Client

New returns a client for the moansubs server at baseURL, authenticating with token. A trailing slash on baseURL is ignored.

func (*Client) ContributeMetadata added in v0.2.0

func (c *Client) ContributeMetadata(ctx context.Context, entries []MetadataEntry) ([]MetadataResult, error)

ContributeMetadata sends POST /api/v1/metadata: what these scenes are, with no subtitle attached.

Deliberately a separate authenticated request rather than something riding along with a download. Downloads are anonymous by documented promise, and receiving a file and telling a node what your library contains are two different consents — a client that wants both makes two requests, and the person doing it chose to.

func (*Client) GetTrack

func (c *Client) GetTrack(ctx context.Context, id int64) (*Track, error)

GetTrack downloads one full subtitle track.

func (*Client) GetTrackFor added in v0.2.0

func (c *Client) GetTrackFor(ctx context.Context, id, forRelease int64) (*Track, error)

GetTrackFor fetches a track timed for forRelease. Pass 0 (or the track's own release) to get the body exactly as its uploader authored it.

The shift is the server's to apply, not the plugin's: it holds the recorded offset for the pairing, and doing it there keeps one implementation of the retiming instead of two that can disagree.

func (*Client) LookupBuckets

func (c *Client) LookupBuckets(ctx context.Context, oshash hash.OSHash, phash *hash.PHash) ([]Release, error)

LookupBuckets performs the default privacy-conscious lookup for one scene file: the 5-char oshash prefix bucket plus, when a phash is present, all five MIH block buckets, in a single batch request. The union of returned releases is deduplicated; the caller filters by true oshash equality and Hamming distance locally — the server never learns which candidate, if any, was the real match.

func (*Client) LookupBucketsBatch

func (c *Client) LookupBucketsBatch(ctx context.Context, keys []SceneKeys) ([][]Release, error)

LookupBucketsBatch resolves many scenes' buckets in as few requests as the server's batch cap allows: bucket keys are deduplicated across scenes (a wall of related content often shares buckets), chunked, fetched, and mapped back so out[i] holds the deduplicated releases for keys[i]. This is what keeps a SceneCard wall at ~1 request instead of one per card (PLAN.md step 5: batched lookups).

func (*Client) LookupExact

func (c *Client) LookupExact(ctx context.Context, oshash hash.OSHash, phash *hash.PHash, maxDistance int) ([]Release, error)

LookupExact is full-hash mode: sends the complete fingerprints to the server for fuzzy matching up to maxDistance (≤8). Opt-in only — this reveals exactly what the bucketed flow is designed not to.

func (*Client) LookupStashIDs

func (c *Client) LookupStashIDs(ctx context.Context, ids []StashID) ([][]Release, error)

LookupStashIDs resolves releases matching any of ids via the batch endpoint's stash_ids form (migration 0011, WP-C9a level 0 "identity" match): a scene's own stash-box ids identify it across every encode, which beats phash outright and costs no stash-box API key. Returns a slice aligned with ids — out[i] holds ids[i]'s matching releases, [] when none, so a caller can attribute a hit back to which of the scene's own ids produced it (e.g. for a "same StashDB scene" reason). An id whose endpoint or stash_id doesn't even parse locally is simply skipped rather than failing the whole call — same "one broken entry doesn't sink the batch" reasoning LookupBucketsBatch's caller (badge) relies on.

func (*Client) Match

func (c *Client) Match(ctx context.Context, req MatchRequest) (*MatchResult, error)

Match calls POST /api/v1/match with a query scene's name metadata. POST keeps titles and filenames out of access logs, same rationale as exact mode. Returns ErrNoMatchEndpoint when the server predates this endpoint.

func (*Client) Unvote

func (c *Client) Unvote(ctx context.Context, trackID int64) error

Unvote retracts the caller's own vote on trackID, if any — idempotent, same as the server's DELETE. Requires the upload token.

func (*Client) Upload

func (c *Client) Upload(ctx context.Context, req UploadRequest) (*UploadResult, error)

Upload pushes one subtitle to the server. Requires the account token.

func (*Client) Version

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

Version calls GET /api/v1/version. A 404 — a pre-0.2 node that predates this endpoint entirely — is not an error: it yields &ServerVersion{Features: nil}, the same "nothing advertised" shape as a current node with an empty feature list, so callers only ever need to check Features, never a separate not-found case.

func (*Client) Vote

func (c *Client) Vote(ctx context.Context, trackID int64, value int, reason, note string) (up, down int, err error)

Vote casts, or replaces, the caller's vote on trackID: value is 1 or -1; reason (one of the five WP-C3 reasons) is required by the server on a down-vote and ignored on an up-vote. Requires the upload token, same Bearer auth as Upload. The server's rejection text (e.g. "cannot vote on your own upload") comes back verbatim rather than wrapped, since the plugin panel shows it straight to the user next to the track row.

func (*Client) VoteCounts

func (c *Client) VoteCounts(ctx context.Context, trackID int64) (up, down int, err error)

VoteCounts fetches a track's current up/down tally via the public GET /api/v1/subtitles/{id}/votes endpoint. It exists because DELETE .../vote answers 204 with no body: after Unvote, this is how a caller learns the post-retract counts without going through GetTrack, whose GET /api/v1/subtitles/{id} would silently bump the download counter as a side effect (API.md).

type MatchCandidate

type MatchCandidate struct {
	Release Release  `json:"release"`
	Title   *string  `json:"title"`
	Stem    *string  `json:"stem"`
	Date    *string  `json:"date"`
	Score   float64  `json:"score"`
	NameSim float64  `json:"name_sim"`
	DeltaMs int64    `json:"delta_ms"`
	Reasons []string `json:"reasons"`
}

MatchCandidate is one scored possibility, mirroring the server's matchCandidate. Title/Stem/Date are the stored release's own name metadata, echoed back so a caller can show what the score was computed against — Date is null when the release has none.

type MatchRequest

type MatchRequest struct {
	Stem       string   `json:"stem,omitempty"`
	Title      string   `json:"title,omitempty"`
	Date       string   `json:"date,omitempty"`
	Studio     string   `json:"studio,omitempty"`
	Performers []string `json:"performers,omitempty"`
	DurationMs int64    `json:"duration_ms"`
}

MatchRequest carries a scene's name metadata to POST /api/v1/match, the no-phash fallback used only once hash-based lookup finds nothing. Mirrors internal/api's matchRequest field-for-field.

type MatchResult

type MatchResult struct {
	Verdict    string           `json:"verdict"`
	Candidates []MatchCandidate `json:"candidates"`
}

MatchResult mirrors the server's matchResponse. Verdict is one of CONFIRMED/LIKELY/AMBIGUOUS/UNMATCHED, but every verdict here is offer-only: name evidence, unlike a fingerprint, is never grounds to auto-apply (PLAN.md "Matching").

type MetadataEntry added in v0.2.0

type MetadataEntry struct {
	OSHash     string    `json:"oshash"`
	Title      string    `json:"title,omitempty"`
	Date       string    `json:"date,omitempty"`
	Studio     string    `json:"studio,omitempty"`
	Performers []string  `json:"performers,omitempty"`
	StashIDs   []StashID `json:"stash_ids,omitempty"`
}

MetadataEntry is one scene's name metadata, contributed without a subtitle. OSHash identifies the release; the server resolves it and answers "not known" rather than creating anything.

func (MetadataEntry) HasContent added in v0.2.0

func (e MetadataEntry) HasContent() bool

HasContent reports whether the entry says anything worth sending. A scene Stash knows nothing about produces an entry the server would accept and record as nothing, so the round trip is skipped instead.

type MetadataResult added in v0.2.0

type MetadataResult struct {
	ReleaseID int64  `json:"release_id"`
	Known     bool   `json:"known"`
	Recorded  bool   `json:"recorded"`
	Error     string `json:"error"`
}

MetadataResult is one entry's answer, in request order.

type Release

type Release struct {
	ID         int64          `json:"id"`
	OSHash     string         `json:"oshash"`
	PHash      *string        `json:"phash"`
	DurationMs int64          `json:"duration_ms"`
	Width      *int           `json:"width"`
	Height     *int           `json:"height"`
	VideoCodec *string        `json:"video_codec"`
	Tracks     []TrackSummary `json:"tracks"`
	// StashIDs is migration 0011's stash-box scene identities (WP-C9a),
	// present on every release a lookup response carries.
	StashIDs []StashID `json:"stash_ids"`
	// Siblings are tracks from other encodes of the same video, kept apart
	// from Tracks so the panel can say plainly that they were timed
	// against a different file. Absent on a server that predates works,
	// which simply means no siblings are offered.
	Siblings []Sibling `json:"siblings"`
}

Release mirrors the lookup API's per-release shape.

type SceneKeys

type SceneKeys struct {
	OSHash hash.OSHash
	PHash  *hash.PHash
}

SceneKeys is one scene file's lookup keys.

type ServerVersion

type ServerVersion struct {
	Version  string   `json:"version"`
	Features []string `json:"features"`
	// StashEndpoints is the node's stash-box endpoint allow-list (WP-R6):
	// msclientStashIDs (plugin's app.go) drops any id whose endpoint isn't
	// in this list before a push, rather than letting the server's 400
	// reject it one id at a time. A single entry of "*" means the node
	// accepts any http(s) endpoint. nil on a server that predates this
	// field — the same "nothing advertised" shape as an empty Features —
	// so callers read that as "send everything, as before".
	StashEndpoints []string `json:"stash_endpoints"`
}

ServerVersion mirrors GET /api/v1/version: the node's build version and its advertised feature list, so a caller can tell what an older server is missing before tripping over a 404 mid-task.

type Sibling added in v0.2.0

type Sibling struct {
	ID         int64   `json:"id"`
	ReleaseID  int64   `json:"release_id"`
	Lang       string  `json:"lang"`
	Generated  bool    `json:"generated"`
	Downloads  int64   `json:"downloads"`
	OffsetMs   *int64  `json:"offset_ms"`
	OffsetFrom *string `json:"offset_source,omitempty"`
}

Sibling is a subtitle authored against another cut of the same video.

OffsetMs is a pointer because "checked, no shift needed" and "nobody has checked" are different things, and showing the second as the first would promise a fit that was never verified.

type StashID

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

StashID is one stash-box scene identity (migration 0011, WP-C9a) — sent on upload and echoed back on every release a lookup response carries.

type Track

type Track struct {
	// OffsetMs is the shift the server applied because this was fetched
	// for another release; 0 when none was.
	OffsetMs   int64           `json:"offset_ms"`
	OffsetFrom string          `json:"offset_source"`
	ID         int64           `json:"id"`
	ReleaseID  int64           `json:"release_id"`
	Lang       string          `json:"lang"`
	Body       string          `json:"body"`
	Generated  bool            `json:"generated"`
	License    string          `json:"license"`
	Source     *string         `json:"source"`
	Provenance json.RawMessage `json:"provenance"`
	// Downloads/Up/Down mirror TrackSummary's counters — API.md documents
	// them on this response too, but note that fetching this endpoint
	// itself increments Downloads by one (API.md "Every successful (200)
	// call here increments the track's downloads counter"), so this is a
	// snapshot from *before* the current call, not after.
	Downloads int64 `json:"downloads"`
	Up        int   `json:"up"`
	Down      int   `json:"down"`
}

Track is a full subtitle track as returned by GET /api/v1/subtitles/{id}.

type TrackSummary

type TrackSummary struct {
	ID            int64  `json:"id"`
	Lang          string `json:"lang"`
	Generated     bool   `json:"generated"`
	License       string `json:"license"`
	HasProvenance bool   `json:"has_provenance"`
	CreatedAt     string `json:"created_at"`
	// Downloads/Up/Down are migration 0006/0008's counters (WP-A2/WP-C3),
	// present on every track summary in lookup responses — this is what
	// lets the plugin panel show a candidate's tallies without a second
	// round trip per track.
	Downloads int64 `json:"downloads"`
	Up        int   `json:"up"`
	Down      int   `json:"down"`
}

TrackSummary mirrors the lookup API's per-track shape.

type UploadRequest

type UploadRequest struct {
	OSHash     string `json:"oshash"`
	PHash      string `json:"phash,omitempty"`
	MD5        string `json:"md5,omitempty"`
	DurationMs int64  `json:"duration_ms"`
	Lang       string `json:"lang"`
	Body       string `json:"body"`

	// Optional scene name metadata (server migration 0003), stored on the
	// release so the v2 no-phash fallback (POST /api/v1/match) can offer it
	// later. Omitempty is load-bearing here: a scene Stash didn't report a
	// studio for must send no "studio" field at all, not an empty string —
	// GetOrCreateRelease's backfill only fires when the existing release has
	// no metadata whatsoever, and an empty string is still "sent".
	Title      string   `json:"title,omitempty"`
	Stem       string   `json:"stem,omitempty"`
	Date       string   `json:"date,omitempty"`
	Studio     string   `json:"studio,omitempty"`
	Performers []string `json:"performers,omitempty"`

	// StashIDs are the scene's stash-box identities (migration 0011,
	// WP-C9a) — sent with every push so the server can attach them to the
	// release, additive like the name metadata above.
	StashIDs []StashID `json:"stash_ids,omitempty"`
}

UploadRequest is one subtitle upload (POST /api/v1/subtitles).

type UploadResult

type UploadResult struct {
	TrackID   int64 `json:"track_id"`
	ReleaseID int64 `json:"release_id"`
	Generated bool  `json:"generated"`
	// Duplicate means a byte-identical track already existed server-side —
	// the normal outcome when a push task is re-run.
	Duplicate bool `json:"duplicate"`
}

UploadResult mirrors the server's upload response.

Jump to

Keyboard shortcuts

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