mirror of
https://github.com/stashapp/stash.git
synced 2026-05-09 05:05:29 +02:00
WIP
This commit is contained in:
parent
66a445c366
commit
ddd9b67863
5 changed files with 290 additions and 29 deletions
177
pkg/sqlite/fingerprint_submission_test.go
Normal file
177
pkg/sqlite/fingerprint_submission_test.go
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package sqlite_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFingerprintSubmissionCreate(t *testing.T) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
submission := &models.FingerprintSubmission{
|
||||
Endpoint: "https://stashdb.org/graphql",
|
||||
StashID: "test-stash-id-1",
|
||||
SceneID: sceneIDs[sceneIdxWithGallery],
|
||||
Vote: models.FingerprintVoteInvalid,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := db.FingerprintSubmission.Create(ctx, submission)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify it was created
|
||||
found, err := db.FingerprintSubmission.Find(ctx, submission.Endpoint, submission.StashID)
|
||||
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)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestFingerprintSubmissionCreateDuplicate(t *testing.T) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
submission := &models.FingerprintSubmission{
|
||||
Endpoint: "https://stashdb.org/graphql",
|
||||
StashID: "test-stash-id-dup",
|
||||
SceneID: sceneIDs[sceneIdxWithGallery],
|
||||
Vote: models.FingerprintVoteValid,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := db.FingerprintSubmission.Create(ctx, submission)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Creating again with same endpoint+stash_id should not error (ON CONFLICT DO NOTHING)
|
||||
submission2 := &models.FingerprintSubmission{
|
||||
Endpoint: "https://stashdb.org/graphql",
|
||||
StashID: "test-stash-id-dup",
|
||||
SceneID: sceneIDs[sceneIdxWithPerformer],
|
||||
Vote: models.FingerprintVoteInvalid,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err = db.FingerprintSubmission.Create(ctx, submission2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Original should still exist unchanged
|
||||
found, err := db.FingerprintSubmission.Find(ctx, submission.Endpoint, submission.StashID)
|
||||
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)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestFingerprintSubmissionFindByEndpoint(t *testing.T) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
endpoint := "https://test-endpoint.org/graphql"
|
||||
|
||||
// Create multiple submissions for the same endpoint
|
||||
for i := 0; i < 3; i++ {
|
||||
submission := &models.FingerprintSubmission{
|
||||
Endpoint: endpoint,
|
||||
StashID: "stash-id-" + string(rune('a'+i)),
|
||||
SceneID: sceneIDs[sceneIdxWithGallery],
|
||||
Vote: models.FingerprintVoteInvalid,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
err := db.FingerprintSubmission.Create(ctx, submission)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Create one for a different endpoint
|
||||
otherSubmission := &models.FingerprintSubmission{
|
||||
Endpoint: "https://other-endpoint.org/graphql",
|
||||
StashID: "other-stash-id",
|
||||
SceneID: sceneIDs[sceneIdxWithGallery],
|
||||
Vote: models.FingerprintVoteValid,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
err := db.FingerprintSubmission.Create(ctx, otherSubmission)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Find by endpoint should return only the 3
|
||||
found, err := db.FingerprintSubmission.FindByEndpoint(ctx, endpoint)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, found, 3)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestFingerprintSubmissionDelete(t *testing.T) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
submission := &models.FingerprintSubmission{
|
||||
Endpoint: "https://delete-test.org/graphql",
|
||||
StashID: "delete-test-stash-id",
|
||||
SceneID: sceneIDs[sceneIdxWithGallery],
|
||||
Vote: models.FingerprintVoteInvalid,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
err := db.FingerprintSubmission.Create(ctx, submission)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Delete it
|
||||
err = db.FingerprintSubmission.Delete(ctx, submission.Endpoint, submission.StashID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify it's gone
|
||||
found, err := db.FingerprintSubmission.Find(ctx, submission.Endpoint, submission.StashID)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, found)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestFingerprintSubmissionDeleteByEndpoint(t *testing.T) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
endpoint := "https://delete-all-test.org/graphql"
|
||||
|
||||
// Create multiple submissions
|
||||
for i := 0; i < 3; i++ {
|
||||
submission := &models.FingerprintSubmission{
|
||||
Endpoint: endpoint,
|
||||
StashID: "delete-all-stash-id-" + string(rune('a'+i)),
|
||||
SceneID: sceneIDs[sceneIdxWithGallery],
|
||||
Vote: models.FingerprintVoteInvalid,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
err := db.FingerprintSubmission.Create(ctx, submission)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Delete all by endpoint
|
||||
err := db.FingerprintSubmission.DeleteByEndpoint(ctx, endpoint)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify all are gone
|
||||
found, err := db.FingerprintSubmission.FindByEndpoint(ctx, endpoint)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, found, 0)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -211,6 +211,9 @@ fragment ScrapedSceneData on ScrapedScene {
|
|||
hash
|
||||
algorithm
|
||||
duration
|
||||
reports
|
||||
user_submitted
|
||||
user_reported
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -282,6 +285,9 @@ fragment ScrapedStashBoxSceneData on ScrapedScene {
|
|||
hash
|
||||
algorithm
|
||||
duration
|
||||
reports
|
||||
user_submitted
|
||||
user_reported
|
||||
}
|
||||
|
||||
studio {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import {
|
||||
FingerprintVote,
|
||||
initialConfig,
|
||||
|
|
@ -87,7 +87,10 @@ export interface ITaggerContextState {
|
|||
stashBoxSceneId: string,
|
||||
vote: GQL.FingerprintVote
|
||||
) => Promise<void>;
|
||||
isMarkedWrong: (sceneId: string, remoteSceneId: string) => boolean;
|
||||
removeFingerprintSubmission: (
|
||||
stashBoxSceneId: string
|
||||
) => Promise<void>;
|
||||
isReported: (sceneId: string, remoteSceneId: string) => boolean;
|
||||
}
|
||||
|
||||
const dummyFn = () => {
|
||||
|
|
@ -120,7 +123,8 @@ export const TaggerStateContext = React.createContext<ITaggerContextState>({
|
|||
pendingFingerprints: [],
|
||||
saveScene: dummyFn,
|
||||
queueFingerprintSubmission: dummyFn,
|
||||
isMarkedWrong: () => false,
|
||||
removeFingerprintSubmission: dummyFn,
|
||||
isReported: () => false,
|
||||
});
|
||||
|
||||
export type IScrapedScene = GQL.ScrapedScene & { resolved?: boolean };
|
||||
|
|
@ -158,6 +162,7 @@ export const TaggerContext: React.FC = ({ children }) => {
|
|||
|
||||
// Fingerprint submission mutations and query
|
||||
const [queueFingerprintMutation] = GQL.useQueueFingerprintSubmissionMutation();
|
||||
const [removeFingerprintMutation] = GQL.useRemoveFingerprintSubmissionMutation();
|
||||
const [submitFingerprintsMutation] = GQL.useSubmitFingerprintSubmissionsMutation();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -245,7 +250,7 @@ export const TaggerContext: React.FC = ({ children }) => {
|
|||
skip: !endpoint,
|
||||
});
|
||||
|
||||
const getPendingFingerprints = useCallback((): PendingSubmission[] => {
|
||||
const pendingFingerprints = useMemo((): PendingSubmission[] => {
|
||||
if (!pendingData?.pendingFingerprintSubmissions) return [];
|
||||
|
||||
return pendingData.pendingFingerprintSubmissions.map((s) => ({
|
||||
|
|
@ -255,15 +260,14 @@ export const TaggerContext: React.FC = ({ children }) => {
|
|||
}));
|
||||
}, [pendingData]);
|
||||
|
||||
const isMarkedWrong = useCallback((sceneId: string, remoteSceneId: string): boolean => {
|
||||
const pendingFps = getPendingFingerprints();
|
||||
return pendingFps.some(
|
||||
function isReported(sceneId: string, remoteSceneId: string): boolean {
|
||||
return pendingFingerprints.some(
|
||||
(fp) =>
|
||||
fp.sceneId === sceneId &&
|
||||
fp.stashId === remoteSceneId &&
|
||||
fp.vote === GQL.FingerprintVote.Invalid
|
||||
);
|
||||
}, [getPendingFingerprints]);
|
||||
}
|
||||
|
||||
async function submitFingerprints() {
|
||||
if (!endpoint) return;
|
||||
|
|
@ -276,7 +280,7 @@ export const TaggerContext: React.FC = ({ children }) => {
|
|||
},
|
||||
});
|
||||
|
||||
refetchPending();
|
||||
await refetchPending();
|
||||
} catch (err) {
|
||||
Toast.error(err);
|
||||
} finally {
|
||||
|
|
@ -309,6 +313,25 @@ export const TaggerContext: React.FC = ({ children }) => {
|
|||
}
|
||||
}
|
||||
|
||||
async function removeFingerprintSubmission(stashBoxSceneId: string) {
|
||||
if (!endpoint) return;
|
||||
|
||||
try {
|
||||
await removeFingerprintMutation({
|
||||
variables: {
|
||||
input: {
|
||||
endpoint,
|
||||
stash_id: stashBoxSceneId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
refetchPending();
|
||||
} catch (err) {
|
||||
Toast.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function clearSearchResults(sceneID: string) {
|
||||
setSearchResults((current) => {
|
||||
const newSearchResults = { ...current };
|
||||
|
|
@ -974,9 +997,10 @@ export const TaggerContext: React.FC = ({ children }) => {
|
|||
resolveScene,
|
||||
saveScene,
|
||||
submitFingerprints,
|
||||
pendingFingerprints: getPendingFingerprints(),
|
||||
pendingFingerprints,
|
||||
queueFingerprintSubmission,
|
||||
isMarkedWrong,
|
||||
removeFingerprintSubmission,
|
||||
isReported,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
faLink,
|
||||
faPlus,
|
||||
faTriangleExclamation,
|
||||
faUndo,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
|
|
@ -177,6 +178,27 @@ function matchChecksums(
|
|||
return matches;
|
||||
}
|
||||
|
||||
const hasUserReportedFingerprint = (
|
||||
scene: IScrapedScene,
|
||||
stashScene: GQL.SlimSceneDataFragment
|
||||
): boolean => {
|
||||
const checksumMatches = matchChecksums(stashScene, scene.fingerprints ?? []);
|
||||
|
||||
const allPhashes = stashScene.files.reduce(
|
||||
(pv: Pick<GQL.Fingerprint, "type" | "value">[], cv) => {
|
||||
return [...pv, ...cv.fingerprints.filter((f) => f.type === "phash")];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const phashMatches = matchPhashes(allPhashes, scene.fingerprints ?? []);
|
||||
|
||||
return (
|
||||
checksumMatches.some((m) => m.userReported) ||
|
||||
phashMatches.some((m) => m.userReported)
|
||||
);
|
||||
};
|
||||
|
||||
const getFingerprintStatus = (
|
||||
scene: IScrapedScene,
|
||||
stashScene: GQL.SlimSceneDataFragment
|
||||
|
|
@ -262,7 +284,7 @@ const getFingerprintStatus = (
|
|||
)}
|
||||
{checksumMatches.length > 0 && (
|
||||
<div className="font-weight-bold">
|
||||
<SuccessIcon className="mr-2" />
|
||||
<SuccessIcon className="SceneTaggerIcon" />
|
||||
<FormattedMessage
|
||||
id="component_tagger.results.hash_matches"
|
||||
values={{
|
||||
|
|
@ -272,7 +294,7 @@ const getFingerprintStatus = (
|
|||
</div>
|
||||
)}
|
||||
{hasReports && (
|
||||
<div className="text-warning font-weight-bold">
|
||||
<div className="text-danger font-weight-bold">
|
||||
<Icon className="SceneTaggerIcon" icon={faTriangleExclamation} />
|
||||
<FormattedMessage
|
||||
id="component_tagger.results.fp_reported"
|
||||
|
|
@ -281,7 +303,7 @@ const getFingerprintStatus = (
|
|||
</div>
|
||||
)}
|
||||
{hasUserSubmitted && (
|
||||
<div className="text-success">
|
||||
<div className="font-weight-bold">
|
||||
<SuccessIcon className="SceneTaggerIcon" />
|
||||
<FormattedMessage id="component_tagger.results.fp_submitted" />
|
||||
</div>
|
||||
|
|
@ -295,6 +317,7 @@ interface IStashSearchResultProps {
|
|||
stashScene: GQL.SlimSceneDataFragment;
|
||||
index: number;
|
||||
isActive: boolean;
|
||||
onMarkWrong?: () => void;
|
||||
}
|
||||
|
||||
const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||
|
|
@ -302,6 +325,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||
stashScene,
|
||||
index,
|
||||
isActive,
|
||||
onMarkWrong,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
@ -318,7 +342,8 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||
currentSource,
|
||||
saveScene,
|
||||
queueFingerprintSubmission,
|
||||
isMarkedWrong,
|
||||
removeFingerprintSubmission,
|
||||
isReported,
|
||||
} = React.useContext(TaggerStateContext);
|
||||
|
||||
const performerGenders = config.performerGenders || genderList;
|
||||
|
|
@ -516,12 +541,20 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||
async function handleMarkWrong() {
|
||||
if (!scene.remote_site_id) return;
|
||||
await queueFingerprintSubmission(stashScene.id, scene.remote_site_id, GQL.FingerprintVote.Invalid);
|
||||
onMarkWrong?.();
|
||||
}
|
||||
|
||||
const markedWrong = scene.remote_site_id
|
||||
? isMarkedWrong(stashScene.id, scene.remote_site_id)
|
||||
async function handleRemoveReport() {
|
||||
if (!scene.remote_site_id) return;
|
||||
await removeFingerprintSubmission(scene.remote_site_id);
|
||||
}
|
||||
|
||||
const isReportedWrong = scene.remote_site_id
|
||||
? isReported(stashScene.id, scene.remote_site_id)
|
||||
: false;
|
||||
|
||||
const alreadyReported = hasUserReportedFingerprint(scene, stashScene);
|
||||
|
||||
function showPerformerModal(t: GQL.ScrapedPerformer) {
|
||||
createPerformerModal(t, (toCreate) => {
|
||||
if (toCreate) {
|
||||
|
|
@ -909,7 +942,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className={cx(isActive ? "col-lg-6" : "", { "marked-wrong": markedWrong })}>
|
||||
<div className={cx(isActive ? "col-lg-6" : "", { "marked-wrong": isReportedWrong })}>
|
||||
<div className="row mx-0">
|
||||
{maybeRenderCoverImage()}
|
||||
<div className="d-flex flex-column justify-content-center scene-metadata">
|
||||
|
|
@ -919,7 +952,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||
<>
|
||||
{renderStudioDate()}
|
||||
{renderPerformerList()}
|
||||
{markedWrong && (
|
||||
{isReportedWrong && (
|
||||
<Badge variant="danger" className="mt-1">
|
||||
<FormattedMessage id="component_tagger.marked_wrong" />
|
||||
</Badge>
|
||||
|
|
@ -949,16 +982,32 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||
{maybeRenderTagsField()}
|
||||
|
||||
<div className="row no-gutters mt-2 align-items-center justify-content-end">
|
||||
{scene.remote_site_id && (
|
||||
{scene.remote_site_id && !isReportedWrong && (
|
||||
<OperationButton
|
||||
className="mr-2"
|
||||
operation={handleMarkWrong}
|
||||
variant="outline-danger"
|
||||
disabled={markedWrong}
|
||||
disabled={alreadyReported}
|
||||
>
|
||||
<Icon icon={faXmark} />
|
||||
<span className="ml-1">
|
||||
<FormattedMessage id="component_tagger.wrong_match" />
|
||||
{alreadyReported ? (
|
||||
<FormattedMessage id="component_tagger.marked_wrong" />
|
||||
) : (
|
||||
<FormattedMessage id="component_tagger.report_match" />
|
||||
)}
|
||||
</span>
|
||||
</OperationButton>
|
||||
)}
|
||||
{scene.remote_site_id && isReportedWrong && (
|
||||
<OperationButton
|
||||
className="mr-2"
|
||||
operation={handleRemoveReport}
|
||||
variant="danger"
|
||||
>
|
||||
<Icon icon={faUndo} />
|
||||
<span className="ml-1">
|
||||
<FormattedMessage id="component_tagger.undo_report" />
|
||||
</span>
|
||||
</OperationButton>
|
||||
)}
|
||||
|
|
@ -968,16 +1017,16 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isActive && scene.remote_site_id && !markedWrong && (
|
||||
{!isActive && scene.remote_site_id && isReportedWrong && (
|
||||
<div className="col-lg-6">
|
||||
<div className="ml-auto d-flex align-items-center">
|
||||
<OperationButton
|
||||
operation={handleMarkWrong}
|
||||
variant="outline-danger"
|
||||
operation={handleRemoveReport}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
title={intl.formatMessage({ id: "component_tagger.wrong_match" })}
|
||||
title={intl.formatMessage({ id: "component_tagger.undo_report" })}
|
||||
>
|
||||
<Icon icon={faXmark} />
|
||||
<Icon icon={faUndo} />
|
||||
</OperationButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1039,6 +1088,9 @@ export const SceneSearchResults: React.FC<ISceneSearchResults> = ({
|
|||
isActive={i === selectedResult}
|
||||
scene={s}
|
||||
stashScene={target}
|
||||
onMarkWrong={
|
||||
i === selectedResult ? () => setSelectedResult(undefined) : undefined
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@
|
|||
},
|
||||
"temp_disable": "Disable temporarily…",
|
||||
"temp_enable": "Enable temporarily…",
|
||||
"undo": "Undo",
|
||||
"unset": "Unset",
|
||||
"use_default": "Use default",
|
||||
"view_history": "View history",
|
||||
|
|
@ -236,8 +237,9 @@
|
|||
"fp_submitted": "You submitted fingerprints",
|
||||
"unnamed": "Unnamed"
|
||||
},
|
||||
"marked_wrong": "Wrong Match",
|
||||
"wrong_match": "Wrong Match",
|
||||
"marked_wrong": "Reported Wrong",
|
||||
"undo_report": "Undo Report",
|
||||
"report_match": "Report Wrong Match",
|
||||
"verb_add_as_alias": "Add scraped name as alias",
|
||||
"verb_link_existing": "Link to existing",
|
||||
"verb_match_fp": "Match Fingerprints",
|
||||
|
|
|
|||
Loading…
Reference in a new issue