diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 0e48063aa..18bcc9dff 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -33,6 +33,9 @@ input PerformerFilterType { OR: PerformerFilterType NOT: PerformerFilterType + name: StringCriterionInput + details: StringCriterionInput + """Filter by favorite""" filter_favorites: Boolean """Filter by birth year""" @@ -105,6 +108,15 @@ input SceneFilterType { OR: SceneFilterType NOT: SceneFilterType + title: StringCriterionInput + details: StringCriterionInput + + """Filter by file oshash""" + oshash: StringCriterionInput + """Filter by file checksum""" + checksum: StringCriterionInput + """Filter by file phash""" + phash: StringCriterionInput """Filter by path""" path: StringCriterionInput """Filter by rating""" @@ -144,6 +156,15 @@ input SceneFilterType { } input MovieFilterType { + + name: StringCriterionInput + director: StringCriterionInput + synopsis: StringCriterionInput + + """Filter by duration (in seconds)""" + duration: IntCriterionInput + """Filter by rating""" + rating: IntCriterionInput """Filter to only include movies with this studio""" studios: HierarchicalMultiCriterionInput """Filter to only include movies missing this property""" @@ -153,6 +174,8 @@ input MovieFilterType { } input StudioFilterType { + name: StringCriterionInput + details: StringCriterionInput """Filter to only include studios with this parent studio""" parents: MultiCriterionInput """Filter by StashID""" @@ -176,6 +199,11 @@ input GalleryFilterType { OR: GalleryFilterType NOT: GalleryFilterType + title: StringCriterionInput + details: StringCriterionInput + + """Filter by file checksum""" + checksum: StringCriterionInput """Filter by path""" path: StringCriterionInput """Filter to only include galleries missing this property""" @@ -241,6 +269,10 @@ input ImageFilterType { OR: ImageFilterType NOT: ImageFilterType + title: StringCriterionInput + + """Filter by file checksum""" + checksum: StringCriterionInput """Filter by path""" path: StringCriterionInput """Filter by rating""" diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index cfa894cb8..de46f1d96 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -203,6 +203,9 @@ func (qb *galleryQueryBuilder) makeFilter(galleryFilter *models.GalleryFilterTyp query.not(qb.makeFilter(galleryFilter.Not)) } + query.handleCriterion(stringCriterionHandler(galleryFilter.Title, "galleries.title")) + query.handleCriterion(stringCriterionHandler(galleryFilter.Details, "galleries.details")) + query.handleCriterion(stringCriterionHandler(galleryFilter.Checksum, "galleries.checksum")) query.handleCriterion(boolCriterionHandler(galleryFilter.IsZip, "galleries.zip")) query.handleCriterion(stringCriterionHandler(galleryFilter.Path, "galleries.path")) query.handleCriterion(intCriterionHandler(galleryFilter.Rating, "galleries.rating")) diff --git a/pkg/sqlite/image.go b/pkg/sqlite/image.go index b0933c93a..d0b6f16f8 100644 --- a/pkg/sqlite/image.go +++ b/pkg/sqlite/image.go @@ -231,6 +231,8 @@ func (qb *imageQueryBuilder) makeFilter(imageFilter *models.ImageFilterType) *fi query.not(qb.makeFilter(imageFilter.Not)) } + query.handleCriterion(stringCriterionHandler(imageFilter.Checksum, "images.checksum")) + query.handleCriterion(stringCriterionHandler(imageFilter.Title, "images.title")) query.handleCriterion(stringCriterionHandler(imageFilter.Path, "images.path")) query.handleCriterion(intCriterionHandler(imageFilter.Rating, "images.rating")) query.handleCriterion(intCriterionHandler(imageFilter.OCounter, "images.o_counter")) diff --git a/pkg/sqlite/movies.go b/pkg/sqlite/movies.go index e6887b47a..49d970459 100644 --- a/pkg/sqlite/movies.go +++ b/pkg/sqlite/movies.go @@ -118,6 +118,11 @@ func (qb *movieQueryBuilder) All() ([]*models.Movie, error) { func (qb *movieQueryBuilder) makeFilter(movieFilter *models.MovieFilterType) *filterBuilder { query := &filterBuilder{} + query.handleCriterion(stringCriterionHandler(movieFilter.Name, "movies.name")) + query.handleCriterion(stringCriterionHandler(movieFilter.Director, "movies.director")) + query.handleCriterion(stringCriterionHandler(movieFilter.Synopsis, "movies.synopsis")) + query.handleCriterion(intCriterionHandler(movieFilter.Rating, "movies.rating")) + query.handleCriterion(durationCriterionHandler(movieFilter.Duration, "movies.duration")) query.handleCriterion(movieIsMissingCriterionHandler(qb, movieFilter.IsMissing)) query.handleCriterion(stringCriterionHandler(movieFilter.URL, "movies.url")) query.handleCriterion(movieStudioCriterionHandler(qb, movieFilter.Studios)) diff --git a/pkg/sqlite/performer.go b/pkg/sqlite/performer.go index a7828159b..fbe33eb62 100644 --- a/pkg/sqlite/performer.go +++ b/pkg/sqlite/performer.go @@ -239,6 +239,9 @@ func (qb *performerQueryBuilder) makeFilter(filter *models.PerformerFilterType) } const tableName = performerTable + query.handleCriterion(stringCriterionHandler(filter.Name, tableName+".name")) + query.handleCriterion(stringCriterionHandler(filter.Details, tableName+".details")) + query.handleCriterion(boolCriterionHandler(filter.FilterFavorites, tableName+".favorite")) query.handleCriterion(yearFilterCriterionHandler(filter.BirthYear, tableName+".birthdate")) diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index 511e1580c..4aec58ab9 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -354,6 +354,11 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi } query.handleCriterion(stringCriterionHandler(sceneFilter.Path, "scenes.path")) + query.handleCriterion(stringCriterionHandler(sceneFilter.Title, "scenes.title")) + query.handleCriterion(stringCriterionHandler(sceneFilter.Details, "scenes.details")) + query.handleCriterion(stringCriterionHandler(sceneFilter.Oshash, "scenes.oshash")) + query.handleCriterion(stringCriterionHandler(sceneFilter.Checksum, "scenes.checksum")) + query.handleCriterion(phashCriterionHandler(sceneFilter.Phash)) query.handleCriterion(intCriterionHandler(sceneFilter.Rating, "scenes.rating")) query.handleCriterion(intCriterionHandler(sceneFilter.OCounter, "scenes.o_counter")) query.handleCriterion(boolCriterionHandler(sceneFilter.Organized, "scenes.organized")) @@ -430,6 +435,29 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt return scenes, countResult, nil } +func phashCriterionHandler(phashFilter *models.StringCriterionInput) criterionHandlerFunc { + return func(f *filterBuilder) { + if phashFilter != nil { + // convert value to int from hex + // ignore errors + value, _ := utils.StringToPhash(phashFilter.Value) + + if modifier := phashFilter.Modifier; phashFilter.Modifier.IsValid() { + switch modifier { + case models.CriterionModifierEquals: + f.addWhere("scenes.phash = ?", value) + case models.CriterionModifierNotEquals: + f.addWhere("scenes.phash != ?", value) + case models.CriterionModifierIsNull: + f.addWhere("scenes.phash IS NULL") + case models.CriterionModifierNotNull: + f.addWhere("scenes.phash IS NOT NULL") + } + } + } + } +} + func durationCriterionHandler(durationFilter *models.IntCriterionInput, column string) criterionHandlerFunc { return func(f *filterBuilder) { if durationFilter != nil { diff --git a/pkg/sqlite/studio.go b/pkg/sqlite/studio.go index 31f371736..34d657148 100644 --- a/pkg/sqlite/studio.go +++ b/pkg/sqlite/studio.go @@ -184,6 +184,8 @@ func (qb *studioQueryBuilder) Query(studioFilter *models.StudioFilterType, findF query.handleCountCriterion(studioFilter.SceneCount, studioTable, sceneTable, studioIDColumn) query.handleCountCriterion(studioFilter.ImageCount, studioTable, imageTable, studioIDColumn) query.handleCountCriterion(studioFilter.GalleryCount, studioTable, galleryTable, studioIDColumn) + query.handleStringCriterionInput(studioFilter.Name, "studios.name") + query.handleStringCriterionInput(studioFilter.Details, "studios.details") query.handleStringCriterionInput(studioFilter.URL, "studios.url") query.handleStringCriterionInput(studioFilter.StashID, "studio_stash_ids.stash_id") diff --git a/pkg/utils/phash.go b/pkg/utils/phash.go index f5e1f2cd9..59d9e0016 100644 --- a/pkg/utils/phash.go +++ b/pkg/utils/phash.go @@ -55,3 +55,12 @@ func findNeighbors(bucket int, neighbors []int, hashes []*Phash, scenes *[]int) func PhashToString(phash int64) string { return strconv.FormatUint(uint64(phash), 16) } + +func StringToPhash(s string) (int64, error) { + ret, err := strconv.ParseUint(s, 16, 64) + if err != nil { + return 0, err + } + + return int64(ret), nil +} diff --git a/ui/v2.5/src/components/Changelog/versions/v080.md b/ui/v2.5/src/components/Changelog/versions/v080.md index bc9e32dca..23c2f37e0 100644 --- a/ui/v2.5/src/components/Changelog/versions/v080.md +++ b/ui/v2.5/src/components/Changelog/versions/v080.md @@ -1,5 +1,6 @@ ### ✨ New Features -* Add button to open scene in external player on handheld devices. ([#679](https://github.com/stashapp/stash/pull/679)) +* Added filter criteria for name, details and hash related fields. ([#1505](https://github.com/stashapp/stash/pull/1505)) +* Added button to open scene in external player on handheld devices. ([#679](https://github.com/stashapp/stash/pull/679)) * Added support for saved and default filters. ([#1474](https://github.com/stashapp/stash/pull/1474)) * Added merge tags functionality. ([#1481](https://github.com/stashapp/stash/pull/1481)) * Added support for triggering plugin tasks during operations. ([#1452](https://github.com/stashapp/stash/pull/1452)) diff --git a/ui/v2.5/src/models/list-filter/criteria/criterion.ts b/ui/v2.5/src/models/list-filter/criteria/criterion.ts index 7e1c55788..2e2b8b380 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -184,8 +184,16 @@ export class StringCriterionOption extends CriterionOption { } } -export function createStringCriterionOption(value: CriterionType) { - return new StringCriterionOption(value, value, value); +export function createStringCriterionOption( + value: CriterionType, + messageID?: string, + parameterName?: string +) { + return new StringCriterionOption( + messageID ?? value, + value, + parameterName ?? messageID ?? value + ); } export class StringCriterion extends Criterion { @@ -236,6 +244,18 @@ export class MandatoryStringCriterionOption extends CriterionOption { } } +export function createMandatoryStringCriterionOption( + value: CriterionType, + messageID?: string, + parameterName?: string +) { + return new MandatoryStringCriterionOption( + messageID ?? value, + value, + parameterName ?? messageID ?? value + ); +} + export class BooleanCriterionOption extends CriterionOption { constructor(messageID: string, value: CriterionType, parameterName?: string) { super({ diff --git a/ui/v2.5/src/models/list-filter/criteria/factory.ts b/ui/v2.5/src/models/list-filter/criteria/factory.ts index 003b59885..ddb946036 100644 --- a/ui/v2.5/src/models/list-filter/criteria/factory.ts +++ b/ui/v2.5/src/models/list-filter/criteria/factory.ts @@ -38,12 +38,16 @@ import { GalleriesCriterion } from "./galleries"; import { CriterionType } from "../types"; import { InteractiveCriterion } from "./interactive"; import { RatingCriterionOption } from "./rating"; +import { PhashCriterionOption } from "./phash"; export function makeCriteria(type: CriterionType = "none") { switch (type) { case "none": return new NoneCriterion(); + case "name": case "path": + case "checksum": + case "oshash": return new StringCriterion( new MandatoryStringCriterionOption(type, type) ); @@ -111,6 +115,13 @@ export function makeCriteria(type: CriterionType = "none") { ); case "gender": return new GenderCriterion(); + case "sceneChecksum": + case "galleryChecksum": + return new StringCriterion( + new StringCriterionOption("checksum", type, "checksum") + ); + case "phash": + return new StringCriterion(PhashCriterionOption); case "ethnicity": case "country": case "hair_color": @@ -124,6 +135,10 @@ export function makeCriteria(type: CriterionType = "none") { case "aliases": case "url": case "stash_id": + case "details": + case "title": + case "director": + case "synopsis": return new StringCriterion(new StringCriterionOption(type, type)); case "interactive": return new InteractiveCriterion(); diff --git a/ui/v2.5/src/models/list-filter/criteria/phash.ts b/ui/v2.5/src/models/list-filter/criteria/phash.ts new file mode 100644 index 000000000..d9b61caf9 --- /dev/null +++ b/ui/v2.5/src/models/list-filter/criteria/phash.ts @@ -0,0 +1,15 @@ +import { CriterionModifier } from "src/core/generated-graphql"; +import { CriterionOption } from "./criterion"; + +export const PhashCriterionOption = new CriterionOption({ + messageID: "media_info.phash", + type: "phash", + parameterName: "phash", + inputType: "text", + modifierOptions: [ + CriterionModifier.Equals, + CriterionModifier.NotEquals, + CriterionModifier.IsNull, + CriterionModifier.NotNull, + ], +}); diff --git a/ui/v2.5/src/models/list-filter/galleries.ts b/ui/v2.5/src/models/list-filter/galleries.ts index cbb18cce2..12450a71f 100644 --- a/ui/v2.5/src/models/list-filter/galleries.ts +++ b/ui/v2.5/src/models/list-filter/galleries.ts @@ -38,7 +38,14 @@ const displayModeOptions = [ ]; const criterionOptions = [ + createStringCriterionOption("title"), + createStringCriterionOption("details"), createStringCriterionOption("path"), + createStringCriterionOption( + "galleryChecksum", + "media_info.checksum", + "checksum" + ), RatingCriterionOption, OrganizedCriterionOption, AverageResolutionCriterionOption, diff --git a/ui/v2.5/src/models/list-filter/images.ts b/ui/v2.5/src/models/list-filter/images.ts index 25f98f68f..157ce603d 100644 --- a/ui/v2.5/src/models/list-filter/images.ts +++ b/ui/v2.5/src/models/list-filter/images.ts @@ -1,5 +1,6 @@ import { createMandatoryNumberCriterionOption, + createMandatoryStringCriterionOption, createStringCriterionOption, } from "./criteria/criterion"; import { ImageIsMissingCriterionOption } from "./criteria/is-missing"; @@ -31,7 +32,9 @@ const sortByOptions = [ const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall]; const criterionOptions = [ - createStringCriterionOption("path"), + createStringCriterionOption("title"), + createMandatoryStringCriterionOption("checksum", "media_info.checksum"), + createMandatoryStringCriterionOption("path"), RatingCriterionOption, OrganizedCriterionOption, createMandatoryNumberCriterionOption("o_counter"), diff --git a/ui/v2.5/src/models/list-filter/movies.ts b/ui/v2.5/src/models/list-filter/movies.ts index 72e7256a6..1c2b8e0f6 100644 --- a/ui/v2.5/src/models/list-filter/movies.ts +++ b/ui/v2.5/src/models/list-filter/movies.ts @@ -1,5 +1,9 @@ -import { createStringCriterionOption } from "./criteria/criterion"; +import { + createMandatoryNumberCriterionOption, + createStringCriterionOption, +} from "./criteria/criterion"; import { MovieIsMissingCriterionOption } from "./criteria/is-missing"; +import { RatingCriterionOption } from "./criteria/rating"; import { StudiosCriterionOption } from "./criteria/studios"; import { ListFilterOptions } from "./filter-options"; import { DisplayMode } from "./types"; @@ -19,6 +23,11 @@ const criterionOptions = [ StudiosCriterionOption, MovieIsMissingCriterionOption, createStringCriterionOption("url"), + createStringCriterionOption("name"), + createStringCriterionOption("director"), + createStringCriterionOption("synopsis"), + createMandatoryNumberCriterionOption("duration"), + RatingCriterionOption, ]; export const MovieListFilterOptions = new ListFilterOptions( diff --git a/ui/v2.5/src/models/list-filter/performers.ts b/ui/v2.5/src/models/list-filter/performers.ts index 3e1f48e80..6fc10e5bf 100644 --- a/ui/v2.5/src/models/list-filter/performers.ts +++ b/ui/v2.5/src/models/list-filter/performers.ts @@ -43,6 +43,8 @@ const numberCriteria: CriterionType[] = [ ]; const stringCriteria: CriterionType[] = [ + "name", + "details", "ethnicity", "country", "hair_color", diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index 380ee606d..2b340abdc 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -1,5 +1,6 @@ import { createMandatoryNumberCriterionOption, + createMandatoryStringCriterionOption, createStringCriterionOption, } from "./criteria/criterion"; import { HasMarkersCriterionOption } from "./criteria/has-markers"; @@ -17,6 +18,7 @@ import { } from "./criteria/tags"; import { ListFilterOptions } from "./filter-options"; import { DisplayMode } from "./types"; +import { PhashCriterionOption } from "./criteria/phash"; const defaultSortBy = "date"; const sortByOptions = [ @@ -46,7 +48,16 @@ const displayModeOptions = [ ]; const criterionOptions = [ - createStringCriterionOption("path"), + createStringCriterionOption("title"), + createMandatoryStringCriterionOption("path"), + createStringCriterionOption("details"), + createMandatoryStringCriterionOption("oshash", "media_info.hash"), + createStringCriterionOption( + "sceneChecksum", + "media_info.checksum", + "checksum" + ), + PhashCriterionOption, RatingCriterionOption, OrganizedCriterionOption, createMandatoryNumberCriterionOption("o_counter"), diff --git a/ui/v2.5/src/models/list-filter/studios.ts b/ui/v2.5/src/models/list-filter/studios.ts index ff52b0d27..fd417aca2 100644 --- a/ui/v2.5/src/models/list-filter/studios.ts +++ b/ui/v2.5/src/models/list-filter/studios.ts @@ -1,5 +1,6 @@ import { createMandatoryNumberCriterionOption, + createMandatoryStringCriterionOption, createStringCriterionOption, } from "./criteria/criterion"; import { StudioIsMissingCriterionOption } from "./criteria/is-missing"; @@ -28,6 +29,8 @@ const sortByOptions = ["name", "random", "rating"] const displayModeOptions = [DisplayMode.Grid]; const criterionOptions = [ + createMandatoryStringCriterionOption("name"), + createStringCriterionOption("details"), ParentStudiosCriterionOption, StudioIsMissingCriterionOption, RatingCriterionOption, diff --git a/ui/v2.5/src/models/list-filter/tags.ts b/ui/v2.5/src/models/list-filter/tags.ts index 17b8d4107..59972ab39 100644 --- a/ui/v2.5/src/models/list-filter/tags.ts +++ b/ui/v2.5/src/models/list-filter/tags.ts @@ -1,5 +1,6 @@ import { createMandatoryNumberCriterionOption, + createMandatoryStringCriterionOption, createStringCriterionOption, } from "./criteria/criterion"; import { TagIsMissingCriterionOption } from "./criteria/is-missing"; @@ -36,6 +37,7 @@ const sortByOptions = [ const displayModeOptions = [DisplayMode.Grid, DisplayMode.List]; const criterionOptions = [ + createMandatoryStringCriterionOption("name"), TagIsMissingCriterionOption, createStringCriterionOption("aliases"), createMandatoryNumberCriterionOption("scene_count"), diff --git a/ui/v2.5/src/models/list-filter/types.ts b/ui/v2.5/src/models/list-filter/types.ts index c15ab94f6..025dc3050 100644 --- a/ui/v2.5/src/models/list-filter/types.ts +++ b/ui/v2.5/src/models/list-filter/types.ts @@ -92,4 +92,14 @@ export type CriterionType = | "death_year" | "url" | "stash_id" - | "interactive"; + | "interactive" + | "name" + | "details" + | "title" + | "oshash" + | "checksum" + | "sceneChecksum" + | "galleryChecksum" + | "phash" + | "director" + | "synopsis";