This commit is contained in:
InfiniteStash 2026-03-31 13:10:40 +02:00
parent 1bc2b831f4
commit 49b673dcd0
11 changed files with 44 additions and 167 deletions

View file

@ -248,7 +248,7 @@ type Query {
validateStashBoxCredentials(input: StashBoxInput!): StashBoxValidationResult!
"List pending fingerprint submissions for a stash-box endpoint"
pendingFingerprintSubmissions(endpoint: String!): [FingerprintSubmission!]!
pendingFingerprintSubmissions(stash_box_endpoint: String!): [FingerprintSubmission!]!
# System status
systemStatus: SystemStatus!
@ -578,7 +578,7 @@ type Mutation {
"Remove a fingerprint submission from the queue"
removeFingerprintSubmission(input: RemoveFingerprintInput!): Boolean!
"Submit all pending fingerprint submissions for a stash-box endpoint"
submitFingerprintSubmissions(endpoint: String!): Boolean!
submitFingerprintSubmissions(stash_box_endpoint: String!): Boolean!
"Backup the database. Optionally returns a link to download the database file"
backupDatabase(input: BackupDatabaseInput!): String

View file

@ -31,7 +31,7 @@ enum FingerprintVote {
}
input FingerprintSubmissionInput {
scene_id: String!
scene_id: ID!
stash_box_scene_id: String!
stash_box_endpoint: String!
vote: FingerprintVote!

View file

@ -15,12 +15,6 @@ import (
)
func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input StashBoxFingerprintSubmissionInput) (bool, error) {
// New format: use fingerprints field with explicit stash-box scene IDs and votes
if len(input.Fingerprints) > 0 {
return r.submitFingerprintsNew(ctx, input.Fingerprints)
}
// Legacy format: use scene_ids and look up stash_ids from scenes
b, err := resolveStashBox(nil, input.StashBoxEndpoint)
if err != nil {
return false, err
@ -45,73 +39,6 @@ func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input
return client.SubmitFingerprints(ctx, scenes)
}
func (r *mutationResolver) submitFingerprintsNew(ctx context.Context, submissions []*FingerprintSubmissionInput) (bool, error) {
// Group submissions by endpoint
byEndpoint := make(map[string][]*FingerprintSubmissionInput)
for _, s := range submissions {
byEndpoint[s.StashBoxEndpoint] = append(byEndpoint[s.StashBoxEndpoint], s)
}
for endpoint, endpointSubmissions := range byEndpoint {
b, err := resolveStashBox(nil, &endpoint)
if err != nil {
return false, err
}
// Collect all scene IDs for this endpoint
sceneIDSet := make(map[string]struct{})
for _, s := range endpointSubmissions {
sceneIDSet[s.SceneID] = struct{}{}
}
sceneIDs := make([]string, 0, len(sceneIDSet))
for id := range sceneIDSet {
sceneIDs = append(sceneIDs, id)
}
ids, err := stringslice.StringSliceToIntSlice(sceneIDs)
if err != nil {
return false, err
}
var scenes []*models.Scene
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
scenes, err = r.sceneService.FindByIDs(ctx, ids, scene.LoadFiles)
return err
}); err != nil {
return false, err
}
// Build a map of scene ID to scene for quick lookup
sceneMap := make(map[int]*models.Scene)
for _, s := range scenes {
sceneMap[s.ID] = s
}
client := r.newStashBoxClient(*b)
// Submit each fingerprint with its vote
for _, sub := range endpointSubmissions {
sceneID, err := strconv.Atoi(sub.SceneID)
if err != nil {
return false, fmt.Errorf("invalid scene ID %s: %w", sub.SceneID, err)
}
s, ok := sceneMap[sceneID]
if !ok {
return false, fmt.Errorf("scene %d not found", sceneID)
}
vote := stashbox.FingerprintVote(sub.Vote)
if err := client.SubmitFingerprintsWithVote(ctx, s, sub.StashBoxSceneID, vote); err != nil {
return false, err
}
}
}
return true, nil
}
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input manager.StashBoxBatchTagInput) (string, error) {
b, err := resolveStashBoxBatchTagInput(input.Endpoint, input.StashBoxEndpoint) //nolint:staticcheck
if err != nil {
@ -330,8 +257,8 @@ func (r *mutationResolver) RemoveFingerprintSubmission(ctx context.Context, inpu
return true, nil
}
func (r *mutationResolver) SubmitFingerprintSubmissions(ctx context.Context, endpoint string) (bool, error) {
b, err := resolveStashBox(nil, &endpoint)
func (r *mutationResolver) SubmitFingerprintSubmissions(ctx context.Context, stashBoxEndpoint string) (bool, error) {
b, err := resolveStashBox(nil, &stashBoxEndpoint)
if err != nil {
return false, err
}
@ -339,7 +266,7 @@ func (r *mutationResolver) SubmitFingerprintSubmissions(ctx context.Context, end
var submissions []*models.FingerprintSubmission
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
submissions, err = r.repository.FingerprintSubmission.FindByEndpoint(ctx, endpoint)
submissions, err = r.repository.FingerprintSubmission.FindByEndpoint(ctx, stashBoxEndpoint)
return err
}); err != nil {
return false, err
@ -349,27 +276,20 @@ func (r *mutationResolver) SubmitFingerprintSubmissions(ctx context.Context, end
return true, nil
}
// Collect all scene IDs
sceneIDSet := make(map[int]struct{})
for _, s := range submissions {
sceneIDSet[s.SceneID] = struct{}{}
}
sceneIDs := make([]int, 0, len(sceneIDSet))
for id := range sceneIDSet {
sceneIDs = append(sceneIDs, id)
ids := make([]int, len(submissions))
for i, sub := range submissions {
ids[i] = sub.SceneID
}
var scenes []*models.Scene
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
scenes, err = r.sceneService.FindByIDs(ctx, sceneIDs, scene.LoadFiles)
scenes, err = r.sceneService.FindByIDs(ctx, ids, scene.LoadFiles)
return err
}); err != nil {
return false, err
}
// Build a map of scene ID to scene
sceneMap := make(map[int]*models.Scene)
for _, s := range scenes {
sceneMap[s.ID] = s
@ -377,7 +297,17 @@ func (r *mutationResolver) SubmitFingerprintSubmissions(ctx context.Context, end
client := r.newStashBoxClient(*b)
// Submit each fingerprint and track successful submissions
if len(submissions) > 40 {
// Submit async to avoid timeouts for large batches
go r.submitFingerprintBatch(client, submissions, sceneMap)
} else {
r.submitFingerprintBatch(client, submissions, sceneMap)
}
return true, nil
}
func (r *mutationResolver) submitFingerprintBatch(client *stashbox.Client, submissions []*models.FingerprintSubmission, sceneMap map[int]*models.Scene) {
var successfulSubmissions []*models.FingerprintSubmission
for _, sub := range submissions {
s, ok := sceneMap[sub.SceneID]
@ -386,8 +316,7 @@ func (r *mutationResolver) SubmitFingerprintSubmissions(ctx context.Context, end
continue
}
vote := stashbox.FingerprintVote(sub.Vote)
if err := client.SubmitFingerprintsWithVote(ctx, s, sub.StashID, vote); err != nil {
if err := client.SubmitFingerprintsWithVote(context.Background(), s, sub.StashID, sub.Vote); err != nil {
logger.Warnf("Failed to submit fingerprint for scene %d: %v", sub.SceneID, err)
continue
}
@ -395,9 +324,8 @@ func (r *mutationResolver) SubmitFingerprintSubmissions(ctx context.Context, end
successfulSubmissions = append(successfulSubmissions, sub)
}
// Delete successful submissions from the queue
if len(successfulSubmissions) > 0 {
if err := r.withTxn(ctx, func(ctx context.Context) error {
if err := r.withTxn(context.Background(), func(ctx context.Context) error {
for _, sub := range successfulSubmissions {
if err := r.repository.FingerprintSubmission.Delete(ctx, sub.Endpoint, sub.StashID); err != nil {
return err
@ -405,9 +333,7 @@ func (r *mutationResolver) SubmitFingerprintSubmissions(ctx context.Context, end
}
return nil
}); err != nil {
return false, err
logger.Warnf("Failed to delete fingerprint submissions: %v", err)
}
}
return true, nil
}

View file

@ -6,9 +6,9 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) PendingFingerprintSubmissions(ctx context.Context, endpoint string) (ret []*models.FingerprintSubmission, err error) {
func (r *queryResolver) PendingFingerprintSubmissions(ctx context.Context, stashBoxEndpoint string) (ret []*models.FingerprintSubmission, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.FingerprintSubmission.FindByEndpoint(ctx, endpoint)
ret, err = r.repository.FingerprintSubmission.FindByEndpoint(ctx, stashBoxEndpoint)
return err
}); err != nil {
return nil, err

View file

@ -34,7 +34,6 @@ type FingerprintSubmission struct {
type FingerprintSubmissionReader interface {
FindByEndpoint(ctx context.Context, endpoint string) ([]*FingerprintSubmission, error)
Find(ctx context.Context, endpoint string, stashID string) (*FingerprintSubmission, error)
}
type FingerprintSubmissionWriter interface {

View file

@ -2,8 +2,6 @@ package sqlite
import (
"context"
"database/sql"
"errors"
"github.com/doug-martin/goqu/v9"
"github.com/doug-martin/goqu/v9/exp"
@ -18,8 +16,7 @@ const (
var (
fingerprintSubmissionsTableMgr = &table{
table: goqu.T(fingerprintSubmissionsTable),
idColumn: goqu.T(fingerprintSubmissionsTable).Col("endpoint"), // not a real ID column, but needed for table struct
table: goqu.T(fingerprintSubmissionsTable),
}
)
@ -100,19 +97,6 @@ func (qb *FingerprintSubmissionStore) DeleteByEndpoint(ctx context.Context, endp
return nil
}
func (qb *FingerprintSubmissionStore) Find(ctx context.Context, endpoint string, stashID string) (*models.FingerprintSubmission, error) {
q := qb.selectDataset().Where(
qb.table().Col("endpoint").Eq(endpoint),
qb.table().Col("stash_id").Eq(stashID),
)
ret, err := qb.get(ctx, q)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return ret, err
}
func (qb *FingerprintSubmissionStore) FindByEndpoint(ctx context.Context, endpoint string) ([]*models.FingerprintSubmission, error) {
q := qb.selectDataset().Where(
qb.table().Col("endpoint").Eq(endpoint),
@ -121,19 +105,6 @@ func (qb *FingerprintSubmissionStore) FindByEndpoint(ctx context.Context, endpoi
return qb.getMany(ctx, q)
}
func (qb *FingerprintSubmissionStore) get(ctx context.Context, q *goqu.SelectDataset) (*models.FingerprintSubmission, error) {
ret, err := qb.getMany(ctx, q)
if err != nil {
return nil, err
}
if len(ret) == 0 {
return nil, sql.ErrNoRows
}
return ret[0], nil
}
func (qb *FingerprintSubmissionStore) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*models.FingerprintSubmission, error) {
const single = false
var ret []*models.FingerprintSubmission

View file

@ -26,13 +26,12 @@ func TestFingerprintSubmissionCreate(t *testing.T) {
assert.NoError(t, err)
// Verify it was created
found, err := db.FingerprintSubmission.Find(ctx, submission.Endpoint, submission.StashID)
found, err := db.FingerprintSubmission.FindByEndpoint(ctx, submission.Endpoint)
assert.NoError(t, err)
assert.NotNil(t, found)
assert.Equal(t, submission.Endpoint, found.Endpoint)
assert.Equal(t, submission.StashID, found.StashID)
assert.Equal(t, submission.SceneID, found.SceneID)
assert.Equal(t, submission.Vote, found.Vote)
assert.Len(t, found, 1)
assert.Equal(t, submission.StashID, found[0].StashID)
assert.Equal(t, submission.SceneID, found[0].SceneID)
assert.Equal(t, submission.Vote, found[0].Vote)
return nil
})
@ -64,20 +63,10 @@ func TestFingerprintSubmissionCreateDuplicate(t *testing.T) {
assert.NoError(t, err)
// Original should still exist unchanged
found, err := db.FingerprintSubmission.Find(ctx, submission.Endpoint, submission.StashID)
found, err := db.FingerprintSubmission.FindByEndpoint(ctx, submission.Endpoint)
assert.NoError(t, err)
assert.Equal(t, models.FingerprintVoteValid, found.Vote)
return nil
})
}
func TestFingerprintSubmissionFind(t *testing.T) {
withTxn(func(ctx context.Context) error {
// Find non-existent
found, err := db.FingerprintSubmission.Find(ctx, "non-existent", "non-existent")
assert.NoError(t, err)
assert.Nil(t, found)
assert.Len(t, found, 1)
assert.Equal(t, models.FingerprintVoteValid, found[0].Vote)
return nil
})
@ -138,9 +127,9 @@ func TestFingerprintSubmissionDelete(t *testing.T) {
assert.NoError(t, err)
// Verify it's gone
found, err := db.FingerprintSubmission.Find(ctx, submission.Endpoint, submission.StashID)
found, err := db.FingerprintSubmission.FindByEndpoint(ctx, submission.Endpoint)
assert.NoError(t, err)
assert.Nil(t, found)
assert.Len(t, found, 0)
return nil
})

View file

@ -460,16 +460,8 @@ func (c Client) submitFingerprints(ctx context.Context, fingerprints []graphql.F
return true, nil
}
// FingerprintVote represents the vote type for a fingerprint submission
type FingerprintVote string
const (
FingerprintVoteValid FingerprintVote = "VALID"
FingerprintVoteInvalid FingerprintVote = "INVALID"
)
// SubmitFingerprintsWithVote submits fingerprints for a scene with an explicit stash-box scene ID and vote
func (c Client) SubmitFingerprintsWithVote(ctx context.Context, scene *models.Scene, stashBoxSceneID string, vote FingerprintVote) error {
func (c Client) SubmitFingerprintsWithVote(ctx context.Context, scene *models.Scene, stashBoxSceneID string, vote models.FingerprintVote) error {
var fingerprints []graphql.FingerprintSubmission
for _, f := range scene.Files.List() {

View file

@ -32,6 +32,6 @@ mutation RemoveFingerprintSubmission($input: RemoveFingerprintInput!) {
removeFingerprintSubmission(input: $input)
}
mutation SubmitFingerprintSubmissions($endpoint: String!) {
submitFingerprintSubmissions(endpoint: $endpoint)
mutation SubmitFingerprintSubmissions($stash_box_endpoint: String!) {
submitFingerprintSubmissions(stash_box_endpoint: $stash_box_endpoint)
}

View file

@ -47,8 +47,8 @@ query LatestVersion {
}
}
query PendingFingerprintSubmissions($endpoint: String!) {
pendingFingerprintSubmissions(endpoint: $endpoint) {
query PendingFingerprintSubmissions($stash_box_endpoint: String!) {
pendingFingerprintSubmissions(stash_box_endpoint: $stash_box_endpoint) {
endpoint
stash_id
scene {

View file

@ -245,7 +245,7 @@ export const TaggerContext: React.FC = ({ children }) => {
// Query pending fingerprint submissions from the backend
const endpoint = currentSource?.sourceInput.stash_box_endpoint;
const { data: pendingData, refetch: refetchPending } = GQL.usePendingFingerprintSubmissionsQuery({
variables: { endpoint: endpoint ?? "" },
variables: { stash_box_endpoint: endpoint ?? "" },
skip: !endpoint,
});
@ -275,7 +275,7 @@ export const TaggerContext: React.FC = ({ children }) => {
setLoading(true);
await submitFingerprintsMutation({
variables: {
endpoint,
stash_box_endpoint: endpoint,
},
});