diff --git a/graphql/documents/mutations/scene.graphql b/graphql/documents/mutations/scene.graphql index 2f54270ec..5cdc021b1 100644 --- a/graphql/documents/mutations/scene.graphql +++ b/graphql/documents/mutations/scene.graphql @@ -54,6 +54,12 @@ mutation BulkSceneUpdate( } } +mutation ScenesUpdate($input : [SceneUpdateInput!]!) { + scenesUpdate(input: $input) { + ...SceneData + } +} + mutation SceneDestroy($id: ID!, $delete_file: Boolean, $delete_generated : Boolean) { sceneDestroy(input: {id: $id, delete_file: $delete_file, delete_generated: $delete_generated}) } \ No newline at end of file diff --git a/graphql/documents/queries/scene.graphql b/graphql/documents/queries/scene.graphql index edc816f2f..83fb58cab 100644 --- a/graphql/documents/queries/scene.graphql +++ b/graphql/documents/queries/scene.graphql @@ -7,6 +7,15 @@ query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene } } +query FindScenesByPathRegex($filter: FindFilterType) { + findScenesByPathRegex(filter: $filter) { + count + scenes { + ...SlimSceneData + } + } +} + query FindScene($id: ID!, $checksum: String) { findScene(id: $id, checksum: $checksum) { ...SceneData diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index c6c9ca409..19f2d5e91 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -5,6 +5,8 @@ type Query { """A function which queries Scene objects""" findScenes(scene_filter: SceneFilterType, scene_ids: [Int!], filter: FindFilterType): FindScenesResultType! + findScenesByPathRegex(filter: FindFilterType): FindScenesResultType! + """A function which queries SceneMarker objects""" findSceneMarkers(scene_marker_filter: SceneMarkerFilterType filter: FindFilterType): FindSceneMarkersResultType! @@ -79,6 +81,7 @@ type Mutation { sceneUpdate(input: SceneUpdateInput!): Scene bulkSceneUpdate(input: BulkSceneUpdateInput!): [Scene!] sceneDestroy(input: SceneDestroyInput!): Boolean! + scenesUpdate(input: [SceneUpdateInput!]!): [Scene] sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker diff --git a/pkg/api/resolver_mutation_scene.go b/pkg/api/resolver_mutation_scene.go index fe724a775..58230d3a8 100644 --- a/pkg/api/resolver_mutation_scene.go +++ b/pkg/api/resolver_mutation_scene.go @@ -6,12 +6,57 @@ import ( "strconv" "time" + "github.com/jmoiron/sqlx" + "github.com/stashapp/stash/pkg/database" "github.com/stashapp/stash/pkg/manager" "github.com/stashapp/stash/pkg/models" ) func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUpdateInput) (*models.Scene, error) { + // Start the transaction and save the scene + tx := database.DB.MustBeginTx(ctx, nil) + + ret, err := r.sceneUpdate(input, tx) + + if err != nil { + _ = tx.Rollback() + return nil, err + } + + // Commit + if err := tx.Commit(); err != nil { + return nil, err + } + + return ret, nil +} + +func (r *mutationResolver) ScenesUpdate(ctx context.Context, input []*models.SceneUpdateInput) ([]*models.Scene, error) { + // Start the transaction and save the scene + tx := database.DB.MustBeginTx(ctx, nil) + + var ret []*models.Scene + + for _, scene := range input { + thisScene, err := r.sceneUpdate(*scene, tx) + ret = append(ret, thisScene) + + if err != nil { + _ = tx.Rollback() + return nil, err + } + } + + // Commit + if err := tx.Commit(); err != nil { + return nil, err + } + + return ret, nil +} + +func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, tx *sqlx.Tx) (*models.Scene, error) { // Populate scene from the input sceneID, _ := strconv.Atoi(input.ID) updatedTime := time.Now() @@ -47,13 +92,10 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp updatedScene.StudioID = &sql.NullInt64{Valid: false} } - // Start the transaction and save the scene marker - tx := database.DB.MustBeginTx(ctx, nil) qb := models.NewSceneQueryBuilder() jqb := models.NewJoinsQueryBuilder() scene, err := qb.Update(updatedScene, tx) if err != nil { - _ = tx.Rollback() return nil, err } @@ -61,7 +103,6 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp gqb := models.NewGalleryQueryBuilder() err = gqb.ClearGalleryId(sceneID, tx) if err != nil { - _ = tx.Rollback() return nil, err } @@ -76,7 +117,6 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp gqb := models.NewGalleryQueryBuilder() _, err := gqb.Update(updatedGallery, tx) if err != nil { - _ = tx.Rollback() return nil, err } } @@ -92,7 +132,6 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp performerJoins = append(performerJoins, performerJoin) } if err := jqb.UpdatePerformersScenes(sceneID, performerJoins, tx); err != nil { - _ = tx.Rollback() return nil, err } @@ -107,12 +146,6 @@ func (r *mutationResolver) SceneUpdate(ctx context.Context, input models.SceneUp tagJoins = append(tagJoins, tagJoin) } if err := jqb.UpdateScenesTags(sceneID, tagJoins, tx); err != nil { - _ = tx.Rollback() - return nil, err - } - - // Commit - if err := tx.Commit(); err != nil { return nil, err } diff --git a/pkg/api/resolver_query_find_scene.go b/pkg/api/resolver_query_find_scene.go index c773b5f92..38feecbd8 100644 --- a/pkg/api/resolver_query_find_scene.go +++ b/pkg/api/resolver_query_find_scene.go @@ -27,3 +27,13 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen Scenes: scenes, }, nil } + +func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (*models.FindScenesResultType, error) { + qb := models.NewSceneQueryBuilder() + + scenes, total := qb.QueryByPathRegex(filter) + return &models.FindScenesResultType{ + Count: total, + Scenes: scenes, + }, nil +} diff --git a/pkg/database/database.go b/pkg/database/database.go index 984439873..24b845340 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -1,12 +1,14 @@ package database import ( + "database/sql" "fmt" + "regexp" "github.com/gobuffalo/packr/v2" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/source" "github.com/jmoiron/sqlx" - _ "github.com/mattn/go-sqlite3" + sqlite3 "github.com/mattn/go-sqlite3" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/utils" "os" @@ -15,11 +17,16 @@ import ( var DB *sqlx.DB var appSchemaVersion uint = 1 +const sqlite3Driver = "sqlite3_regexp" + func Initialize(databasePath string) { runMigrations(databasePath) + // register custom driver with regexp function + registerRegexpFunc() + // https://github.com/mattn/go-sqlite3 - conn, err := sqlx.Open("sqlite3", "file:"+databasePath+"?_fk=true") + conn, err := sqlx.Open(sqlite3Driver, "file:"+databasePath+"?_fk=true") conn.SetMaxOpenConns(25) conn.SetMaxIdleConns(4) if err != nil { @@ -62,3 +69,16 @@ func runMigrations(databasePath string) { } } } + +func registerRegexpFunc() { + regexFn := func(re, s string) (bool, error) { + return regexp.MatchString(re, s) + } + + sql.Register(sqlite3Driver, + &sqlite3.SQLiteDriver{ + ConnectHook: func(conn *sqlite3.SQLiteConn) error { + return conn.RegisterFunc("regexp", regexFn, true) + }, + }) +} diff --git a/pkg/models/querybuilder_scene.go b/pkg/models/querybuilder_scene.go index f19725d88..bb9f7f97b 100644 --- a/pkg/models/querybuilder_scene.go +++ b/pkg/models/querybuilder_scene.go @@ -291,6 +291,32 @@ func getMultiCriterionClause(table string, joinTable string, joinTableField stri return whereClause, havingClause } +func (qb *SceneQueryBuilder) QueryByPathRegex(findFilter *FindFilterType) ([]*Scene, int) { + if findFilter == nil { + findFilter = &FindFilterType{} + } + + var whereClauses []string + var havingClauses []string + var args []interface{} + body := selectDistinctIDs("scenes") + + if q := findFilter.Q; q != nil && *q != "" { + whereClauses = append(whereClauses, "scenes.path regexp '" + *q + "'") + } + + sortAndPagination := qb.getSceneSort(findFilter) + getPagination(findFilter) + idsResult, countResult := executeFindQuery("scenes", body, args, sortAndPagination, whereClauses, havingClauses) + + var scenes []*Scene + for _, id := range idsResult { + scene, _ := qb.Find(id) + scenes = append(scenes, scene) + } + + return scenes, countResult +} + func (qb *SceneQueryBuilder) getSceneSort(findFilter *FindFilterType) string { if findFilter == nil { return " ORDER BY scenes.path, scenes.date ASC " diff --git a/ui/v2/src/App.tsx b/ui/v2/src/App.tsx index 93df837ba..89c30b2b8 100755 --- a/ui/v2/src/App.tsx +++ b/ui/v2/src/App.tsx @@ -10,6 +10,7 @@ import { Settings } from "./components/Settings/Settings"; import { Stats } from "./components/Stats"; import Studios from "./components/Studios/Studios"; import Tags from "./components/Tags/Tags"; +import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser"; interface IProps {} @@ -27,6 +28,7 @@ export const App: FunctionComponent = (props: IProps) => { + diff --git a/ui/v2/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index 2526aa52a..e10b57189 100644 --- a/ui/v2/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -5,12 +5,14 @@ import { Divider, FormGroup, H4, + AnchorButton, } from "@blueprintjs/core"; import React, { FunctionComponent, useState } from "react"; import { StashService } from "../../../core/StashService"; import { ErrorUtils } from "../../../utils/errors"; import { ToastUtils } from "../../../utils/toasts"; import { GenerateButton } from "./GenerateButton"; +import { Link } from "react-router-dom"; interface IProps {} @@ -94,6 +96,12 @@ export const SettingsTasksPanel: FunctionComponent = (props: IProps) => onChange={() => setNameFromMetadata(!nameFromMetadata)} /> + + + ) + } + + return ( + +

Scene Filename Parser

+ onFindClicked(input)} + /> + + {isLoading ? : undefined} + {renderTable()} +
+ ); +}; + \ No newline at end of file diff --git a/ui/v2/src/core/StashService.ts b/ui/v2/src/core/StashService.ts index 027ae6306..fb605382d 100644 --- a/ui/v2/src/core/StashService.ts +++ b/ui/v2/src/core/StashService.ts @@ -183,6 +183,10 @@ export class StashService { return GQL.useBulkSceneUpdate({ variables: input, refetchQueries: ["FindScenes"] }); } + public static useScenesUpdate(input: GQL.SceneUpdateInput[]) { + return GQL.useScenesUpdate({ variables: { input : input }}); + } + public static useSceneDestroy(input: GQL.SceneDestroyInput) { return GQL.useSceneDestroy({ variables: input }); } @@ -275,6 +279,13 @@ export class StashService { }); } + public static querySceneByPathRegex(filter: GQL.FindFilterType) { + return StashService.client.query({ + query: GQL.FindScenesByPathRegexDocument, + variables: {filter: filter}, + }); + } + public static nullToUndefined(value: any): any { if (_.isPlainObject(value)) { return _.mapValues(value, StashService.nullToUndefined); diff --git a/ui/v2/src/index.scss b/ui/v2/src/index.scss index 82ff44ef7..411a5a408 100755 --- a/ui/v2/src/index.scss +++ b/ui/v2/src/index.scss @@ -260,3 +260,36 @@ span.block { text-decoration: underline; } } + +#parser-container { + margin: 10px auto; + width: 75%; +} + +#parser-container .inputs label { + width: 12em; +} + +#parser-container .inputs .bp3-input-group { + width: 80ch; +} + +.scene-parser-row .bp3-checkbox { + margin: 0px -20px 0px 0px; +} + +.scene-parser-row .title input { + width: 50ch; +} + +.scene-parser-row input { + min-width: 10ch; +} + +.scene-parser-row .bp3-form-group { + margin-bottom: 0px; +} + +.scene-parser-row div:first-child > input { + margin-bottom: 5px; +} \ No newline at end of file