Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gqlgen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,8 @@ models:
fields:
career_length:
resolver: true
FingerprintSubmission:
fields:
scene:
resolver: true

14 changes: 13 additions & 1 deletion graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ type Query {
): Directory!
validateStashBoxCredentials(input: StashBoxInput!): StashBoxValidationResult!

"List pending fingerprint submissions for a stash-box endpoint"
pendingFingerprintSubmissions(
stash_box_endpoint: String!
): [FingerprintSubmission!]!

# System status
systemStatus: SystemStatus!

Expand Down Expand Up @@ -563,13 +568,20 @@ type Mutation {
"Submit fingerprints to stash-box instance"
submitStashBoxFingerprints(
input: StashBoxFingerprintSubmissionInput!
): Boolean!
): Boolean! @deprecated(reason: "Use submitFingerprintSubmissions")

"Submit scene as draft to stash-box instance"
submitStashBoxSceneDraft(input: StashBoxDraftSubmissionInput!): ID
"Submit performer as draft to stash-box instance"
submitStashBoxPerformerDraft(input: StashBoxDraftSubmissionInput!): ID

"Add a fingerprint submission to the queue"
queueFingerprintSubmission(input: QueueFingerprintInput!): Boolean!
"Remove a fingerprint submission from the queue"
removeFingerprintSubmission(input: RemoveFingerprintInput!): Boolean!
"Submit all pending fingerprint submissions for a stash-box endpoint"
submitFingerprintSubmissions(stash_box_endpoint: String!): Boolean!

"Backup the database. Optionally returns a link to download the database file"
backupDatabase(input: BackupDatabaseInput!): String

Expand Down
3 changes: 3 additions & 0 deletions graphql/schema/types/scraper.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,9 @@ type StashBoxFingerprint {
algorithm: String!
hash: String!
duration: Int!
reports: Int!
user_submitted: Boolean!
user_reported: Boolean!
}

"""
Expand Down
25 changes: 25 additions & 0 deletions graphql/schema/types/stash-box.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ input StashIDInput {
updated_at: Time
}

enum FingerprintVote {
VALID
INVALID
}

input StashBoxFingerprintSubmissionInput {
scene_ids: [String!]!
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
Expand All @@ -36,3 +41,23 @@ input StashBoxDraftSubmissionInput {
stash_box_index: Int @deprecated(reason: "use stash_box_endpoint")
stash_box_endpoint: String
}

type FingerprintSubmission {
endpoint: String!
stash_id: String!
scene: Scene!
vote: FingerprintVote!
created_at: Time!
}

input QueueFingerprintInput {
endpoint: String!
stash_id: String!
scene_id: ID!
vote: FingerprintVote!
}

input RemoveFingerprintInput {
endpoint: String!
stash_id: String!
}
3 changes: 3 additions & 0 deletions graphql/stash-box/query.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
reports
user_submitted
user_reported
Comment on lines +100 to +102
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With these new fields will it break other Stashbox instance requests? From my understanding they don't return a null they would just error out the entire query.

Not 100% on this on, might need some education.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These fields were added to stash-box in version 0.6.0 IIRC, which was in early Jan 25. If the stash-box instance doesn't support the fields, it will fail, but that's a fairly old version..

That said, TPDB does not support these fields, and will break. Supporting them can be done by just stubbing them out and returning 0/false.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I can notify TPDB when this gets closer to a merge so that it minimizes breakages.

}

fragment SceneFragment on Scene {
Expand Down
4 changes: 4 additions & 0 deletions internal/api/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ func (r *Resolver) Plugin() PluginResolver {
func (r *Resolver) ConfigResult() ConfigResultResolver {
return &configResultResolver{r}
}
func (r *Resolver) FingerprintSubmission() FingerprintSubmissionResolver {
return &fingerprintSubmissionResolver{r}
}

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Expand All @@ -137,6 +140,7 @@ type folderResolver struct{ *Resolver }
type savedFilterResolver struct{ *Resolver }
type pluginResolver struct{ *Resolver }
type configResultResolver struct{ *Resolver }
type fingerprintSubmissionResolver struct{ *Resolver }

func (r *Resolver) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return r.repository.WithTxn(ctx, fn)
Expand Down
25 changes: 25 additions & 0 deletions internal/api/resolver_model_fingerprint_submission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package api

import (
"context"
"fmt"

"github.com/stashapp/stash/pkg/models"
)

func (r *fingerprintSubmissionResolver) Scene(ctx context.Context, obj *models.FingerprintSubmission) (*models.Scene, error) {
var ret *models.Scene
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Scene.Find(ctx, obj.SceneID)
return err
}); err != nil {
return nil, err
}

if ret == nil {
return nil, fmt.Errorf("scene %d not found", obj.SceneID)
}

return ret, nil
}
121 changes: 120 additions & 1 deletion internal/api/resolver_mutation_stash_box.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"time"

"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/logger"
Expand All @@ -14,7 +15,7 @@ import (
)

func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input StashBoxFingerprintSubmissionInput) (bool, error) {
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint)
b, err := resolveStashBox(input.StashBoxIndex, input.StashBoxEndpoint) //nolint:staticcheck
if err != nil {
return false, err
}
Expand Down Expand Up @@ -222,3 +223,121 @@ func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, inp

return res, err
}

func (r *mutationResolver) QueueFingerprintSubmission(ctx context.Context, input QueueFingerprintInput) (bool, error) {
sceneID, err := strconv.Atoi(input.SceneID)
if err != nil {
return false, fmt.Errorf("invalid scene ID: %w", err)
}

submission := &models.FingerprintSubmission{
Endpoint: input.Endpoint,
StashID: input.StashID,
SceneID: sceneID,
Vote: models.FingerprintVote(input.Vote),
CreatedAt: time.Now(),
}

if err := r.withTxn(ctx, func(ctx context.Context) error {
// Remove any existing submission for this stash ID before creating a new one
if err := r.repository.FingerprintSubmission.Delete(ctx, input.Endpoint, input.StashID); err != nil {
return err
}
return r.repository.FingerprintSubmission.Create(ctx, submission)
}); err != nil {
return false, err
}

return true, nil
}

func (r *mutationResolver) RemoveFingerprintSubmission(ctx context.Context, input RemoveFingerprintInput) (bool, error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
return r.repository.FingerprintSubmission.Delete(ctx, input.Endpoint, input.StashID)
}); err != nil {
return false, err
}

return true, nil
}

func (r *mutationResolver) SubmitFingerprintSubmissions(ctx context.Context, stashBoxEndpoint string) (bool, error) {
b, err := resolveStashBox(nil, &stashBoxEndpoint)
if err != nil {
return false, err
}

var submissions []*models.FingerprintSubmission
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
submissions, err = r.repository.FingerprintSubmission.FindByEndpoint(ctx, stashBoxEndpoint)
return err
}); err != nil {
return false, err
}

if len(submissions) == 0 {
return true, nil
}

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, ids, scene.LoadFiles)
return err
}); err != nil {
return false, err
}

sceneMap := make(map[int]*models.Scene)
for _, s := range scenes {
sceneMap[s.ID] = s
}

client := r.newStashBoxClient(*b)

if len(submissions) > 40 {
// Submit async to avoid timeouts for large batches
go r.submitFingerprintBatch(client, submissions, sceneMap)
} else {
r.submitFingerprintBatch(client, submissions, sceneMap)
}
Comment on lines +304 to +309
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making a couple notes in this area:

  • Is this 40 due to the default page being 40 or was it just a number you liked?

  • This seems to be its own route. Would that mean that this won't cancel/shutdown on server restart and just crash out if a user restarts mid submission batch? Might be a good idea to pass this through the JobManager so it can be tracked like a regular task.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

40 is just a number that seems reasonable. The problem is fingerprints are submitted one by one, so if you have hundreds of them, the browser request times out before they can finish submitting, and then the batch gets cancelled without cleaning up. So past a certain number of fingerprints it's impossible to ever empty the queue.

The goroutine runs until it's done or the server restarts. I agree a task would be more appropriate, but this isn't a permanent solution. Once batch submission support is added to stash-box, we can submit all of them at once without being worried about timeouts, and then we can remove the goroutine.

Here's the relevant PR: stashapp/stash-box#1009


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]
if !ok {
logger.Warnf("Scene %d not found for fingerprint submission, skipping", sub.SceneID)
continue
}

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
}

successfulSubmissions = append(successfulSubmissions, sub)
}

if len(successfulSubmissions) > 0 {
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
}
}
return nil
}); err != nil {
logger.Warnf("Failed to delete fingerprint submissions: %v", err)
}
}
}
17 changes: 17 additions & 0 deletions internal/api/resolver_query_stash_box.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package api

import (
"context"

"github.com/stashapp/stash/pkg/models"
)

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, stashBoxEndpoint)
return err
}); err != nil {
return nil, err
}
return ret, nil
}
47 changes: 47 additions & 0 deletions pkg/models/fingerprint_submission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package models

import (
"context"
"time"
)

type FingerprintVote string

const (
FingerprintVoteValid FingerprintVote = "VALID"
FingerprintVoteInvalid FingerprintVote = "INVALID"
)

func (e FingerprintVote) IsValid() bool {
switch e {
case FingerprintVoteValid, FingerprintVoteInvalid:
return true
}
return false
}

func (e FingerprintVote) String() string {
return string(e)
}

type FingerprintSubmission struct {
Endpoint string `json:"endpoint"`
StashID string `json:"stash_id"`
SceneID int `json:"scene_id"`
Vote FingerprintVote `json:"vote"`
CreatedAt time.Time `json:"created_at"`
}

type FingerprintSubmissionReader interface {
FindByEndpoint(ctx context.Context, endpoint string) ([]*FingerprintSubmission, error)
}

type FingerprintSubmissionWriter interface {
Create(ctx context.Context, newObject *FingerprintSubmission) error
Delete(ctx context.Context, endpoint string, stashID string) error
}

type FingerprintSubmissionReaderWriter interface {
FingerprintSubmissionReader
FingerprintSubmissionWriter
}
27 changes: 14 additions & 13 deletions pkg/models/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,20 @@ type TxnManager interface {
type Repository struct {
TxnManager TxnManager

Blob BlobReader
File FileReaderWriter
Folder FolderReaderWriter
Gallery GalleryReaderWriter
GalleryChapter GalleryChapterReaderWriter
Image ImageReaderWriter
Group GroupReaderWriter
Performer PerformerReaderWriter
Scene SceneReaderWriter
SceneMarker SceneMarkerReaderWriter
Studio StudioReaderWriter
Tag TagReaderWriter
SavedFilter SavedFilterReaderWriter
Blob BlobReader
File FileReaderWriter
Folder FolderReaderWriter
Gallery GalleryReaderWriter
GalleryChapter GalleryChapterReaderWriter
Image ImageReaderWriter
Group GroupReaderWriter
Performer PerformerReaderWriter
Scene SceneReaderWriter
SceneMarker SceneMarkerReaderWriter
Studio StudioReaderWriter
Tag TagReaderWriter
SavedFilter SavedFilterReaderWriter
FingerprintSubmission FingerprintSubmissionReaderWriter
}

func (r *Repository) WithTxn(ctx context.Context, fn txn.TxnFunc) error {
Expand Down
9 changes: 6 additions & 3 deletions pkg/models/stash_box.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package models

type StashBoxFingerprint struct {
Algorithm string `json:"algorithm"`
Hash string `json:"hash"`
Duration int `json:"duration"`
Algorithm string `json:"algorithm"`
Hash string `json:"hash"`
Duration int `json:"duration"`
Reports int `json:"reports"`
UserSubmitted bool `json:"user_submitted"`
UserReported bool `json:"user_reported"`
}

type StashBox struct {
Expand Down
Loading
Loading