diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 7ebc6ec72..13fc2ef7f 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -89,6 +89,10 @@ input TitleDuplicationCriterionInput { duplicated: Boolean } +input URLDuplicationCriterionInput { + duplicated: Boolean +} + input StashIDCriterionInput { """ If present, this value is treated as a predicate. @@ -262,6 +266,8 @@ input SceneFilterType { duplicated_stash_id: StashIDDuplicationCriterionInput "Filter Scenes that have the same title" duplicated_title: TitleDuplicationCriterionInput + "Filter Scenes that have the same URL" + duplicated_url: URLDuplicationCriterionInput "Filter by resolution" resolution: ResolutionCriterionInput "Filter by orientation" diff --git a/pkg/models/scene.go b/pkg/models/scene.go index 5aac3030d..364ea2b75 100644 --- a/pkg/models/scene.go +++ b/pkg/models/scene.go @@ -16,6 +16,10 @@ type TitleDuplicationCriterionInput struct { Duplicated *bool `json:"duplicated"` } +type URLDuplicationCriterionInput struct { + Duplicated *bool `json:"duplicated"` +} + type SceneFilterType struct { OperatorFilter[SceneFilterType] ID *IntCriterionInput `json:"id"` @@ -47,6 +51,8 @@ type SceneFilterType struct { DuplicatedStashID *StashIDDuplicationCriterionInput `json:"duplicated_stash_id"` // Filter Scenes that have the same title DuplicatedTitle *TitleDuplicationCriterionInput `json:"duplicated_title"` + // Filter Scenes that have the same URL + DuplicatedURL *URLDuplicationCriterionInput `json:"duplicated_url"` // Filter by resolution Resolution *ResolutionCriterionInput `json:"resolution"` // Filter by orientation diff --git a/pkg/sqlite/scene_filter.go b/pkg/sqlite/scene_filter.go index 5c5b818ae..559e2004b 100644 --- a/pkg/sqlite/scene_filter.go +++ b/pkg/sqlite/scene_filter.go @@ -158,6 +158,7 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler { qb.phashDuplicatedCriterionHandler(sceneFilter.Duplicated, qb.addSceneFilesTable), qb.stashIDDuplicatedCriterionHandler(sceneFilter.DuplicatedStashID), qb.titleDuplicatedCriterionHandler(sceneFilter.DuplicatedTitle), + qb.urlDuplicatedCriterionHandler(sceneFilter.DuplicatedURL), &dateCriterionHandler{sceneFilter.Date, "scenes.date", nil}, ×tampCriterionHandler{sceneFilter.CreatedAt, "scenes.created_at", nil}, ×tampCriterionHandler{sceneFilter.UpdatedAt, "scenes.updated_at", nil}, @@ -331,6 +332,22 @@ func (qb *sceneFilterHandler) titleDuplicatedCriterionHandler(duplicatedFilter * } } +func (qb *sceneFilterHandler) urlDuplicatedCriterionHandler(duplicatedFilter *models.URLDuplicationCriterionInput) criterionHandlerFunc { + return func(ctx context.Context, f *filterBuilder) { + if duplicatedFilter != nil && duplicatedFilter.Duplicated != nil { + var v string + if *duplicatedFilter.Duplicated { + v = ">" + } else { + v = "=" + } + + // Find URLs that appear on more than one scene + f.addInnerJoin("(SELECT scene_id FROM scene_urls INNER JOIN (SELECT url FROM scene_urls GROUP BY url HAVING COUNT(DISTINCT scene_id) "+v+" 1) dupes ON scene_urls.url = dupes.url)", "scurl", "scenes.id = scurl.scene_id") + } + } +} + func (qb *sceneFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc { return func(ctx context.Context, f *filterBuilder) { if codec != nil { diff --git a/ui/v2.5/src/components/List/Filters/DuplicateFilter.tsx b/ui/v2.5/src/components/List/Filters/DuplicateFilter.tsx index 7905dc4ae..3e67bdae6 100644 --- a/ui/v2.5/src/components/List/Filters/DuplicateFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/DuplicateFilter.tsx @@ -6,6 +6,7 @@ import { Option, SelectedList } from "./SidebarListFilter"; import { DuplicatedCriterionOption } from "src/models/list-filter/criteria/phash"; import { DuplicatedStashIDCriterionOption } from "src/models/list-filter/criteria/stash-ids"; import { DuplicatedTitleCriterionOption } from "src/models/list-filter/criteria/title"; +import { DuplicatedURLCriterionOption } from "src/models/list-filter/criteria/url"; import { SidebarSection } from "src/components/Shared/Sidebar"; import { Icon } from "src/components/Shared/Icon"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; @@ -16,6 +17,7 @@ const DUPLICATE_TYPES = { phash: DuplicatedCriterionOption, stash_id: DuplicatedStashIDCriterionOption, title: DuplicatedTitleCriterionOption, + url: DuplicatedURLCriterionOption, } as const; type DuplicateTypeId = keyof typeof DUPLICATE_TYPES; @@ -32,6 +34,7 @@ const DUPLICATE_TYPE_MESSAGE_IDS: Record = { phash: "media_info.phash", stash_id: "stash_id", title: "title", + url: "url", }; export const SidebarDuplicateFilter: React.FC = ({ diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 04464584b..6a7f4a3c9 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -1063,6 +1063,7 @@ "duplicated_phash": "Duplicated (pHash)", "duplicated_stash_id": "Duplicated (Stash ID)", "duplicated_title": "Duplicated (Title)", + "duplicated_url": "Duplicated (URL)", "duration": "Duration", "effect_filters": { "aspect": "Aspect", diff --git a/ui/v2.5/src/models/list-filter/criteria/url.ts b/ui/v2.5/src/models/list-filter/criteria/url.ts new file mode 100644 index 000000000..b5877e255 --- /dev/null +++ b/ui/v2.5/src/models/list-filter/criteria/url.ts @@ -0,0 +1,20 @@ +import { UrlDuplicationCriterionInput } from "src/core/generated-graphql"; +import { BooleanCriterionOption, StringCriterion } from "./criterion"; + +export const DuplicatedURLCriterionOption = new BooleanCriterionOption( + "duplicated_url", + "duplicated_url", + () => new DuplicatedURLCriterion() +); + +export class DuplicatedURLCriterion extends StringCriterion { + constructor() { + super(DuplicatedURLCriterionOption); + } + + public toCriterionInput(): UrlDuplicationCriterionInput { + return { + duplicated: this.value === "true", + }; + } +} diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index 0bdaf238d..a405ad536 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -36,6 +36,7 @@ import { StashIDCriterionOption, } from "./criteria/stash-ids"; import { DuplicatedTitleCriterionOption } from "./criteria/title"; +import { DuplicatedURLCriterionOption } from "./criteria/url"; import { RatingCriterionOption } from "./criteria/rating"; import { PathCriterionOption } from "./criteria/path"; import { OrientationCriterionOption } from "./criteria/orientation"; @@ -106,6 +107,7 @@ const criterionOptions = [ DuplicatedCriterionOption, DuplicatedStashIDCriterionOption, DuplicatedTitleCriterionOption, + DuplicatedURLCriterionOption, OrganizedCriterionOption, RatingCriterionOption, createMandatoryNumberCriterionOption("o_counter", "o_count", { diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index 9e0101468..db58f65fc 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -199,6 +199,7 @@ export type CriterionType = | "duplicated" | "duplicated_stash_id" | "duplicated_title" + | "duplicated_url" | "ignore_auto_tag" | "file_count" | "stash_id_endpoint"