mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Parent studios (#595)
* Refactor getMultiCriterionClause Co-authored-by: Anon247 <61889302+Anon247@users.noreply.github.com>
This commit is contained in:
@@ -3,6 +3,22 @@ fragment StudioData on Studio {
|
||||
checksum
|
||||
name
|
||||
url
|
||||
parent_studio {
|
||||
id
|
||||
checksum
|
||||
name
|
||||
url
|
||||
image_path
|
||||
scene_count
|
||||
}
|
||||
child_studios {
|
||||
id
|
||||
checksum
|
||||
name
|
||||
url
|
||||
image_path
|
||||
scene_count
|
||||
}
|
||||
image_path
|
||||
scene_count
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
mutation StudioCreate(
|
||||
$name: String!,
|
||||
$url: String,
|
||||
$image: String) {
|
||||
$image: String
|
||||
$parent_id: ID) {
|
||||
|
||||
studioCreate(input: { name: $name, url: $url, image: $image }) {
|
||||
studioCreate(input: { name: $name, url: $url, image: $image, parent_id: $parent_id }) {
|
||||
...StudioData
|
||||
}
|
||||
}
|
||||
@@ -12,9 +13,10 @@ mutation StudioUpdate(
|
||||
$id: ID!
|
||||
$name: String,
|
||||
$url: String,
|
||||
$image: String) {
|
||||
$image: String
|
||||
$parent_id: ID) {
|
||||
|
||||
studioUpdate(input: { id: $id, name: $name, url: $url, image: $image }) {
|
||||
studioUpdate(input: { id: $id, name: $name, url: $url, image: $image, parent_id: $parent_id }) {
|
||||
...StudioData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
query FindStudios($filter: FindFilterType) {
|
||||
findStudios(filter: $filter) {
|
||||
query FindStudios($filter: FindFilterType, $studio_filter: StudioFilterType ) {
|
||||
findStudios(filter: $filter, studio_filter: $studio_filter) {
|
||||
count
|
||||
studios {
|
||||
...StudioData
|
||||
|
||||
@@ -20,7 +20,7 @@ type Query {
|
||||
"""Find a studio by ID"""
|
||||
findStudio(id: ID!): Studio
|
||||
"""A function which queries Studio objects"""
|
||||
findStudios(filter: FindFilterType): FindStudiosResultType!
|
||||
findStudios(studio_filter: StudioFilterType, filter: FindFilterType): FindStudiosResultType!
|
||||
|
||||
"""Find a movie by ID"""
|
||||
findMovie(id: ID!): Movie
|
||||
|
||||
@@ -91,6 +91,11 @@ input MovieFilterType {
|
||||
studios: MultiCriterionInput
|
||||
}
|
||||
|
||||
input StudioFilterType {
|
||||
"""Filter to only include studios with this parent studio"""
|
||||
parents: MultiCriterionInput
|
||||
}
|
||||
|
||||
enum CriterionModifier {
|
||||
"""="""
|
||||
EQUALS,
|
||||
|
||||
@@ -3,7 +3,8 @@ type Studio {
|
||||
checksum: String!
|
||||
name: String!
|
||||
url: String
|
||||
|
||||
parent_studio: Studio
|
||||
child_studios: [Studio!]!
|
||||
image_path: String # Resolver
|
||||
scene_count: Int # Resolver
|
||||
}
|
||||
@@ -11,6 +12,7 @@ type Studio {
|
||||
input StudioCreateInput {
|
||||
name: String!
|
||||
url: String
|
||||
parent_id: ID
|
||||
"""This should be base64 encoded"""
|
||||
image: String
|
||||
}
|
||||
@@ -19,6 +21,7 @@ input StudioUpdateInput {
|
||||
id: ID!
|
||||
name: String
|
||||
url: String
|
||||
parent_id: ID,
|
||||
"""This should be base64 encoded"""
|
||||
image: String
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
@@ -31,3 +32,17 @@ func (r *studioResolver) SceneCount(ctx context.Context, obj *models.Studio) (*i
|
||||
res, err := qb.CountByStudioID(obj.ID)
|
||||
return &res, err
|
||||
}
|
||||
|
||||
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (*models.Studio, error) {
|
||||
if !obj.ParentID.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.Find(int(obj.ParentID.Int64), nil)
|
||||
}
|
||||
|
||||
func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) ([]*models.Studio, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.FindChildren(obj.ID, nil)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
@@ -40,6 +41,10 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
if input.URL != nil {
|
||||
newStudio.URL = sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.ParentID != nil {
|
||||
parentID, _ := strconv.ParseInt(*input.ParentID, 10, 64)
|
||||
newStudio.ParentID = sql.NullInt64{Int64: parentID, Valid: true}
|
||||
}
|
||||
|
||||
// Start the transaction and save the studio
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
@@ -61,33 +66,48 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.StudioUpdateInput) (*models.Studio, error) {
|
||||
// Populate studio from the input
|
||||
studioID, _ := strconv.Atoi(input.ID)
|
||||
updatedStudio := models.Studio{
|
||||
|
||||
updatedStudio := models.StudioPartial{
|
||||
ID: studioID,
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: time.Now()},
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
|
||||
}
|
||||
if input.Image != nil {
|
||||
_, imageData, err := utils.ProcessBase64Image(*input.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updatedStudio.Image = imageData
|
||||
updatedStudio.Image = &imageData
|
||||
}
|
||||
if input.Name != nil {
|
||||
// generate checksum from studio name rather than image
|
||||
checksum := utils.MD5FromString(*input.Name)
|
||||
updatedStudio.Name = sql.NullString{String: *input.Name, Valid: true}
|
||||
updatedStudio.Checksum = checksum
|
||||
updatedStudio.Name = &sql.NullString{String: *input.Name, Valid: true}
|
||||
updatedStudio.Checksum = &checksum
|
||||
}
|
||||
if input.URL != nil {
|
||||
updatedStudio.URL = sql.NullString{String: *input.URL, Valid: true}
|
||||
updatedStudio.URL = &sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
|
||||
if input.ParentID != nil {
|
||||
parentID, _ := strconv.ParseInt(*input.ParentID, 10, 64)
|
||||
updatedStudio.ParentID = &sql.NullInt64{Int64: parentID, Valid: true}
|
||||
} else {
|
||||
// parent studio must be nullable
|
||||
updatedStudio.ParentID = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
// Start the transaction and save the studio
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
|
||||
if err := manager.ValidateModifyStudio(updatedStudio, tx); err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
studio, err := qb.Update(updatedStudio, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (*models.Stud
|
||||
return qb.Find(idInt, nil)
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindStudios(ctx context.Context, filter *models.FindFilterType) (*models.FindStudiosResultType, error) {
|
||||
func (r *queryResolver) FindStudios(ctx context.Context, studioFilter *models.StudioFilterType, filter *models.FindFilterType) (*models.FindStudiosResultType, error) {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
studios, total := qb.Query(filter)
|
||||
studios, total := qb.Query(studioFilter, filter)
|
||||
return &models.FindStudiosResultType{
|
||||
Count: total,
|
||||
Studios: studios,
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
var DB *sqlx.DB
|
||||
var dbPath string
|
||||
var appSchemaVersion uint = 8
|
||||
var appSchemaVersion uint = 9
|
||||
var databaseSchemaVersion uint
|
||||
|
||||
const sqlite3Driver = "sqlite3ex"
|
||||
|
||||
3
pkg/database/migrations/9_studios_parent_studio.up.sql
Normal file
3
pkg/database/migrations/9_studios_parent_studio.up.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE studios
|
||||
ADD COLUMN parent_id INTEGER DEFAULT NULL CHECK ( id IS NOT parent_id ) REFERENCES studios(id) on delete set null;
|
||||
CREATE INDEX index_studios_on_parent_id on studios (parent_id);
|
||||
@@ -2,17 +2,19 @@ package jsonschema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/json-iterator/go"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"os"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type Studio struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
||||
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
ParentStudio string `json:"parent_studio,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
||||
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
func LoadStudioFile(filePath string) (*Studio, error) {
|
||||
|
||||
36
pkg/manager/studio.go
Normal file
36
pkg/manager/studio.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func ValidateModifyStudio(studio models.StudioPartial, tx *sqlx.Tx) error {
|
||||
if studio.ParentID == nil || !studio.ParentID.Valid {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensure there is no cyclic dependency
|
||||
thisID := studio.ID
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
|
||||
currentParentID := *studio.ParentID
|
||||
|
||||
for currentParentID.Valid {
|
||||
if currentParentID.Int64 == int64(thisID) {
|
||||
return errors.New("studio cannot be an ancestor of itself")
|
||||
}
|
||||
|
||||
currentStudio, err := qb.Find(int(currentParentID.Int64), tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding parent studio: %s", err.Error())
|
||||
}
|
||||
|
||||
currentParentID = currentStudio.ParentID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -3,6 +3,12 @@ package manager
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
@@ -10,11 +16,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/manager/paths"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"math"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ExportTask struct {
|
||||
@@ -395,6 +396,8 @@ func (t *ExportTask) ExportStudios(ctx context.Context, workers int) {
|
||||
func exportStudio(wg *sync.WaitGroup, jobChan <-chan *models.Studio) {
|
||||
defer wg.Done()
|
||||
|
||||
studioQB := models.NewStudioQueryBuilder()
|
||||
|
||||
for studio := range jobChan {
|
||||
|
||||
newStudioJSON := jsonschema.Studio{
|
||||
@@ -408,6 +411,12 @@ func exportStudio(wg *sync.WaitGroup, jobChan <-chan *models.Studio) {
|
||||
if studio.URL.Valid {
|
||||
newStudioJSON.URL = studio.URL.String
|
||||
}
|
||||
if studio.ParentID.Valid {
|
||||
parent, _ := studioQB.Find(int(studio.ParentID.Int64), nil)
|
||||
if parent != nil {
|
||||
newStudioJSON.ParentStudio = parent.Name.String
|
||||
}
|
||||
}
|
||||
|
||||
newStudioJSON.Image = utils.GetBase64StringFromData(studio.Image)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package manager
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -157,7 +158,8 @@ func (t *ImportTask) ImportPerformers(ctx context.Context) {
|
||||
|
||||
func (t *ImportTask) ImportStudios(ctx context.Context) {
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
|
||||
pendingParent := make(map[string][]*jsonschema.Studio)
|
||||
|
||||
for i, mappingJSON := range t.Mappings.Studios {
|
||||
index := i + 1
|
||||
@@ -172,35 +174,28 @@ func (t *ImportTask) ImportStudios(ctx context.Context) {
|
||||
|
||||
logger.Progressf("[studios] %d of %d", index, len(t.Mappings.Studios))
|
||||
|
||||
// generate checksum from studio name rather than image
|
||||
checksum := utils.MD5FromString(studioJSON.Name)
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
_, imageData, err := utils.ProcessBase64Image(studioJSON.Image)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
logger.Errorf("[studios] <%s> invalid image: %s", mappingJSON.Checksum, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Populate a new studio from the input
|
||||
newStudio := models.Studio{
|
||||
Image: imageData,
|
||||
Checksum: checksum,
|
||||
Name: sql.NullString{String: studioJSON.Name, Valid: true},
|
||||
URL: sql.NullString{String: studioJSON.URL, Valid: true},
|
||||
CreatedAt: models.SQLiteTimestamp{Timestamp: t.getTimeFromJSONTime(studioJSON.CreatedAt)},
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: t.getTimeFromJSONTime(studioJSON.UpdatedAt)},
|
||||
}
|
||||
|
||||
_, err = qb.Create(newStudio, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
if err := t.ImportStudio(studioJSON, pendingParent, tx); err != nil {
|
||||
tx.Rollback()
|
||||
logger.Errorf("[studios] <%s> failed to create: %s", mappingJSON.Checksum, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// create the leftover studios, warning for missing parents
|
||||
if len(pendingParent) > 0 {
|
||||
logger.Warnf("[studios] importing studios with missing parents")
|
||||
|
||||
for _, s := range pendingParent {
|
||||
for _, orphanStudioJSON := range s {
|
||||
if err := t.ImportStudio(orphanStudioJSON, nil, tx); err != nil {
|
||||
tx.Rollback()
|
||||
logger.Errorf("[studios] <%s> failed to create: %s", orphanStudioJSON.Name, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("[studios] importing")
|
||||
if err := tx.Commit(); err != nil {
|
||||
logger.Errorf("[studios] import failed to commit: %s", err.Error())
|
||||
@@ -208,6 +203,74 @@ func (t *ImportTask) ImportStudios(ctx context.Context) {
|
||||
logger.Info("[studios] import complete")
|
||||
}
|
||||
|
||||
func (t *ImportTask) ImportStudio(studioJSON *jsonschema.Studio, pendingParent map[string][]*jsonschema.Studio, tx *sqlx.Tx) error {
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
|
||||
// generate checksum from studio name rather than image
|
||||
checksum := utils.MD5FromString(studioJSON.Name)
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
_, imageData, err := utils.ProcessBase64Image(studioJSON.Image)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid image: %s", err.Error())
|
||||
}
|
||||
|
||||
// Populate a new studio from the input
|
||||
newStudio := models.Studio{
|
||||
Image: imageData,
|
||||
Checksum: checksum,
|
||||
Name: sql.NullString{String: studioJSON.Name, Valid: true},
|
||||
URL: sql.NullString{String: studioJSON.URL, Valid: true},
|
||||
CreatedAt: models.SQLiteTimestamp{Timestamp: t.getTimeFromJSONTime(studioJSON.CreatedAt)},
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: t.getTimeFromJSONTime(studioJSON.UpdatedAt)},
|
||||
}
|
||||
|
||||
// Populate the parent ID
|
||||
if studioJSON.ParentStudio != "" {
|
||||
studio, err := qb.FindByName(studioJSON.ParentStudio, tx, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error finding studio by name <%s>: %s", studioJSON.ParentStudio, err.Error())
|
||||
}
|
||||
|
||||
if studio == nil {
|
||||
// its possible that the parent hasn't been created yet
|
||||
// do it after it is created
|
||||
if pendingParent == nil {
|
||||
logger.Warnf("[studios] studio <%s> does not exist", studioJSON.ParentStudio)
|
||||
} else {
|
||||
// add to the pending parent list so that it is created after the parent
|
||||
s := pendingParent[studioJSON.ParentStudio]
|
||||
s = append(s, studioJSON)
|
||||
pendingParent[studioJSON.ParentStudio] = s
|
||||
|
||||
// skip
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
newStudio.ParentID = sql.NullInt64{Int64: int64(studio.ID), Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = qb.Create(newStudio, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// now create the studios pending this studios creation
|
||||
s := pendingParent[studioJSON.Name]
|
||||
for _, childStudioJSON := range s {
|
||||
// map is nil since we're not checking parent studios at this point
|
||||
if err := t.ImportStudio(childStudioJSON, nil, tx); err != nil {
|
||||
return fmt.Errorf("failed to create child studio <%s>: %s", childStudioJSON.Name, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// delete the entry from the map so that we know its not left over
|
||||
delete(pendingParent, studioJSON.Name)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *ImportTask) ImportMovies(ctx context.Context) {
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewMovieQueryBuilder()
|
||||
|
||||
@@ -10,8 +10,20 @@ type Studio struct {
|
||||
Checksum string `db:"checksum" json:"checksum"`
|
||||
Name sql.NullString `db:"name" json:"name"`
|
||||
URL sql.NullString `db:"url" json:"url"`
|
||||
ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type StudioPartial struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
Image *[]byte `db:"image" json:"image"`
|
||||
Checksum *string `db:"checksum" json:"checksum"`
|
||||
Name *sql.NullString `db:"name" json:"name"`
|
||||
URL *sql.NullString `db:"url" json:"url"`
|
||||
ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
|
||||
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
var DefaultStudioImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgVBQsJl1CMZAAAASJJREFUeNrt3N0JwyAYhlEj3cj9R3Cm5rbkqtAP+qrnGaCYHPwJpLlaa++mmLpbAERAgAgIEAEBIiBABERAgAgIEAEBIiBABERAgAgIEAHZuVflj40x4i94zhk9vqsVvEq6AsQqMP1EjORx20OACAgQRRx7T+zzcFBxcjNDfoB4ntQqTm5Awo7MlqywZxcgYQ+RlqywJ3ozJAQCSBiEJSsQA0gYBpDAgAARECACAkRAgAgIEAERECACAmSjUv6eAOSB8m8YIGGzBUjYbAESBgMkbBkDEjZbgITBAClcxiqQvEoatreYIWEBASIgJ4Gkf11ntXH3nS9uxfGWfJ5J9hAgAgJEQAQEiIAAERAgAgJEQAQEiIAAERAgAgJEQAQEiL7qBuc6RKLHxr0CAAAAAElFTkSuQmCC"
|
||||
|
||||
@@ -2,7 +2,6 @@ package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
@@ -148,7 +147,7 @@ func (qb *MovieQueryBuilder) Query(movieFilter *MovieFilterType, findFilter *Fin
|
||||
args = append(args, studioID)
|
||||
}
|
||||
|
||||
whereClause, havingClause := qb.getMultiCriterionClause("studio", "", "studio_id", studiosFilter)
|
||||
whereClause, havingClause := getMultiCriterionClause("movies", "studio", "", "", "studio_id", studiosFilter)
|
||||
whereClauses = appendClause(whereClauses, whereClause)
|
||||
havingClauses = appendClause(havingClauses, havingClause)
|
||||
}
|
||||
@@ -165,29 +164,6 @@ func (qb *MovieQueryBuilder) Query(movieFilter *MovieFilterType, findFilter *Fin
|
||||
return movies, countResult
|
||||
}
|
||||
|
||||
// returns where clause and having clause
|
||||
func (qb *MovieQueryBuilder) getMultiCriterionClause(table string, joinTable string, joinTableField string, criterion *MultiCriterionInput) (string, string) {
|
||||
whereClause := ""
|
||||
havingClause := ""
|
||||
if criterion.Modifier == CriterionModifierIncludes {
|
||||
// includes any of the provided ids
|
||||
whereClause = table + ".id IN " + getInBinding(len(criterion.Value))
|
||||
} else if criterion.Modifier == CriterionModifierIncludesAll {
|
||||
// includes all of the provided ids
|
||||
whereClause = table + ".id IN " + getInBinding(len(criterion.Value))
|
||||
havingClause = "count(distinct " + table + ".id) IS " + strconv.Itoa(len(criterion.Value))
|
||||
} else if criterion.Modifier == CriterionModifierExcludes {
|
||||
// excludes all of the provided ids
|
||||
if joinTable != "" {
|
||||
whereClause = "not exists (select " + joinTable + ".movie_id from " + joinTable + " where " + joinTable + ".movie_id = movies.id and " + joinTable + "." + joinTableField + " in " + getInBinding(len(criterion.Value)) + ")"
|
||||
} else {
|
||||
whereClause = "not exists (select m.id from movies as m where m.id = movies.id and m." + joinTableField + " in " + getInBinding(len(criterion.Value)) + ")"
|
||||
}
|
||||
}
|
||||
|
||||
return whereClause, havingClause
|
||||
}
|
||||
|
||||
func (qb *MovieQueryBuilder) getMovieSort(findFilter *FindFilterType) string {
|
||||
var sort string
|
||||
var direction string
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package models_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -86,6 +87,42 @@ func TestMovieFindByNames(t *testing.T) {
|
||||
assert.Equal(t, strings.ToLower(movieNames[movieIdxWithScene]), strings.ToLower(movies[1].Name.String))
|
||||
}
|
||||
|
||||
func TestMovieQueryStudio(t *testing.T) {
|
||||
mqb := models.NewMovieQueryBuilder()
|
||||
studioCriterion := models.MultiCriterionInput{
|
||||
Value: []string{
|
||||
strconv.Itoa(studioIDs[studioIdxWithMovie]),
|
||||
},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
}
|
||||
|
||||
movieFilter := models.MovieFilterType{
|
||||
Studios: &studioCriterion,
|
||||
}
|
||||
|
||||
movies, _ := mqb.Query(&movieFilter, nil)
|
||||
|
||||
assert.Len(t, movies, 1)
|
||||
|
||||
// ensure id is correct
|
||||
assert.Equal(t, movieIDs[movieIdxWithStudio], movies[0].ID)
|
||||
|
||||
studioCriterion = models.MultiCriterionInput{
|
||||
Value: []string{
|
||||
strconv.Itoa(studioIDs[studioIdxWithMovie]),
|
||||
},
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
}
|
||||
|
||||
q := getMovieStringValue(movieIdxWithStudio, titleField)
|
||||
findFilter := models.FindFilterType{
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
movies, _ = mqb.Query(&movieFilter, &findFilter)
|
||||
assert.Len(t, movies, 0)
|
||||
}
|
||||
|
||||
// TODO Update
|
||||
// TODO Destroy
|
||||
// TODO Find
|
||||
|
||||
@@ -2,7 +2,6 @@ package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
@@ -329,7 +328,7 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
|
||||
}
|
||||
|
||||
query.body += " LEFT JOIN tags on tags_join.tag_id = tags.id"
|
||||
whereClause, havingClause := getMultiCriterionClause("tags", "scenes_tags", "tag_id", tagsFilter)
|
||||
whereClause, havingClause := getMultiCriterionClause("scenes", "tags", "scenes_tags", "scene_id", "tag_id", tagsFilter)
|
||||
query.addWhere(whereClause)
|
||||
query.addHaving(havingClause)
|
||||
}
|
||||
@@ -340,7 +339,7 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
|
||||
}
|
||||
|
||||
query.body += " LEFT JOIN performers ON performers_join.performer_id = performers.id"
|
||||
whereClause, havingClause := getMultiCriterionClause("performers", "performers_scenes", "performer_id", performersFilter)
|
||||
whereClause, havingClause := getMultiCriterionClause("scenes", "performers", "performers_scenes", "scene_id", "performer_id", performersFilter)
|
||||
query.addWhere(whereClause)
|
||||
query.addHaving(havingClause)
|
||||
}
|
||||
@@ -350,7 +349,7 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
|
||||
query.addArg(studioID)
|
||||
}
|
||||
|
||||
whereClause, havingClause := getMultiCriterionClause("studio", "", "studio_id", studiosFilter)
|
||||
whereClause, havingClause := getMultiCriterionClause("scenes", "studio", "", "", "studio_id", studiosFilter)
|
||||
query.addWhere(whereClause)
|
||||
query.addHaving(havingClause)
|
||||
}
|
||||
@@ -361,7 +360,7 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
|
||||
}
|
||||
|
||||
query.body += " LEFT JOIN movies ON movies_join.movie_id = movies.id"
|
||||
whereClause, havingClause := getMultiCriterionClause("movies", "movies_scenes", "movie_id", moviesFilter)
|
||||
whereClause, havingClause := getMultiCriterionClause("scenes", "movies", "movies_scenes", "scene_id", "movie_id", moviesFilter)
|
||||
query.addWhere(whereClause)
|
||||
query.addHaving(havingClause)
|
||||
}
|
||||
@@ -414,29 +413,6 @@ func getDurationWhereClause(durationFilter IntCriterionInput) (string, []interfa
|
||||
return clause, args
|
||||
}
|
||||
|
||||
// returns where clause and having clause
|
||||
func getMultiCriterionClause(table string, joinTable string, joinTableField string, criterion *MultiCriterionInput) (string, string) {
|
||||
whereClause := ""
|
||||
havingClause := ""
|
||||
if criterion.Modifier == CriterionModifierIncludes {
|
||||
// includes any of the provided ids
|
||||
whereClause = table + ".id IN " + getInBinding(len(criterion.Value))
|
||||
} else if criterion.Modifier == CriterionModifierIncludesAll {
|
||||
// includes all of the provided ids
|
||||
whereClause = table + ".id IN " + getInBinding(len(criterion.Value))
|
||||
havingClause = "count(distinct " + table + ".id) IS " + strconv.Itoa(len(criterion.Value))
|
||||
} else if criterion.Modifier == CriterionModifierExcludes {
|
||||
// excludes all of the provided ids
|
||||
if joinTable != "" {
|
||||
whereClause = "not exists (select " + joinTable + ".scene_id from " + joinTable + " where " + joinTable + ".scene_id = scenes.id and " + joinTable + "." + joinTableField + " in " + getInBinding(len(criterion.Value)) + ")"
|
||||
} else {
|
||||
whereClause = "not exists (select s.id from scenes as s where s.id = scenes.id and s." + joinTableField + " in " + getInBinding(len(criterion.Value)) + ")"
|
||||
}
|
||||
}
|
||||
|
||||
return whereClause, havingClause
|
||||
}
|
||||
|
||||
func (qb *SceneQueryBuilder) QueryAllByPathRegex(regex string) ([]*Scene, error) {
|
||||
var args []interface{}
|
||||
body := selectDistinctIDs("scenes") + " WHERE scenes.path regexp ?"
|
||||
|
||||
@@ -676,6 +676,42 @@ func TestSceneQueryStudio(t *testing.T) {
|
||||
assert.Len(t, scenes, 0)
|
||||
}
|
||||
|
||||
func TestSceneQueryMovies(t *testing.T) {
|
||||
sqb := models.NewSceneQueryBuilder()
|
||||
movieCriterion := models.MultiCriterionInput{
|
||||
Value: []string{
|
||||
strconv.Itoa(movieIDs[movieIdxWithScene]),
|
||||
},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
}
|
||||
|
||||
sceneFilter := models.SceneFilterType{
|
||||
Movies: &movieCriterion,
|
||||
}
|
||||
|
||||
scenes, _ := sqb.Query(&sceneFilter, nil)
|
||||
|
||||
assert.Len(t, scenes, 1)
|
||||
|
||||
// ensure id is correct
|
||||
assert.Equal(t, sceneIDs[sceneIdxWithMovie], scenes[0].ID)
|
||||
|
||||
movieCriterion = models.MultiCriterionInput{
|
||||
Value: []string{
|
||||
strconv.Itoa(movieIDs[movieIdxWithScene]),
|
||||
},
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
}
|
||||
|
||||
q := getSceneStringValue(sceneIdxWithMovie, titleField)
|
||||
findFilter := models.FindFilterType{
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
scenes, _ = sqb.Query(&sceneFilter, &findFilter)
|
||||
assert.Len(t, scenes, 0)
|
||||
}
|
||||
|
||||
func TestSceneQuerySorting(t *testing.T) {
|
||||
sort := titleField
|
||||
direction := models.SortDirectionEnumAsc
|
||||
|
||||
@@ -239,6 +239,29 @@ func getIntCriterionWhereClause(column string, input IntCriterionInput) (string,
|
||||
return column + " " + binding, count
|
||||
}
|
||||
|
||||
// returns where clause and having clause
|
||||
func getMultiCriterionClause(primaryTable, foreignTable, joinTable, primaryFK, foreignFK string, criterion *MultiCriterionInput) (string, string) {
|
||||
whereClause := ""
|
||||
havingClause := ""
|
||||
if criterion.Modifier == CriterionModifierIncludes {
|
||||
// includes any of the provided ids
|
||||
whereClause = foreignTable + ".id IN " + getInBinding(len(criterion.Value))
|
||||
} else if criterion.Modifier == CriterionModifierIncludesAll {
|
||||
// includes all of the provided ids
|
||||
whereClause = foreignTable + ".id IN " + getInBinding(len(criterion.Value))
|
||||
havingClause = "count(distinct " + foreignTable + ".id) IS " + strconv.Itoa(len(criterion.Value))
|
||||
} else if criterion.Modifier == CriterionModifierExcludes {
|
||||
// excludes all of the provided ids
|
||||
if joinTable != "" {
|
||||
whereClause = "not exists (select " + joinTable + "." + primaryFK + " from " + joinTable + " where " + joinTable + "." + primaryFK + " = " + primaryTable + ".id and " + joinTable + "." + foreignFK + " in " + getInBinding(len(criterion.Value)) + ")"
|
||||
} else {
|
||||
whereClause = "not exists (select s.id from " + primaryTable + " as s where s.id = " + primaryTable + ".id and s." + foreignFK + " in " + getInBinding(len(criterion.Value)) + ")"
|
||||
}
|
||||
}
|
||||
|
||||
return whereClause, havingClause
|
||||
}
|
||||
|
||||
func runIdsQuery(query string, args []interface{}) ([]int, error) {
|
||||
var result []struct {
|
||||
Int int `db:"id"`
|
||||
|
||||
@@ -16,8 +16,8 @@ func NewStudioQueryBuilder() StudioQueryBuilder {
|
||||
func (qb *StudioQueryBuilder) Create(newStudio Studio, tx *sqlx.Tx) (*Studio, error) {
|
||||
ensureTx(tx)
|
||||
result, err := tx.NamedExec(
|
||||
`INSERT INTO studios (image, checksum, name, url, created_at, updated_at)
|
||||
VALUES (:image, :checksum, :name, :url, :created_at, :updated_at)
|
||||
`INSERT INTO studios (image, checksum, name, url, parent_id, created_at, updated_at)
|
||||
VALUES (:image, :checksum, :name, :url, :parent_id, :created_at, :updated_at)
|
||||
`,
|
||||
newStudio,
|
||||
)
|
||||
@@ -35,20 +35,21 @@ func (qb *StudioQueryBuilder) Create(newStudio Studio, tx *sqlx.Tx) (*Studio, er
|
||||
return &newStudio, nil
|
||||
}
|
||||
|
||||
func (qb *StudioQueryBuilder) Update(updatedStudio Studio, tx *sqlx.Tx) (*Studio, error) {
|
||||
func (qb *StudioQueryBuilder) Update(updatedStudio StudioPartial, tx *sqlx.Tx) (*Studio, error) {
|
||||
ensureTx(tx)
|
||||
_, err := tx.NamedExec(
|
||||
`UPDATE studios SET `+SQLGenKeys(updatedStudio)+` WHERE studios.id = :id`,
|
||||
`UPDATE studios SET `+SQLGenKeysPartial(updatedStudio)+` WHERE studios.id = :id`,
|
||||
updatedStudio,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Get(&updatedStudio, `SELECT * FROM studios WHERE id = ? LIMIT 1`, updatedStudio.ID); err != nil {
|
||||
var ret Studio
|
||||
if err := tx.Get(&ret, `SELECT * FROM studios WHERE id = ? LIMIT 1`, updatedStudio.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &updatedStudio, nil
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (qb *StudioQueryBuilder) Destroy(id string, tx *sqlx.Tx) error {
|
||||
@@ -73,6 +74,12 @@ func (qb *StudioQueryBuilder) Find(id int, tx *sqlx.Tx) (*Studio, error) {
|
||||
return qb.queryStudio(query, args, tx)
|
||||
}
|
||||
|
||||
func (qb *StudioQueryBuilder) FindChildren(id int, tx *sqlx.Tx) ([]*Studio, error) {
|
||||
query := "SELECT studios.* FROM studios WHERE studios.parent_id = ?"
|
||||
args := []interface{}{id}
|
||||
return qb.queryStudios(query, args, tx)
|
||||
}
|
||||
|
||||
func (qb *StudioQueryBuilder) FindBySceneID(sceneID int) (*Studio, error) {
|
||||
query := "SELECT studios.* FROM studios JOIN scenes ON studios.id = scenes.studio_id WHERE scenes.id = ? LIMIT 1"
|
||||
args := []interface{}{sceneID}
|
||||
@@ -101,7 +108,10 @@ func (qb *StudioQueryBuilder) AllSlim() ([]*Studio, error) {
|
||||
return qb.queryStudios("SELECT studios.id, studios.name FROM studios "+qb.getStudioSort(nil), nil, nil)
|
||||
}
|
||||
|
||||
func (qb *StudioQueryBuilder) Query(findFilter *FindFilterType) ([]*Studio, int) {
|
||||
func (qb *StudioQueryBuilder) Query(studioFilter *StudioFilterType, findFilter *FindFilterType) ([]*Studio, int) {
|
||||
if studioFilter == nil {
|
||||
studioFilter = &StudioFilterType{}
|
||||
}
|
||||
if findFilter == nil {
|
||||
findFilter = &FindFilterType{}
|
||||
}
|
||||
@@ -116,11 +126,26 @@ func (qb *StudioQueryBuilder) Query(findFilter *FindFilterType) ([]*Studio, int)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
searchColumns := []string{"studios.name"}
|
||||
|
||||
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
|
||||
whereClauses = append(whereClauses, clause)
|
||||
args = append(args, thisArgs...)
|
||||
}
|
||||
|
||||
if parentsFilter := studioFilter.Parents; parentsFilter != nil && len(parentsFilter.Value) > 0 {
|
||||
body += `
|
||||
left join studios as parent_studio on parent_studio.id = studios.parent_id
|
||||
`
|
||||
|
||||
for _, studioID := range parentsFilter.Value {
|
||||
args = append(args, studioID)
|
||||
}
|
||||
|
||||
whereClause, havingClause := getMultiCriterionClause("studios", "parent_studio", "", "", "parent_id", parentsFilter)
|
||||
whereClauses = appendClause(whereClauses, whereClause)
|
||||
havingClauses = appendClause(havingClauses, havingClause)
|
||||
}
|
||||
|
||||
sortAndPagination := qb.getStudioSort(findFilter) + getPagination(findFilter)
|
||||
idsResult, countResult := executeFindQuery("studios", body, args, sortAndPagination, whereClauses, havingClauses)
|
||||
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
package models_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -39,6 +43,173 @@ func TestStudioFindByName(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestStudioQueryParent(t *testing.T) {
|
||||
sqb := models.NewStudioQueryBuilder()
|
||||
studioCriterion := models.MultiCriterionInput{
|
||||
Value: []string{
|
||||
strconv.Itoa(studioIDs[studioIdxWithChildStudio]),
|
||||
},
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
}
|
||||
|
||||
studioFilter := models.StudioFilterType{
|
||||
Parents: &studioCriterion,
|
||||
}
|
||||
|
||||
studios, _ := sqb.Query(&studioFilter, nil)
|
||||
|
||||
assert.Len(t, studios, 1)
|
||||
|
||||
// ensure id is correct
|
||||
assert.Equal(t, sceneIDs[studioIdxWithParentStudio], studios[0].ID)
|
||||
|
||||
studioCriterion = models.MultiCriterionInput{
|
||||
Value: []string{
|
||||
strconv.Itoa(studioIDs[studioIdxWithChildStudio]),
|
||||
},
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
}
|
||||
|
||||
q := getStudioStringValue(studioIdxWithParentStudio, titleField)
|
||||
findFilter := models.FindFilterType{
|
||||
Q: &q,
|
||||
}
|
||||
|
||||
studios, _ = sqb.Query(&studioFilter, &findFilter)
|
||||
assert.Len(t, studios, 0)
|
||||
}
|
||||
|
||||
func TestStudioDestroyParent(t *testing.T) {
|
||||
const parentName = "parent"
|
||||
const childName = "child"
|
||||
|
||||
// create parent and child studios
|
||||
ctx := context.TODO()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
createdParent, err := createStudio(tx, parentName, nil)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error creating parent studio: %s", err.Error())
|
||||
}
|
||||
|
||||
parentID := int64(createdParent.ID)
|
||||
createdChild, err := createStudio(tx, childName, &parentID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error creating child studio: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error committing: %s", err.Error())
|
||||
}
|
||||
|
||||
sqb := models.NewStudioQueryBuilder()
|
||||
|
||||
// destroy the parent
|
||||
tx = database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
err = sqb.Destroy(strconv.Itoa(createdParent.ID), tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error destroying parent studio: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error committing: %s", err.Error())
|
||||
}
|
||||
|
||||
// destroy the child
|
||||
tx = database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
err = sqb.Destroy(strconv.Itoa(createdChild.ID), tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error destroying child studio: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error committing: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStudioFindChildren(t *testing.T) {
|
||||
sqb := models.NewStudioQueryBuilder()
|
||||
|
||||
studios, err := sqb.FindChildren(studioIDs[studioIdxWithChildStudio], nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error calling FindChildren: %s", err.Error())
|
||||
}
|
||||
|
||||
assert.Len(t, studios, 1)
|
||||
assert.Equal(t, studioIDs[studioIdxWithParentStudio], studios[0].ID)
|
||||
|
||||
studios, err = sqb.FindChildren(0, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error calling FindChildren: %s", err.Error())
|
||||
}
|
||||
|
||||
assert.Len(t, studios, 0)
|
||||
}
|
||||
|
||||
func TestStudioUpdateClearParent(t *testing.T) {
|
||||
const parentName = "clearParent_parent"
|
||||
const childName = "clearParent_child"
|
||||
|
||||
// create parent and child studios
|
||||
ctx := context.TODO()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
createdParent, err := createStudio(tx, parentName, nil)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error creating parent studio: %s", err.Error())
|
||||
}
|
||||
|
||||
parentID := int64(createdParent.ID)
|
||||
createdChild, err := createStudio(tx, childName, &parentID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error creating child studio: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error committing: %s", err.Error())
|
||||
}
|
||||
|
||||
sqb := models.NewStudioQueryBuilder()
|
||||
|
||||
// clear the parent id from the child
|
||||
tx = database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
updatePartial := models.StudioPartial{
|
||||
ID: createdChild.ID,
|
||||
ParentID: &sql.NullInt64{Valid: false},
|
||||
}
|
||||
|
||||
updatedStudio, err := sqb.Update(updatePartial, tx)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error updated studio: %s", err.Error())
|
||||
}
|
||||
|
||||
if updatedStudio.ParentID.Valid {
|
||||
t.Error("updated studio has parent ID set")
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatalf("Error committing: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Create
|
||||
// TODO Update
|
||||
// TODO Destroy
|
||||
|
||||
@@ -22,12 +22,12 @@ import (
|
||||
const totalScenes = 12
|
||||
const performersNameCase = 3
|
||||
const performersNameNoCase = 2
|
||||
const moviesNameCase = 1
|
||||
const moviesNameCase = 2
|
||||
const moviesNameNoCase = 1
|
||||
const totalGalleries = 1
|
||||
const tagsNameNoCase = 2
|
||||
const tagsNameCase = 5
|
||||
const studiosNameCase = 1
|
||||
const studiosNameCase = 4
|
||||
const studiosNameNoCase = 1
|
||||
|
||||
var sceneIDs []int
|
||||
@@ -61,9 +61,10 @@ const performerIdx1WithDupName = 3
|
||||
const performerIdxWithDupName = 4
|
||||
|
||||
const movieIdxWithScene = 0
|
||||
const movieIdxWithStudio = 1
|
||||
|
||||
// movies with dup names start from the end
|
||||
const movieIdxWithDupName = 1
|
||||
const movieIdxWithDupName = 2
|
||||
|
||||
const galleryIdxWithScene = 0
|
||||
|
||||
@@ -78,9 +79,12 @@ const tagIdx1WithDupName = 5
|
||||
const tagIdxWithDupName = 6
|
||||
|
||||
const studioIdxWithScene = 0
|
||||
const studioIdxWithMovie = 1
|
||||
const studioIdxWithChildStudio = 2
|
||||
const studioIdxWithParentStudio = 3
|
||||
|
||||
// studios with dup names start from the end
|
||||
const studioIdxWithDupName = 1
|
||||
const studioIdxWithDupName = 4
|
||||
|
||||
const markerIdxWithScene = 0
|
||||
|
||||
@@ -196,6 +200,16 @@ func populateDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := linkMovieStudio(tx, movieIdxWithStudio, studioIdxWithMovie); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := linkStudioParent(tx, studioIdxWithChildStudio, studioIdxWithParentStudio); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := createMarker(tx, sceneIdxWithMarker, tagIdxWithPrimaryMarker, []int{tagIdxWithMarker}); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
@@ -312,10 +326,9 @@ func createMovies(tx *sqlx.Tx, n int, o int) error {
|
||||
const namePlain = "Name"
|
||||
const nameNoCase = "NaMe"
|
||||
|
||||
name := namePlain
|
||||
|
||||
for i := 0; i < n+o; i++ {
|
||||
index := i
|
||||
name := namePlain
|
||||
|
||||
if i >= n { // i<n tags get normal names
|
||||
name = nameNoCase // i>=n movies get dup names if case is not checked
|
||||
@@ -323,8 +336,9 @@ func createMovies(tx *sqlx.Tx, n int, o int) error {
|
||||
} // so count backwards to 0 as needed
|
||||
// movies [ i ] and [ n + o - i - 1 ] should have similar names with only the Name!=NaMe part different
|
||||
|
||||
name = getMovieStringValue(index, name)
|
||||
movie := models.Movie{
|
||||
Name: sql.NullString{String: getMovieStringValue(index, name), Valid: true},
|
||||
Name: sql.NullString{String: name, Valid: true},
|
||||
FrontImage: []byte(models.DefaultMovieImage),
|
||||
Checksum: utils.MD5FromString(name),
|
||||
}
|
||||
@@ -332,7 +346,7 @@ func createMovies(tx *sqlx.Tx, n int, o int) error {
|
||||
created, err := mqb.Create(movie, tx)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating movie %v+: %s", movie, err.Error())
|
||||
return fmt.Errorf("Error creating movie [%d] %v+: %s", i, movie, err.Error())
|
||||
}
|
||||
|
||||
movieIDs = append(movieIDs, created.ID)
|
||||
@@ -432,16 +446,35 @@ func getStudioStringValue(index int, field string) string {
|
||||
return "studio_" + strconv.FormatInt(int64(index), 10) + "_" + field
|
||||
}
|
||||
|
||||
func createStudio(tx *sqlx.Tx, name string, parentID *int64) (*models.Studio, error) {
|
||||
sqb := models.NewStudioQueryBuilder()
|
||||
studio := models.Studio{
|
||||
Name: sql.NullString{String: name, Valid: true},
|
||||
Image: []byte(models.DefaultStudioImage),
|
||||
Checksum: utils.MD5FromString(name),
|
||||
}
|
||||
|
||||
if parentID != nil {
|
||||
studio.ParentID = sql.NullInt64{Int64: *parentID, Valid: true}
|
||||
}
|
||||
|
||||
created, err := sqb.Create(studio, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error creating studio %v+: %s", studio, err.Error())
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
//createStudios creates n studios with plain Name and o studios with camel cased NaMe included
|
||||
func createStudios(tx *sqlx.Tx, n int, o int) error {
|
||||
sqb := models.NewStudioQueryBuilder()
|
||||
const namePlain = "Name"
|
||||
const nameNoCase = "NaMe"
|
||||
|
||||
name := namePlain
|
||||
|
||||
for i := 0; i < n+o; i++ {
|
||||
index := i
|
||||
name := namePlain
|
||||
|
||||
if i >= n { // i<n studios get normal names
|
||||
name = nameNoCase // i>=n studios get dup names if case is not checked
|
||||
@@ -449,16 +482,11 @@ func createStudios(tx *sqlx.Tx, n int, o int) error {
|
||||
} // so count backwards to 0 as needed
|
||||
// studios [ i ] and [ n + o - i - 1 ] should have similar names with only the Name!=NaMe part different
|
||||
|
||||
tag := models.Studio{
|
||||
Name: sql.NullString{String: getStudioStringValue(index, name), Valid: true},
|
||||
Image: []byte(models.DefaultStudioImage),
|
||||
Checksum: utils.MD5FromString(name),
|
||||
}
|
||||
|
||||
created, err := sqb.Create(tag, tx)
|
||||
name = getStudioStringValue(index, name)
|
||||
created, err := createStudio(tx, name, nil)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating studio %v+: %s", tag, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
studioIDs = append(studioIDs, created.ID)
|
||||
@@ -582,3 +610,27 @@ func linkSceneStudio(tx *sqlx.Tx, sceneIndex, studioIndex int) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func linkMovieStudio(tx *sqlx.Tx, movieIndex, studioIndex int) error {
|
||||
mqb := models.NewMovieQueryBuilder()
|
||||
|
||||
movie := models.MoviePartial{
|
||||
ID: movieIDs[movieIndex],
|
||||
StudioID: &sql.NullInt64{Int64: int64(studioIDs[studioIndex]), Valid: true},
|
||||
}
|
||||
_, err := mqb.Update(movie, tx)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func linkStudioParent(tx *sqlx.Tx, parentIndex, childIndex int) error {
|
||||
sqb := models.NewStudioQueryBuilder()
|
||||
|
||||
studio := models.StudioPartial{
|
||||
ID: studioIDs[childIndex],
|
||||
ParentID: &sql.NullInt64{Int64: int64(studioIDs[parentIndex]), Valid: true},
|
||||
}
|
||||
_, err := sqb.Update(studio, tx)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
const markup = `
|
||||
### ✨ New Features
|
||||
* Add support for parent/child studios.
|
||||
|
||||
### 🎨 Improvements
|
||||
* Show rating as stars in scene page.
|
||||
* Add reload scrapers button.
|
||||
|
||||
@@ -140,6 +140,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
if (
|
||||
criterion.type !== "performers" &&
|
||||
criterion.type !== "studios" &&
|
||||
criterion.type !== "parent_studios" &&
|
||||
criterion.type !== "tags" &&
|
||||
criterion.type !== "sceneTags" &&
|
||||
criterion.type !== "movies"
|
||||
|
||||
@@ -23,7 +23,13 @@ type ValidTypes =
|
||||
type Option = { value: string; label: string };
|
||||
|
||||
interface ITypeProps {
|
||||
type?: "performers" | "studios" | "tags" | "sceneTags" | "movies";
|
||||
type?:
|
||||
| "performers"
|
||||
| "studios"
|
||||
| "parent_studios"
|
||||
| "tags"
|
||||
| "sceneTags"
|
||||
| "movies";
|
||||
}
|
||||
interface IFilterProps {
|
||||
ids?: string[];
|
||||
@@ -175,7 +181,7 @@ export const MarkerTitleSuggest: React.FC<IMarkerSuggestProps> = (props) => {
|
||||
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) =>
|
||||
props.type === "performers" ? (
|
||||
<PerformerSelect {...(props as IFilterProps)} />
|
||||
) : props.type === "studios" ? (
|
||||
) : props.type === "studios" || props.type === "parent_studios" ? (
|
||||
<StudioSelect {...(props as IFilterProps)} />
|
||||
) : props.type === "movies" ? (
|
||||
<MovieSelect {...(props as IFilterProps)} />
|
||||
|
||||
@@ -3,12 +3,45 @@ import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { FormattedPlural } from "react-intl";
|
||||
import { NavUtils } from "src/utils";
|
||||
|
||||
interface IProps {
|
||||
studio: GQL.StudioDataFragment;
|
||||
hideParent?: boolean;
|
||||
}
|
||||
|
||||
export const StudioCard: React.FC<IProps> = ({ studio }) => {
|
||||
function maybeRenderParent(
|
||||
studio: GQL.StudioDataFragment,
|
||||
hideParent?: boolean
|
||||
) {
|
||||
if (!hideParent && studio.parent_studio) {
|
||||
return (
|
||||
<div>
|
||||
Part of
|
||||
<Link to={`/studios/${studio.parent_studio.id}`}>
|
||||
{studio.parent_studio.name}
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderChildren(studio: GQL.StudioDataFragment) {
|
||||
if (studio.child_studios.length > 0) {
|
||||
return (
|
||||
<div>
|
||||
Parent of
|
||||
<Link to={NavUtils.makeChildStudiosUrl(studio)}>
|
||||
{studio.child_studios.length} studios
|
||||
</Link>
|
||||
.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const StudioCard: React.FC<IProps> = ({ studio, hideParent }) => {
|
||||
return (
|
||||
<Card className="studio-card">
|
||||
<Link to={`/studios/${studio.id}`} className="studio-card-header">
|
||||
@@ -29,6 +62,8 @@ export const StudioCard: React.FC<IProps> = ({ studio }) => {
|
||||
/>
|
||||
.
|
||||
</span>
|
||||
{maybeRenderParent(studio, hideParent)}
|
||||
{maybeRenderChildren(studio)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
|
||||
import { Table } from "react-bootstrap";
|
||||
import { Table, Tabs, Tab } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
import cx from "classnames";
|
||||
@@ -18,9 +18,11 @@ import {
|
||||
DetailsEditNavbar,
|
||||
Modal,
|
||||
LoadingIndicator,
|
||||
StudioSelect,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { StudioScenesPanel } from "./StudioScenesPanel";
|
||||
import { StudioChildrenPanel } from "./StudioChildrenPanel";
|
||||
|
||||
export const Studio: React.FC = () => {
|
||||
const history = useHistory();
|
||||
@@ -36,6 +38,7 @@ export const Studio: React.FC = () => {
|
||||
const [image, setImage] = useState<string>();
|
||||
const [name, setName] = useState<string>();
|
||||
const [url, setUrl] = useState<string>();
|
||||
const [parentStudioId, setParentStudioId] = useState<string>();
|
||||
|
||||
// Studio state
|
||||
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
||||
@@ -55,6 +58,7 @@ export const Studio: React.FC = () => {
|
||||
function updateStudioEditState(state: Partial<GQL.StudioDataFragment>) {
|
||||
setName(state.name);
|
||||
setUrl(state.url ?? undefined);
|
||||
setParentStudioId(state?.parent_studio?.id ?? undefined);
|
||||
}
|
||||
|
||||
function updateStudioData(studioData: Partial<GQL.StudioDataFragment>) {
|
||||
@@ -89,6 +93,7 @@ export const Studio: React.FC = () => {
|
||||
const input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput> = {
|
||||
name,
|
||||
url,
|
||||
parent_id: parentStudioId,
|
||||
image,
|
||||
};
|
||||
|
||||
@@ -189,6 +194,20 @@ export const Studio: React.FC = () => {
|
||||
isEditing: !!isEditing,
|
||||
onChange: setUrl,
|
||||
})}
|
||||
<tr>
|
||||
<td>Parent Studio</td>
|
||||
<td>
|
||||
<StudioSelect
|
||||
onSelect={(items) =>
|
||||
setParentStudioId(
|
||||
items.length > 0 ? items[0]?.id : undefined
|
||||
)
|
||||
}
|
||||
ids={parentStudioId ? [parentStudioId] : []}
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
<DetailsEditNavbar
|
||||
@@ -205,7 +224,14 @@ export const Studio: React.FC = () => {
|
||||
</div>
|
||||
{!isNew && (
|
||||
<div className="col-12 col-sm-8">
|
||||
<StudioScenesPanel studio={studio} />
|
||||
<Tabs id="studio-tabs" mountOnEnter>
|
||||
<Tab eventKey="studio-scenes-panel" title="Scenes">
|
||||
<StudioScenesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab eventKey="studio-children-panel" title="Child Studios">
|
||||
<StudioChildrenPanel studio={studio} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
{renderDeleteAlert()}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ParentStudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { StudioList } from "../StudioList";
|
||||
|
||||
interface IStudioChildrenPanel {
|
||||
studio: Partial<GQL.StudioDataFragment>;
|
||||
}
|
||||
|
||||
export const StudioChildrenPanel: React.FC<IStudioChildrenPanel> = ({
|
||||
studio,
|
||||
}) => {
|
||||
function filterHook(filter: ListFilterModel) {
|
||||
const studioValue = { id: studio.id!, label: studio.name! };
|
||||
// if studio is already present, then we modify it, otherwise add
|
||||
let parentStudioCriterion = filter.criteria.find((c) => {
|
||||
return c.type === "parent_studios";
|
||||
}) as ParentStudiosCriterion;
|
||||
|
||||
if (
|
||||
parentStudioCriterion &&
|
||||
(parentStudioCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
||||
parentStudioCriterion.modifier === GQL.CriterionModifier.Includes)
|
||||
) {
|
||||
// add the studio if not present
|
||||
if (
|
||||
!parentStudioCriterion.value.find((p) => {
|
||||
return p.id === studio.id;
|
||||
})
|
||||
) {
|
||||
parentStudioCriterion.value.push(studioValue);
|
||||
}
|
||||
|
||||
parentStudioCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
||||
} else {
|
||||
// overwrite
|
||||
parentStudioCriterion = new ParentStudiosCriterion();
|
||||
parentStudioCriterion.value = [studioValue];
|
||||
filter.criteria.push(parentStudioCriterion);
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
return <StudioList fromParent filterHook={filterHook} />;
|
||||
};
|
||||
@@ -5,9 +5,19 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { StudioCard } from "./StudioCard";
|
||||
|
||||
export const StudioList: React.FC = () => {
|
||||
interface IStudioList {
|
||||
fromParent?: boolean;
|
||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||
}
|
||||
|
||||
export const StudioList: React.FC<IStudioList> = ({
|
||||
fromParent,
|
||||
filterHook,
|
||||
}) => {
|
||||
const listData = useStudiosList({
|
||||
renderContent,
|
||||
subComponent: fromParent,
|
||||
filterHook,
|
||||
});
|
||||
|
||||
function renderContent(
|
||||
@@ -20,7 +30,11 @@ export const StudioList: React.FC = () => {
|
||||
return (
|
||||
<div className="row px-xl-5 justify-content-center">
|
||||
{result.data.findStudios.studios.map((studio) => (
|
||||
<StudioCard key={studio.id} studio={studio} />
|
||||
<StudioCard
|
||||
key={studio.id}
|
||||
studio={studio}
|
||||
hideParent={fromParent}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -74,6 +74,7 @@ export const useFindStudios = (filter: ListFilterModel) =>
|
||||
GQL.useFindStudiosQuery({
|
||||
variables: {
|
||||
filter: filter.makeFindFilter(),
|
||||
studio_filter: filter.makeStudioFilter(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ export type CriterionType =
|
||||
| "tattoos"
|
||||
| "piercings"
|
||||
| "aliases"
|
||||
| "gender";
|
||||
| "gender"
|
||||
| "parent_studios";
|
||||
|
||||
type Option = string | number | IOptionType;
|
||||
export type CriterionValue = string | number | ILabeledId[];
|
||||
@@ -93,6 +94,8 @@ export abstract class Criterion {
|
||||
return "Aliases";
|
||||
case "gender":
|
||||
return "Gender";
|
||||
case "parent_studios":
|
||||
return "Parent Studios";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,3 +24,13 @@ export class StudiosCriterionOption implements ICriterionOption {
|
||||
public label: string = Criterion.getLabel("studios");
|
||||
public value: CriterionType = "studios";
|
||||
}
|
||||
|
||||
export class ParentStudiosCriterion extends StudiosCriterion {
|
||||
public type: CriterionType = "parent_studios";
|
||||
public parameterName: string = "parents";
|
||||
}
|
||||
|
||||
export class ParentStudiosCriterionOption implements ICriterionOption {
|
||||
public label: string = Criterion.getLabel("parent_studios");
|
||||
public value: CriterionType = "parent_studios";
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import { NoneCriterion } from "./none";
|
||||
import { PerformersCriterion } from "./performers";
|
||||
import { RatingCriterion } from "./rating";
|
||||
import { ResolutionCriterion } from "./resolution";
|
||||
import { StudiosCriterion } from "./studios";
|
||||
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
|
||||
import { TagsCriterion } from "./tags";
|
||||
import { GenderCriterion } from "./gender";
|
||||
import { MoviesCriterion } from "./movies";
|
||||
@@ -50,6 +50,8 @@ export function makeCriteria(type: CriterionType = "none") {
|
||||
return new PerformersCriterion();
|
||||
case "studios":
|
||||
return new StudiosCriterion();
|
||||
case "parent_studios":
|
||||
return new ParentStudiosCriterion();
|
||||
case "movies":
|
||||
return new MoviesCriterion();
|
||||
case "birth_year":
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SceneMarkerFilterType,
|
||||
SortDirectionEnum,
|
||||
MovieFilterType,
|
||||
StudioFilterType,
|
||||
} from "src/core/generated-graphql";
|
||||
import { stringToGender } from "src/core/StashService";
|
||||
import {
|
||||
@@ -41,7 +42,12 @@ import {
|
||||
ResolutionCriterion,
|
||||
ResolutionCriterionOption,
|
||||
} from "./criteria/resolution";
|
||||
import { StudiosCriterion, StudiosCriterionOption } from "./criteria/studios";
|
||||
import {
|
||||
StudiosCriterion,
|
||||
StudiosCriterionOption,
|
||||
ParentStudiosCriterion,
|
||||
ParentStudiosCriterionOption,
|
||||
} from "./criteria/studios";
|
||||
import {
|
||||
SceneTagsCriterionOption,
|
||||
TagsCriterion,
|
||||
@@ -159,7 +165,10 @@ export class ListFilterModel {
|
||||
this.sortBy = "name";
|
||||
this.sortByOptions = ["name", "scenes_count"];
|
||||
this.displayModeOptions = [DisplayMode.Grid];
|
||||
this.criterionOptions = [new NoneCriterionOption()];
|
||||
this.criterionOptions = [
|
||||
new NoneCriterionOption(),
|
||||
new ParentStudiosCriterionOption(),
|
||||
];
|
||||
break;
|
||||
case FilterMode.Movies:
|
||||
this.sortBy = "name";
|
||||
@@ -576,4 +585,22 @@ export class ListFilterModel {
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public makeStudioFilter(): StudioFilterType {
|
||||
const result: StudioFilterType = {};
|
||||
this.criteria.forEach((criterion) => {
|
||||
switch (criterion.type) {
|
||||
case "parent_studios": {
|
||||
const studCrit = criterion as ParentStudiosCriterion;
|
||||
result.parents = {
|
||||
value: studCrit.value.map((studio) => studio.id),
|
||||
modifier: studCrit.modifier,
|
||||
};
|
||||
break;
|
||||
}
|
||||
// no default
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
||||
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||
import {
|
||||
StudiosCriterion,
|
||||
ParentStudiosCriterion,
|
||||
} from "src/models/list-filter/criteria/studios";
|
||||
import { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { FilterMode } from "src/models/list-filter/types";
|
||||
@@ -30,6 +33,17 @@ const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||
return `/scenes?${filter.makeQueryParameters()}`;
|
||||
};
|
||||
|
||||
const makeChildStudiosUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||
if (!studio.id) return "#";
|
||||
const filter = new ListFilterModel(FilterMode.Studios);
|
||||
const criterion = new ParentStudiosCriterion();
|
||||
criterion.value = [
|
||||
{ id: studio.id, label: studio.name || `Studio ${studio.id}` },
|
||||
];
|
||||
filter.criteria.push(criterion);
|
||||
return `/studios?${filter.makeQueryParameters()}`;
|
||||
};
|
||||
|
||||
const makeMovieScenesUrl = (movie: Partial<GQL.MovieDataFragment>) => {
|
||||
if (!movie.id) return "#";
|
||||
const filter = new ListFilterModel(FilterMode.Scenes);
|
||||
@@ -73,4 +87,5 @@ export default {
|
||||
makeTagScenesUrl,
|
||||
makeSceneMarkerUrl,
|
||||
makeMovieScenesUrl,
|
||||
makeChildStudiosUrl,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user