Add report fingerprint functionality (#705)

This commit is contained in:
WithoutPants
2024-11-25 09:34:34 +11:00
committed by GitHub
parent a4c9b6076b
commit f38634babc
17 changed files with 742 additions and 192 deletions

View File

@@ -1,4 +1,5 @@
import { FC } from "react";
import cx from "classnames";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
@@ -7,13 +8,14 @@ interface Props {
className?: string;
color?: string;
title?: string;
variant?: "danger" | "success" | "info" | "warning";
}
const Icon: FC<Props> = ({ icon, className, color, title }) => (
const Icon: FC<Props> = ({ icon, className, color, title, variant }) => (
<FontAwesomeIcon
title={title}
icon={icon}
className={`fa-icon ${className}`}
className={cx("fa-icon", className, { [`text-${variant}`]: variant })}
color={color}
/>
);

View File

@@ -35,7 +35,9 @@ fragment SceneFragment on Scene {
algorithm
duration
submissions
reports
user_submitted
user_reported
created
updated
}

View File

@@ -6,7 +6,7 @@ mutation UnmatchFingerprint(
) {
unmatchFingerprint: submitFingerprint(
input: {
unmatch: true
vote: REMOVE
scene_id: $scene_id
fingerprint: { hash: $hash, algorithm: $algorithm, duration: $duration }
}

View File

@@ -293,8 +293,14 @@ export type Fingerprint = {
created: Scalars["Time"]["output"];
duration: Scalars["Int"]["output"];
hash: Scalars["String"]["output"];
/** number of times this fingerprint has been reported */
reports: Scalars["Int"]["output"];
/** number of times this fingerprint has been submitted (excluding reports) */
submissions: Scalars["Int"]["output"];
updated: Scalars["Time"]["output"];
/** true if the current user reported this fingerprint */
user_reported: Scalars["Boolean"]["output"];
/** true if the current user submitted this fingerprint */
user_submitted: Scalars["Boolean"]["output"];
};
@@ -332,9 +338,20 @@ export type FingerprintQueryInput = {
export type FingerprintSubmission = {
fingerprint: FingerprintInput;
scene_id: Scalars["ID"]["input"];
/** @deprecated Use `vote` with REMOVE instead */
unmatch?: InputMaybe<Scalars["Boolean"]["input"]>;
vote?: InputMaybe<FingerprintSubmissionType>;
};
export enum FingerprintSubmissionType {
/** Report as invalid */
INVALID = "INVALID",
/** Remove vote */
REMOVE = "REMOVE",
/** Positive vote */
VALID = "VALID",
}
export type FuzzyDate = {
__typename: "FuzzyDate";
accuracy: DateAccuracyEnum;
@@ -2076,7 +2093,9 @@ export type EditFragment = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -2852,7 +2871,9 @@ export type EditFragment = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -3054,7 +3075,9 @@ export type SceneFragment = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -3418,7 +3441,9 @@ export type ApplyEditMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -4249,7 +4274,9 @@ export type ApplyEditMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -4645,7 +4672,9 @@ export type PerformerEditMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -5476,7 +5505,9 @@ export type PerformerEditMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -5682,7 +5713,9 @@ export type PerformerEditUpdateMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -6513,7 +6546,9 @@ export type PerformerEditUpdateMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -6763,7 +6798,9 @@ export type SceneEditMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -7594,7 +7631,9 @@ export type SceneEditMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -7800,7 +7839,9 @@ export type SceneEditUpdateMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -8631,7 +8672,9 @@ export type SceneEditUpdateMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -8836,7 +8879,9 @@ export type StudioEditMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -9667,7 +9712,9 @@ export type StudioEditMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -9873,7 +9920,9 @@ export type StudioEditUpdateMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -10704,7 +10753,9 @@ export type StudioEditUpdateMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -10909,7 +10960,9 @@ export type TagEditMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -11740,7 +11793,9 @@ export type TagEditMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -11946,7 +12001,9 @@ export type TagEditUpdateMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -12777,7 +12834,9 @@ export type TagEditUpdateMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -13126,7 +13185,9 @@ export type VoteMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -13957,7 +14018,9 @@ export type VoteMutation = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -14422,7 +14485,9 @@ export type EditQuery = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -15253,7 +15318,9 @@ export type EditQuery = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -15452,7 +15519,9 @@ export type EditUpdateQuery = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -15903,7 +15972,9 @@ export type EditsQuery = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -16754,7 +16825,9 @@ export type EditsQuery = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -17105,7 +17178,9 @@ export type QueryExistingSceneQuery = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -17256,7 +17331,9 @@ export type QueryExistingSceneQuery = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -18107,7 +18184,9 @@ export type QueryExistingSceneQuery = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -18225,7 +18304,9 @@ export type SceneQuery = {
algorithm: FingerprintAlgorithm;
duration: number;
submissions: number;
reports: number;
user_submitted: boolean;
user_reported: boolean;
created: string;
updated: string;
}>;
@@ -19272,10 +19353,15 @@ export const SceneFragmentDoc = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -20882,10 +20968,15 @@ export const EditFragmentDoc = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -22126,10 +22217,15 @@ export const ApplyEditDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -24609,10 +24705,15 @@ export const PerformerEditDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -26220,10 +26321,15 @@ export const PerformerEditUpdateDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -28017,10 +28123,15 @@ export const SceneEditDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -29625,10 +29736,15 @@ export const SceneEditUpdateDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -31220,10 +31336,15 @@ export const StudioEditDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -32828,10 +32949,15 @@ export const StudioEditUpdateDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -34423,10 +34549,15 @@ export const TagEditDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -36031,10 +36162,15 @@ export const TagEditUpdateDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -37281,8 +37417,8 @@ export const UnmatchFingerprintDocument = {
fields: [
{
kind: "ObjectField",
name: { kind: "Name", value: "unmatch" },
value: { kind: "BooleanValue", value: true },
name: { kind: "Name", value: "vote" },
value: { kind: "EnumValue", value: "REMOVE" },
},
{
kind: "ObjectField",
@@ -38302,10 +38438,15 @@ export const VoteDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -40749,10 +40890,15 @@ export const EditDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -42920,10 +43066,15 @@ export const EditUpdateDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -43390,10 +43541,15 @@ export const EditsDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -45865,10 +46021,15 @@ export const QueryExistingSceneDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],
@@ -47250,10 +47411,15 @@ export const SceneDocument = {
{ kind: "Field", name: { kind: "Name", value: "algorithm" } },
{ kind: "Field", name: { kind: "Name", value: "duration" } },
{ kind: "Field", name: { kind: "Name", value: "submissions" } },
{ kind: "Field", name: { kind: "Name", value: "reports" } },
{
kind: "Field",
name: { kind: "Name", value: "user_submitted" },
},
{
kind: "Field",
name: { kind: "Name", value: "user_reported" },
},
{ kind: "Field", name: { kind: "Name", value: "created" } },
{ kind: "Field", name: { kind: "Name", value: "updated" } },
],

View File

@@ -1,20 +1,13 @@
import { FC, useContext } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Button, Card, Tabs, Tab, Table } from "react-bootstrap";
import {
faCheckCircle,
faTimesCircle,
faSpinner,
} from "@fortawesome/free-solid-svg-icons";
import { Button, Card, Tabs, Tab } from "react-bootstrap";
import {
usePendingEditsCount,
TargetTypeEnum,
useUnmatchFingerprint,
SceneFragment as Scene,
} from "src/graphql";
import AuthContext from "src/AuthContext";
import { useToast } from "src/hooks";
import {
canEdit,
tagHref,
@@ -22,26 +15,15 @@ import {
studioHref,
createHref,
formatDuration,
formatDateTime,
formatPendingEdits,
getUrlBySite,
compareByName,
} from "src/utils";
import {
ROUTE_SCENE_EDIT,
ROUTE_SCENES,
ROUTE_SCENE_DELETE,
} from "src/constants/route";
import {
GenderIcon,
TagLink,
PerformerName,
Icon,
} from "src/components/fragments";
import { ROUTE_SCENE_EDIT, ROUTE_SCENE_DELETE } from "src/constants/route";
import { GenderIcon, TagLink, PerformerName } from "src/components/fragments";
import { EditList, URLList } from "src/components/list";
import Image from "src/components/image";
type Fingerprint = NonNullable<Scene["fingerprints"][number]>;
import { FingerprintTable } from "./components/fingerprints";
const DEFAULT_TAB = "description";
@@ -54,9 +36,6 @@ const SceneComponent: FC<Props> = ({ scene }) => {
const location = useLocation();
const activeTab = location.hash?.slice(1) || DEFAULT_TAB;
const auth = useContext(AuthContext);
const addToast = useToast();
const [unmatchFingerprint, { loading: unmatching }] = useUnmatchFingerprint();
const { data: editData } = usePendingEditsCount({
type: TargetTypeEnum.SCENE,
@@ -83,72 +62,6 @@ const SceneComponent: FC<Props> = ({ scene }) => {
})
.map((p, index) => (index % 2 === 2 ? [" • ", p] : p));
async function handleFingerprintUnmatch(fingerprint: Fingerprint) {
if (unmatching) return;
const { data } = await unmatchFingerprint({
variables: {
scene_id: scene.id,
algorithm: fingerprint.algorithm,
hash: fingerprint.hash,
duration: fingerprint.duration,
},
});
const success = data?.unmatchFingerprint;
addToast({
variant: success ? "success" : "danger",
content: `${
success ? "Removed" : "Failed to remove"
} fingerprint submission`,
});
}
function maybeRenderSubmitted(fingerprint: Fingerprint) {
if (fingerprint.user_submitted) {
return (
<Button
className="user-submitted"
title="Submitted by you - click to remove submission"
onKeyDown={() => handleFingerprintUnmatch(fingerprint)}
onClick={() => handleFingerprintUnmatch(fingerprint)}
variant="link"
>
{!unmatching ? (
<>
<Icon icon={faCheckCircle} />
<Icon icon={faTimesCircle} />
</>
) : (
<Icon icon={faSpinner} className="fa-spin" />
)}
</Button>
);
}
}
const fingerprints = scene.fingerprints.map((fingerprint) => (
<tr key={fingerprint.hash}>
<td>{fingerprint.algorithm}</td>
<td className="font-monospace">
<Link
to={`${createHref(ROUTE_SCENES)}?fingerprint=${fingerprint.hash}`}
>
{fingerprint.hash}
</Link>
</td>
<td>
<span title={`${fingerprint.duration}s`}>
{formatDuration(fingerprint.duration)}
</span>
</td>
<td>
{fingerprint.submissions}
{maybeRenderSubmitted(fingerprint)}
</td>
<td>{formatDateTime(fingerprint.created)}</td>
<td>{formatDateTime(fingerprint.updated)}</td>
</tr>
));
const tags = [...scene.tags].sort(compareByName).map((tag) => (
<li key={tag.name}>
<TagLink
@@ -262,38 +175,7 @@ const SceneComponent: FC<Props> = ({ scene }) => {
)}
</Tab>
<Tab eventKey="fingerprints" title="Fingerprints" mountOnEnter={false}>
<div className="scene-fingerprints my-4">
<h4>Fingerprints:</h4>
{fingerprints.length === 0 ? (
<h6>No fingerprints found for this scene.</h6>
) : (
<Table striped variant="dark">
<thead>
<tr>
<td>
<b>Algorithm</b>
</td>
<td>
<b>Hash</b>
</td>
<td>
<b>Duration</b>
</td>
<td>
<b>Submissions</b>
</td>
<td>
<b>First Added</b>
</td>
<td>
<b>Last Added</b>
</td>
</tr>
</thead>
<tbody>{fingerprints}</tbody>
</Table>
)}
</div>
<FingerprintTable scene={scene} />
</Tab>
<Tab eventKey="links" title="Links">
<URLList urls={scene.urls} />

View File

@@ -0,0 +1,146 @@
import { FC } from "react";
import { Link } from "react-router-dom";
import { Button, Table } from "react-bootstrap";
import {
faCheckCircle,
faTimesCircle,
faSpinner,
faTriangleExclamation,
} from "@fortawesome/free-solid-svg-icons";
import { Fingerprint, useUnmatchFingerprint } from "src/graphql";
import { useToast } from "src/hooks";
import { createHref, formatDate, formatDuration } from "src/utils";
import { ROUTE_SCENES } from "src/constants/route";
import { Icon } from "src/components/fragments";
interface Props {
scene: {
id: string;
fingerprints: Fingerprint[];
};
}
type MatchType = "submission" | "report";
export const FingerprintTable: FC<Props> = ({ scene }) => {
const addToast = useToast();
const [unmatchFingerprint, { loading: unmatching }] = useUnmatchFingerprint();
const handleFingerprintUnmatch = async (
fingerprint: Fingerprint,
type: MatchType,
) => {
if (unmatching) return;
const { data } = await unmatchFingerprint({
variables: {
scene_id: scene.id,
algorithm: fingerprint.algorithm,
hash: fingerprint.hash,
duration: fingerprint.duration,
},
});
const success = data?.unmatchFingerprint;
addToast({
variant: success ? "success" : "danger",
content: `${
success ? "Removed" : "Failed to remove"
} fingerprint ${type}`,
});
};
const renderUnmatch = (fingerprint: Fingerprint, type: MatchType) => (
<Button
className="user-submitted"
title={`Remove ${type}`}
onKeyDown={() => handleFingerprintUnmatch(fingerprint, type)}
onClick={() => handleFingerprintUnmatch(fingerprint, type)}
variant="link"
disabled={unmatching}
>
{!unmatching ? (
<>
<Icon icon={faCheckCircle} />
<Icon icon={faTimesCircle} />
</>
) : (
<Icon icon={faSpinner} className="fa-spin" />
)}
</Button>
);
return (
<div className="scene-fingerprints my-4">
<h4>Fingerprints:</h4>
{scene.fingerprints.length === 0 ? (
<h6>No fingerprints found for this scene.</h6>
) : (
<Table striped variant="dark">
<thead>
<tr>
<td>
<b>Algorithm</b>
</td>
<td>
<b>Hash</b>
</td>
<td>
<b>Duration</b>
</td>
<td>
<b>Submissions</b>
</td>
<td>
<b>Reports</b>
</td>
<td>
<b>First Added</b>
</td>
<td>
<b>Last Added</b>
</td>
</tr>
</thead>
<tbody>
{scene.fingerprints.map((fingerprint) => (
<tr key={fingerprint.hash}>
<td>{fingerprint.algorithm}</td>
<td className="font-monospace">
<Link
to={`${createHref(ROUTE_SCENES)}?fingerprint=${fingerprint.hash}`}
>
{fingerprint.hash}
</Link>
</td>
<td>
<span title={`${fingerprint.duration}s`}>
{formatDuration(fingerprint.duration)}
</span>
</td>
<td>
{fingerprint.submissions}
{fingerprint.user_submitted &&
renderUnmatch(fingerprint, "submission")}
</td>
<td>
{fingerprint.reports > 0 && (
<>
{fingerprint.reports}{" "}
<Icon icon={faTriangleExclamation} variant="danger" />
{fingerprint.user_reported &&
renderUnmatch(fingerprint, "report")}
</>
)}
</td>
<td>{formatDate(fingerprint.created)}</td>
<td>{formatDate(fingerprint.updated)}</td>
</tr>
))}
</tbody>
</Table>
)}
</div>
);
};

View File

@@ -13,6 +13,22 @@ export const formatDateTime = (dateTime: Date | string, utc = false) => {
})}`;
};
export const formatDate = (dateTime: Date | string, utc = false) => {
const timeZone = utc ? "UTC" : undefined;
const date = dateTime instanceof Date ? dateTime : new Date(dateTime);
return date.toLocaleString("en-us", {
month: "short",
year: "numeric",
day: "numeric",
timeZone,
});
};
export const formatISODate = (dateTime: Date | string) => {
const date = dateTime instanceof Date ? dateTime : new Date(dateTime);
return date.toISOString().slice(0, 10);
};
export const isValidDate = (date?: string) => !date || isValid(parseISO(date));
export const dateWithinRange = (

View File

@@ -22,14 +22,29 @@ enum FavoriteFilter {
ALL
}
enum FingerprintSubmissionType {
"Positive vote"
VALID
"Report as invalid"
INVALID
"Remove vote"
REMOVE
}
type Fingerprint {
hash: String!
algorithm: FingerprintAlgorithm!
duration: Int!
"number of times this fingerprint has been submitted (excluding reports)"
submissions: Int!
"number of times this fingerprint has been reported"
reports: Int!
created: Time!
updated: Time!
"true if the current user submitted this fingerprint"
user_submitted: Boolean!
"true if the current user reported this fingerprint"
user_reported: Boolean!
}
type DraftFingerprint {
@@ -64,7 +79,8 @@ input FingerprintQueryInput {
input FingerprintSubmission {
scene_id: ID!
fingerprint: FingerprintInput!
unmatch: Boolean
unmatch: Boolean @deprecated(reason: "Use `vote` with REMOVE instead")
vote: FingerprintSubmissionType = VALID
}
type Scene {

View File

@@ -4,7 +4,7 @@ import (
"github.com/jmoiron/sqlx"
)
var appSchemaVersion uint = 39
var appSchemaVersion uint = 40
var databaseProviders map[string]databaseProvider

View File

@@ -0,0 +1,2 @@
ALTER TABLE "scene_fingerprints"
ADD COLUMN "vote" SMALLINT NOT NULL DEFAULT 1 CHECK (vote = -1 OR vote = 1);

View File

@@ -142,8 +142,10 @@ type ComplexityRoot struct {
Created func(childComplexity int) int
Duration func(childComplexity int) int
Hash func(childComplexity int) int
Reports func(childComplexity int) int
Submissions func(childComplexity int) int
Updated func(childComplexity int) int
UserReported func(childComplexity int) int
UserSubmitted func(childComplexity int) int
}
@@ -1244,6 +1246,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Fingerprint.Hash(childComplexity), true
case "Fingerprint.reports":
if e.complexity.Fingerprint.Reports == nil {
break
}
return e.complexity.Fingerprint.Reports(childComplexity), true
case "Fingerprint.submissions":
if e.complexity.Fingerprint.Submissions == nil {
break
@@ -1258,6 +1267,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Fingerprint.Updated(childComplexity), true
case "Fingerprint.user_reported":
if e.complexity.Fingerprint.UserReported == nil {
break
}
return e.complexity.Fingerprint.UserReported(childComplexity), true
case "Fingerprint.user_submitted":
if e.complexity.Fingerprint.UserSubmitted == nil {
break
@@ -5003,14 +5019,29 @@ enum FavoriteFilter {
ALL
}
enum FingerprintSubmissionType {
"Positive vote"
VALID
"Report as invalid"
INVALID
"Remove vote"
REMOVE
}
type Fingerprint {
hash: String!
algorithm: FingerprintAlgorithm!
duration: Int!
"number of times this fingerprint has been submitted (excluding reports)"
submissions: Int!
"number of times this fingerprint has been reported"
reports: Int!
created: Time!
updated: Time!
"true if the current user submitted this fingerprint"
user_submitted: Boolean!
"true if the current user reported this fingerprint"
user_reported: Boolean!
}
type DraftFingerprint {
@@ -5045,7 +5076,8 @@ input FingerprintQueryInput {
input FingerprintSubmission {
scene_id: ID!
fingerprint: FingerprintInput!
unmatch: Boolean
unmatch: Boolean @deprecated(reason: "Use ` + "`" + `vote` + "`" + ` with REMOVE instead")
vote: FingerprintSubmissionType = VALID
}
type Scene {
@@ -10931,6 +10963,50 @@ func (ec *executionContext) fieldContext_Fingerprint_submissions(_ context.Conte
return fc, nil
}
func (ec *executionContext) _Fingerprint_reports(ctx context.Context, field graphql.CollectedField, obj *Fingerprint) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Fingerprint_reports(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Reports, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Fingerprint_reports(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Fingerprint",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Int does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _Fingerprint_created(ctx context.Context, field graphql.CollectedField, obj *Fingerprint) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Fingerprint_created(ctx, field)
if err != nil {
@@ -11063,6 +11139,50 @@ func (ec *executionContext) fieldContext_Fingerprint_user_submitted(_ context.Co
return fc, nil
}
func (ec *executionContext) _Fingerprint_user_reported(ctx context.Context, field graphql.CollectedField, obj *Fingerprint) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Fingerprint_user_reported(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.UserReported, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Fingerprint_user_reported(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Fingerprint",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type Boolean does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _FuzzyDate_date(ctx context.Context, field graphql.CollectedField, obj *FuzzyDate) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_FuzzyDate_date(ctx, field)
if err != nil {
@@ -25704,12 +25824,16 @@ func (ec *executionContext) fieldContext_Scene_fingerprints(ctx context.Context,
return ec.fieldContext_Fingerprint_duration(ctx, field)
case "submissions":
return ec.fieldContext_Fingerprint_submissions(ctx, field)
case "reports":
return ec.fieldContext_Fingerprint_reports(ctx, field)
case "created":
return ec.fieldContext_Fingerprint_created(ctx, field)
case "updated":
return ec.fieldContext_Fingerprint_updated(ctx, field)
case "user_submitted":
return ec.fieldContext_Fingerprint_user_submitted(ctx, field)
case "user_reported":
return ec.fieldContext_Fingerprint_user_reported(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Fingerprint", field.Name)
},
@@ -27245,12 +27369,16 @@ func (ec *executionContext) fieldContext_SceneEdit_added_fingerprints(_ context.
return ec.fieldContext_Fingerprint_duration(ctx, field)
case "submissions":
return ec.fieldContext_Fingerprint_submissions(ctx, field)
case "reports":
return ec.fieldContext_Fingerprint_reports(ctx, field)
case "created":
return ec.fieldContext_Fingerprint_created(ctx, field)
case "updated":
return ec.fieldContext_Fingerprint_updated(ctx, field)
case "user_submitted":
return ec.fieldContext_Fingerprint_user_submitted(ctx, field)
case "user_reported":
return ec.fieldContext_Fingerprint_user_reported(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Fingerprint", field.Name)
},
@@ -27302,12 +27430,16 @@ func (ec *executionContext) fieldContext_SceneEdit_removed_fingerprints(_ contex
return ec.fieldContext_Fingerprint_duration(ctx, field)
case "submissions":
return ec.fieldContext_Fingerprint_submissions(ctx, field)
case "reports":
return ec.fieldContext_Fingerprint_reports(ctx, field)
case "created":
return ec.fieldContext_Fingerprint_created(ctx, field)
case "updated":
return ec.fieldContext_Fingerprint_updated(ctx, field)
case "user_submitted":
return ec.fieldContext_Fingerprint_user_submitted(ctx, field)
case "user_reported":
return ec.fieldContext_Fingerprint_user_reported(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Fingerprint", field.Name)
},
@@ -27746,12 +27878,16 @@ func (ec *executionContext) fieldContext_SceneEdit_fingerprints(_ context.Contex
return ec.fieldContext_Fingerprint_duration(ctx, field)
case "submissions":
return ec.fieldContext_Fingerprint_submissions(ctx, field)
case "reports":
return ec.fieldContext_Fingerprint_reports(ctx, field)
case "created":
return ec.fieldContext_Fingerprint_created(ctx, field)
case "updated":
return ec.fieldContext_Fingerprint_updated(ctx, field)
case "user_submitted":
return ec.fieldContext_Fingerprint_user_submitted(ctx, field)
case "user_reported":
return ec.fieldContext_Fingerprint_user_reported(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Fingerprint", field.Name)
},
@@ -34492,7 +34628,11 @@ func (ec *executionContext) unmarshalInputFingerprintSubmission(ctx context.Cont
asMap[k] = v
}
fieldsInOrder := [...]string{"scene_id", "fingerprint", "unmatch"}
if _, present := asMap["vote"]; !present {
asMap["vote"] = "VALID"
}
fieldsInOrder := [...]string{"scene_id", "fingerprint", "unmatch", "vote"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
@@ -34520,6 +34660,13 @@ func (ec *executionContext) unmarshalInputFingerprintSubmission(ctx context.Cont
return it, err
}
it.Unmatch = data
case "vote":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("vote"))
data, err := ec.unmarshalOFingerprintSubmissionType2ᚖgithubᚗcomᚋstashappᚋstashᚑboxᚋpkgᚋmodelsᚐFingerprintSubmissionType(ctx, v)
if err != nil {
return it, err
}
it.Vote = data
}
}
@@ -39492,6 +39639,11 @@ func (ec *executionContext) _Fingerprint(ctx context.Context, sel ast.SelectionS
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "reports":
out.Values[i] = ec._Fingerprint_reports(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "created":
out.Values[i] = ec._Fingerprint_created(ctx, field, obj)
if out.Values[i] == graphql.Null {
@@ -39507,6 +39659,11 @@ func (ec *executionContext) _Fingerprint(ctx context.Context, sel ast.SelectionS
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "user_reported":
out.Values[i] = ec._Fingerprint_user_reported(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -49785,6 +49942,22 @@ func (ec *executionContext) unmarshalOFingerprintInput2ᚕᚖgithubᚗcomᚋstas
return res, nil
}
func (ec *executionContext) unmarshalOFingerprintSubmissionType2ᚖgithubᚗcomᚋstashappᚋstashᚑboxᚋpkgᚋmodelsᚐFingerprintSubmissionType(ctx context.Context, v interface{}) (*FingerprintSubmissionType, error) {
if v == nil {
return nil, nil
}
var res = new(FingerprintSubmissionType)
err := res.UnmarshalGQL(v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalOFingerprintSubmissionType2ᚖgithubᚗcomᚋstashappᚋstashᚑboxᚋpkgᚋmodelsᚐFingerprintSubmissionType(ctx context.Context, sel ast.SelectionSet, v *FingerprintSubmissionType) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return v
}
func (ec *executionContext) marshalOFuzzyDate2ᚖgithubᚗcomᚋstashappᚋstashᚑboxᚋpkgᚋmodelsᚐFuzzyDate(ctx context.Context, sel ast.SelectionSet, v *FuzzyDate) graphql.Marshaler {
if v == nil {
return graphql.Null

View File

@@ -137,13 +137,19 @@ type EyeColorCriterionInput struct {
}
type Fingerprint struct {
Hash string `json:"hash"`
Algorithm FingerprintAlgorithm `json:"algorithm"`
Duration int `json:"duration"`
Submissions int `json:"submissions"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UserSubmitted bool `json:"user_submitted"`
Hash string `json:"hash"`
Algorithm FingerprintAlgorithm `json:"algorithm"`
Duration int `json:"duration"`
// number of times this fingerprint has been submitted (excluding reports)
Submissions int `json:"submissions"`
// number of times this fingerprint has been reported
Reports int `json:"reports"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
// true if the current user submitted this fingerprint
UserSubmitted bool `json:"user_submitted"`
// true if the current user reported this fingerprint
UserReported bool `json:"user_reported"`
}
type FingerprintEditInput struct {
@@ -170,9 +176,10 @@ type FingerprintQueryInput struct {
}
type FingerprintSubmission struct {
SceneID uuid.UUID `json:"scene_id"`
Fingerprint *FingerprintInput `json:"fingerprint"`
Unmatch *bool `json:"unmatch,omitempty"`
SceneID uuid.UUID `json:"scene_id"`
Fingerprint *FingerprintInput `json:"fingerprint"`
Unmatch *bool `json:"unmatch,omitempty"`
Vote *FingerprintSubmissionType `json:"vote,omitempty"`
}
type FuzzyDate struct {
@@ -1251,6 +1258,52 @@ func (e FingerprintAlgorithm) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type FingerprintSubmissionType string
const (
// Positive vote
FingerprintSubmissionTypeValid FingerprintSubmissionType = "VALID"
// Report as invalid
FingerprintSubmissionTypeInvalid FingerprintSubmissionType = "INVALID"
// Remove vote
FingerprintSubmissionTypeRemove FingerprintSubmissionType = "REMOVE"
)
var AllFingerprintSubmissionType = []FingerprintSubmissionType{
FingerprintSubmissionTypeValid,
FingerprintSubmissionTypeInvalid,
FingerprintSubmissionTypeRemove,
}
func (e FingerprintSubmissionType) IsValid() bool {
switch e {
case FingerprintSubmissionTypeValid, FingerprintSubmissionTypeInvalid, FingerprintSubmissionTypeRemove:
return true
}
return false
}
func (e FingerprintSubmissionType) String() string {
return string(e)
}
func (e *FingerprintSubmissionType) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = FingerprintSubmissionType(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid FingerprintSubmissionType", str)
}
return nil
}
func (e FingerprintSubmissionType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type GenderEnum string
const (

View File

@@ -105,6 +105,7 @@ type SceneFingerprint struct {
Algorithm string `db:"algorithm" json:"algorithm"`
Duration int `db:"duration" json:"duration"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Vote int `db:"vote" json:"vote"`
}
type SceneFingerprints []*SceneFingerprint
@@ -125,32 +126,6 @@ func (f *SceneFingerprints) Add(o interface{}) {
*f = append(*f, o.(*SceneFingerprint))
}
type DBSceneFingerprint struct {
SceneID uuid.UUID `db:"scene_id" json:"scene_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
FingerprintID int `db:"fingerprint_id" json:"fingerprint_id"`
Duration int `db:"duration" json:"duration"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
type DBSceneFingerprints []*DBSceneFingerprint
func (f DBSceneFingerprints) Each(fn func(interface{})) {
for _, v := range f {
fn(*v)
}
}
func (f DBSceneFingerprints) EachPtr(fn func(interface{})) {
for _, v := range f {
fn(v)
}
}
func (f *DBSceneFingerprints) Add(o interface{}) {
*f = append(*f, o.(*DBSceneFingerprint))
}
func CreateSceneFingerprints(sceneID uuid.UUID, fingerprints []*FingerprintEditInput) SceneFingerprints {
var ret SceneFingerprints
@@ -172,7 +147,7 @@ func CreateSceneFingerprints(sceneID uuid.UUID, fingerprints []*FingerprintEditI
return ret
}
func CreateSubmittedSceneFingerprints(sceneID uuid.UUID, fingerprints []*FingerprintInput) SceneFingerprints {
func CreateSubmittedSceneFingerprints(sceneID uuid.UUID, fingerprints []*FingerprintInput, vote int) SceneFingerprints {
var ret SceneFingerprints
for _, fingerprint := range fingerprints {
@@ -184,6 +159,7 @@ func CreateSubmittedSceneFingerprints(sceneID uuid.UUID, fingerprints []*Fingerp
Hash: fingerprint.Hash,
Algorithm: fingerprint.Algorithm.String(),
Duration: fingerprint.Duration,
Vote: vote,
})
}
}

View File

@@ -9,7 +9,7 @@ type SceneRepo interface {
SoftDelete(scene Scene) (*Scene, error)
CreateURLs(newJoins SceneURLs) error
UpdateURLs(scene uuid.UUID, updatedJoins SceneURLs) error
CreateFingerprints(newJoins SceneFingerprints) error
CreateOrReplaceFingerprints(newJoins SceneFingerprints) error
UpdateFingerprints(sceneID uuid.UUID, updatedJoins SceneFingerprints) error
DestroyFingerprints(sceneID uuid.UUID, toDelete SceneFingerprints) error
Find(id uuid.UUID) (*Scene, error)
@@ -27,6 +27,7 @@ type SceneRepo interface {
// GetAllFingerprints returns fingerprints for each of the scene ids provided.
// currentUserID is used to populate the UserSubmitted field.
GetAllFingerprints(currentUserID uuid.UUID, ids []uuid.UUID, onlySubmitted bool) ([][]*Fingerprint, []error)
SubmittedHashExists(sceneID uuid.UUID, hash string, algorithm FingerprintAlgorithm) (bool, error)
GetPerformers(id uuid.UUID) (PerformersScenes, error)
GetAllAppearances(ids []uuid.UUID) ([]PerformersScenes, []error)
GetURLs(id uuid.UUID) ([]*URL, error)

View File

@@ -48,7 +48,7 @@ func Create(ctx context.Context, fac models.Repo, input models.SceneCreateInput)
}
sceneFingerprints := models.CreateSceneFingerprints(scene.ID, input.Fingerprints)
if err := qb.CreateFingerprints(sceneFingerprints); err != nil {
if err := qb.CreateOrReplaceFingerprints(sceneFingerprints); err != nil {
return nil, err
}
@@ -232,6 +232,17 @@ func Destroy(fac models.Repo, input models.SceneDestroyInput) (bool, error) {
return true, nil
}
func submissionTypeToInt(t models.FingerprintSubmissionType) int {
switch t {
case models.FingerprintSubmissionTypeValid:
return 1
case models.FingerprintSubmissionTypeInvalid:
return -1
default:
return 0
}
}
func SubmitFingerprint(ctx context.Context, fac models.Repo, input models.FingerprintSubmission) (bool, error) {
qb := fac.Scene()
@@ -257,11 +268,34 @@ func SubmitFingerprint(ctx context.Context, fac models.Repo, input models.Finger
input.Fingerprint.UserIds = []uuid.UUID{currentUserID}
}
sceneFingerprint := models.CreateSubmittedSceneFingerprints(scene.ID, []*models.FingerprintInput{input.Fingerprint})
// set the default vote
vote := models.FingerprintSubmissionTypeValid
if input.Vote != nil {
vote = *input.Vote
}
if input.Unmatch == nil || !*input.Unmatch {
// if the user is reporting a fingerprint, ensure that the fingerprint has at least one submission
if vote == models.FingerprintSubmissionTypeInvalid {
submissionExists, err := qb.SubmittedHashExists(input.SceneID, input.Fingerprint.Hash, input.Fingerprint.Algorithm)
if err != nil {
return false, err
}
if !submissionExists {
return false, errors.New("fingerprint has no submissions")
}
}
voteInt := submissionTypeToInt(vote)
sceneFingerprint := models.CreateSubmittedSceneFingerprints(scene.ID, []*models.FingerprintInput{input.Fingerprint}, voteInt)
// vote == 0 means the user is unmatching the fingerprint
// Unmatch is the deprecated field, but we still need to support it
unmatch := vote == models.FingerprintSubmissionTypeRemove || (input.Unmatch != nil && *input.Unmatch)
if !unmatch {
// set the new fingerprints
if err := qb.CreateFingerprints(sceneFingerprint); err != nil {
if err := qb.CreateOrReplaceFingerprints(sceneFingerprint); err != nil {
return false, err
}
} else {

34
pkg/sqlx/fingerprints.go Normal file
View File

@@ -0,0 +1,34 @@
package sqlx
import (
"time"
"github.com/gofrs/uuid"
)
type dbSceneFingerprint struct {
SceneID uuid.UUID `db:"scene_id" json:"scene_id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
FingerprintID int `db:"fingerprint_id" json:"fingerprint_id"`
Duration int `db:"duration" json:"duration"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
Vote int `db:"vote" json:"vote"`
}
type dbSceneFingerprints []*dbSceneFingerprint
func (f dbSceneFingerprints) Each(fn func(interface{})) {
for _, v := range f {
fn(*v)
}
}
func (f dbSceneFingerprints) EachPtr(fn func(interface{})) {
for _, v := range f {
fn(v)
}
}
func (f *dbSceneFingerprints) Add(o interface{}) {
*f = append(*f, o.(*dbSceneFingerprint))
}

View File

@@ -27,7 +27,7 @@ var (
})
sceneFingerprintTable = newTableJoin(sceneTable, "scene_fingerprints", sceneJoinKey, func() interface{} {
return &models.DBSceneFingerprint{}
return &dbSceneFingerprint{}
})
sceneURLTable = newTableJoin(sceneTable, "scene_urls", sceneJoinKey, func() interface{} {
@@ -79,21 +79,27 @@ func (qb *sceneQueryBuilder) UpdateURLs(scene uuid.UUID, updatedJoins models.Sce
return qb.dbi.ReplaceJoins(sceneURLTable, scene, &updatedJoins)
}
func (qb *sceneQueryBuilder) CreateFingerprints(sceneFingerprints models.SceneFingerprints) error {
conflictHandling := `ON CONFLICT DO NOTHING`
func (qb *sceneQueryBuilder) CreateOrReplaceFingerprints(sceneFingerprints models.SceneFingerprints) error {
conflictHandling := `
ON CONFLICT ON CONSTRAINT scene_fingerprints_scene_id_fingerprint_id_user_id_key
DO UPDATE SET
duration = EXCLUDED.duration,
vote = EXCLUDED.vote
`
var fingerprints models.DBSceneFingerprints
var fingerprints dbSceneFingerprints
for _, fp := range sceneFingerprints {
id, err := qb.getOrCreateFingerprintID(fp.Hash, fp.Algorithm)
if err != nil {
return err
}
fingerprints = append(fingerprints, &models.DBSceneFingerprint{
fingerprints = append(fingerprints, &dbSceneFingerprint{
FingerprintID: id,
SceneID: fp.SceneID,
UserID: fp.UserID,
Duration: fp.Duration,
Vote: fp.Vote,
})
}
@@ -105,7 +111,7 @@ func (qb *sceneQueryBuilder) UpdateFingerprints(sceneID uuid.UUID, updatedJoins
return err
}
return qb.CreateFingerprints(updatedJoins)
return qb.CreateOrReplaceFingerprints(updatedJoins)
}
func (qb *sceneQueryBuilder) DestroyFingerprints(sceneID uuid.UUID, toDestroy models.SceneFingerprints) error {
@@ -572,14 +578,17 @@ func (qb *sceneQueryBuilder) queryScenes(query string, args []interface{}) (mode
}
type sceneFingerprintGroup struct {
SceneID uuid.UUID `db:"scene_id"`
Hash string `db:"hash"`
Algorithm models.FingerprintAlgorithm `db:"algorithm"`
Duration float64 `db:"duration"`
Submissions int `db:"submissions"`
UserSubmitted bool `db:"user_submitted"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
SceneID uuid.UUID `db:"scene_id"`
Hash string `db:"hash"`
Algorithm models.FingerprintAlgorithm `db:"algorithm"`
Duration float64 `db:"duration"`
Submissions int `db:"submissions"`
Reports int `db:"reports"`
NetSubmissions int `db:"net_submissions"`
UserSubmitted bool `db:"user_submitted"`
UserReported bool `db:"user_reported"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func fingerprintGroupToFingerprint(fpg sceneFingerprintGroup) *models.Fingerprint {
@@ -588,7 +597,9 @@ func fingerprintGroupToFingerprint(fpg sceneFingerprintGroup) *models.Fingerprin
Algorithm: fpg.Algorithm,
Duration: int(fpg.Duration),
Submissions: fpg.Submissions,
Reports: fpg.Reports,
UserSubmitted: fpg.UserSubmitted,
UserReported: fpg.UserReported,
Created: fpg.CreatedAt,
Updated: fpg.UpdatedAt,
}
@@ -612,10 +623,13 @@ func (qb *sceneQueryBuilder) GetAllFingerprints(currentUserID uuid.UUID, ids []u
FP.hash,
FP.algorithm,
mode() WITHIN GROUP (ORDER BY SFP.duration) as duration,
COUNT(SFP.fingerprint_id) as submissions,
COUNT(CASE WHEN SFP.vote = 1 THEN 1 END) as submissions,
COUNT(CASE WHEN SFP.vote = -1 THEN 1 END) as reports,
SUM(SFP.vote) as net_submissions,
MIN(created_at) as created_at,
MAX(created_at) as updated_at,
bool_or(SFP.user_id = :userid) as user_submitted
bool_or(SFP.user_id = :userid AND SFP.vote = 1) as user_submitted,
bool_or(SFP.user_id = :userid AND SFP.vote = -1) as user_reported
FROM scene_fingerprints SFP
JOIN fingerprints FP ON SFP.fingerprint_id = FP.id
WHERE SFP.scene_id IN (:sceneids)
@@ -627,7 +641,7 @@ func (qb *sceneQueryBuilder) GetAllFingerprints(currentUserID uuid.UUID, ids []u
query += `
GROUP BY SFP.scene_id, FP.algorithm, FP.hash
ORDER BY submissions DESC`
ORDER BY net_submissions DESC`
arg := map[string]interface{}{
"userid": currentUserID,
@@ -667,6 +681,38 @@ func (qb *sceneQueryBuilder) GetAllFingerprints(currentUserID uuid.UUID, ids []u
return result, nil
}
// SubmittedHashExists returns true if the given hash exists for the given scene
func (qb *sceneQueryBuilder) SubmittedHashExists(sceneID uuid.UUID, hash string, algorithm models.FingerprintAlgorithm) (bool, error) {
query := `
SELECT
1
FROM scene_fingerprints f
JOIN fingerprints fp ON f.fingerprint_id = fp.id
WHERE f.scene_id = :sceneid AND fp.hash = :hash AND fp.algorithm = :algorithm AND f.vote = 1
`
arg := map[string]interface{}{
"sceneid": sceneID,
"hash": hash,
"algorithm": algorithm,
}
query, args, err := sqlx.Named(query, arg)
if err != nil {
return false, err
}
result := false
if err := qb.dbi.queryFunc(query, args, func(rows *sqlx.Rows) error {
result = true
return nil
}); err != nil {
return false, err
}
return result, nil
}
func (qb *sceneQueryBuilder) GetPerformers(id uuid.UUID) (models.PerformersScenes, error) {
joins := models.PerformersScenes{}
err := qb.dbi.FindJoins(scenePerformerTable, id, &joins)
@@ -1005,13 +1051,14 @@ func (qb *sceneQueryBuilder) addFingerprintsFromEdit(scene *models.Scene, data *
Algorithm: fingerprint.Algorithm.String(),
SceneID: scene.ID,
UserID: userID,
Vote: 1,
Duration: fingerprint.Duration,
CreatedAt: time.Now(),
})
}
}
return qb.CreateFingerprints(newFingerprints)
return qb.CreateOrReplaceFingerprints(newFingerprints)
}
func (qb *sceneQueryBuilder) getOrCreateFingerprintID(hash string, algorithm string) (int, error) {