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
|
checksum
|
||||||
name
|
name
|
||||||
url
|
url
|
||||||
|
parent_studio {
|
||||||
|
id
|
||||||
|
checksum
|
||||||
|
name
|
||||||
|
url
|
||||||
|
image_path
|
||||||
|
scene_count
|
||||||
|
}
|
||||||
|
child_studios {
|
||||||
|
id
|
||||||
|
checksum
|
||||||
|
name
|
||||||
|
url
|
||||||
|
image_path
|
||||||
|
scene_count
|
||||||
|
}
|
||||||
image_path
|
image_path
|
||||||
scene_count
|
scene_count
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
mutation StudioCreate(
|
mutation StudioCreate(
|
||||||
$name: String!,
|
$name: String!,
|
||||||
$url: 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
|
...StudioData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,9 +13,10 @@ mutation StudioUpdate(
|
|||||||
$id: ID!
|
$id: ID!
|
||||||
$name: String,
|
$name: String,
|
||||||
$url: 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
|
...StudioData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query FindStudios($filter: FindFilterType) {
|
query FindStudios($filter: FindFilterType, $studio_filter: StudioFilterType ) {
|
||||||
findStudios(filter: $filter) {
|
findStudios(filter: $filter, studio_filter: $studio_filter) {
|
||||||
count
|
count
|
||||||
studios {
|
studios {
|
||||||
...StudioData
|
...StudioData
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type Query {
|
|||||||
"""Find a studio by ID"""
|
"""Find a studio by ID"""
|
||||||
findStudio(id: ID!): Studio
|
findStudio(id: ID!): Studio
|
||||||
"""A function which queries Studio objects"""
|
"""A function which queries Studio objects"""
|
||||||
findStudios(filter: FindFilterType): FindStudiosResultType!
|
findStudios(studio_filter: StudioFilterType, filter: FindFilterType): FindStudiosResultType!
|
||||||
|
|
||||||
"""Find a movie by ID"""
|
"""Find a movie by ID"""
|
||||||
findMovie(id: ID!): Movie
|
findMovie(id: ID!): Movie
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ input MovieFilterType {
|
|||||||
studios: MultiCriterionInput
|
studios: MultiCriterionInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input StudioFilterType {
|
||||||
|
"""Filter to only include studios with this parent studio"""
|
||||||
|
parents: MultiCriterionInput
|
||||||
|
}
|
||||||
|
|
||||||
enum CriterionModifier {
|
enum CriterionModifier {
|
||||||
"""="""
|
"""="""
|
||||||
EQUALS,
|
EQUALS,
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ type Studio {
|
|||||||
checksum: String!
|
checksum: String!
|
||||||
name: String!
|
name: String!
|
||||||
url: String
|
url: String
|
||||||
|
parent_studio: Studio
|
||||||
|
child_studios: [Studio!]!
|
||||||
image_path: String # Resolver
|
image_path: String # Resolver
|
||||||
scene_count: Int # Resolver
|
scene_count: Int # Resolver
|
||||||
}
|
}
|
||||||
@@ -11,6 +12,7 @@ type Studio {
|
|||||||
input StudioCreateInput {
|
input StudioCreateInput {
|
||||||
name: String!
|
name: String!
|
||||||
url: String
|
url: String
|
||||||
|
parent_id: ID
|
||||||
"""This should be base64 encoded"""
|
"""This should be base64 encoded"""
|
||||||
image: String
|
image: String
|
||||||
}
|
}
|
||||||
@@ -19,6 +21,7 @@ input StudioUpdateInput {
|
|||||||
id: ID!
|
id: ID!
|
||||||
name: String
|
name: String
|
||||||
url: String
|
url: String
|
||||||
|
parent_id: ID,
|
||||||
"""This should be base64 encoded"""
|
"""This should be base64 encoded"""
|
||||||
image: String
|
image: String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"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)
|
res, err := qb.CountByStudioID(obj.ID)
|
||||||
return &res, err
|
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"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
|
"github.com/stashapp/stash/pkg/manager"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
@@ -40,6 +41,10 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
|||||||
if input.URL != nil {
|
if input.URL != nil {
|
||||||
newStudio.URL = sql.NullString{String: *input.URL, Valid: true}
|
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
|
// Start the transaction and save the studio
|
||||||
tx := database.DB.MustBeginTx(ctx, nil)
|
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) {
|
func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.StudioUpdateInput) (*models.Studio, error) {
|
||||||
// Populate studio from the input
|
// Populate studio from the input
|
||||||
studioID, _ := strconv.Atoi(input.ID)
|
studioID, _ := strconv.Atoi(input.ID)
|
||||||
updatedStudio := models.Studio{
|
|
||||||
|
updatedStudio := models.StudioPartial{
|
||||||
ID: studioID,
|
ID: studioID,
|
||||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: time.Now()},
|
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
|
||||||
}
|
}
|
||||||
if input.Image != nil {
|
if input.Image != nil {
|
||||||
_, imageData, err := utils.ProcessBase64Image(*input.Image)
|
_, imageData, err := utils.ProcessBase64Image(*input.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
updatedStudio.Image = imageData
|
updatedStudio.Image = &imageData
|
||||||
}
|
}
|
||||||
if input.Name != nil {
|
if input.Name != nil {
|
||||||
// generate checksum from studio name rather than image
|
// generate checksum from studio name rather than image
|
||||||
checksum := utils.MD5FromString(*input.Name)
|
checksum := utils.MD5FromString(*input.Name)
|
||||||
updatedStudio.Name = sql.NullString{String: *input.Name, Valid: true}
|
updatedStudio.Name = &sql.NullString{String: *input.Name, Valid: true}
|
||||||
updatedStudio.Checksum = checksum
|
updatedStudio.Checksum = &checksum
|
||||||
}
|
}
|
||||||
if input.URL != nil {
|
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
|
// Start the transaction and save the studio
|
||||||
tx := database.DB.MustBeginTx(ctx, nil)
|
tx := database.DB.MustBeginTx(ctx, nil)
|
||||||
qb := models.NewStudioQueryBuilder()
|
qb := models.NewStudioQueryBuilder()
|
||||||
|
|
||||||
|
if err := manager.ValidateModifyStudio(updatedStudio, tx); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
studio, err := qb.Update(updatedStudio, tx)
|
studio, err := qb.Update(updatedStudio, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (*models.Stud
|
|||||||
return qb.Find(idInt, nil)
|
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()
|
qb := models.NewStudioQueryBuilder()
|
||||||
studios, total := qb.Query(filter)
|
studios, total := qb.Query(studioFilter, filter)
|
||||||
return &models.FindStudiosResultType{
|
return &models.FindStudiosResultType{
|
||||||
Count: total,
|
Count: total,
|
||||||
Studios: studios,
|
Studios: studios,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
|
|
||||||
var DB *sqlx.DB
|
var DB *sqlx.DB
|
||||||
var dbPath string
|
var dbPath string
|
||||||
var appSchemaVersion uint = 8
|
var appSchemaVersion uint = 9
|
||||||
var databaseSchemaVersion uint
|
var databaseSchemaVersion uint
|
||||||
|
|
||||||
const sqlite3Driver = "sqlite3ex"
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/json-iterator/go"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Studio struct {
|
type Studio struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
URL string `json:"url,omitempty"`
|
URL string `json:"url,omitempty"`
|
||||||
Image string `json:"image,omitempty"`
|
ParentStudio string `json:"parent_studio,omitempty"`
|
||||||
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
Image string `json:"image,omitempty"`
|
||||||
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
|
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadStudioFile(filePath string) (*Studio, error) {
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
@@ -10,11 +16,6 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/manager/paths"
|
"github.com/stashapp/stash/pkg/manager/paths"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
"math"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExportTask struct {
|
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) {
|
func exportStudio(wg *sync.WaitGroup, jobChan <-chan *models.Studio) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
|
studioQB := models.NewStudioQueryBuilder()
|
||||||
|
|
||||||
for studio := range jobChan {
|
for studio := range jobChan {
|
||||||
|
|
||||||
newStudioJSON := jsonschema.Studio{
|
newStudioJSON := jsonschema.Studio{
|
||||||
@@ -408,6 +411,12 @@ func exportStudio(wg *sync.WaitGroup, jobChan <-chan *models.Studio) {
|
|||||||
if studio.URL.Valid {
|
if studio.URL.Valid {
|
||||||
newStudioJSON.URL = studio.URL.String
|
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)
|
newStudioJSON.Image = utils.GetBase64StringFromData(studio.Image)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package manager
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -157,7 +158,8 @@ func (t *ImportTask) ImportPerformers(ctx context.Context) {
|
|||||||
|
|
||||||
func (t *ImportTask) ImportStudios(ctx context.Context) {
|
func (t *ImportTask) ImportStudios(ctx context.Context) {
|
||||||
tx := database.DB.MustBeginTx(ctx, nil)
|
tx := database.DB.MustBeginTx(ctx, nil)
|
||||||
qb := models.NewStudioQueryBuilder()
|
|
||||||
|
pendingParent := make(map[string][]*jsonschema.Studio)
|
||||||
|
|
||||||
for i, mappingJSON := range t.Mappings.Studios {
|
for i, mappingJSON := range t.Mappings.Studios {
|
||||||
index := i + 1
|
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))
|
logger.Progressf("[studios] %d of %d", index, len(t.Mappings.Studios))
|
||||||
|
|
||||||
// generate checksum from studio name rather than image
|
if err := t.ImportStudio(studioJSON, pendingParent, tx); err != nil {
|
||||||
checksum := utils.MD5FromString(studioJSON.Name)
|
tx.Rollback()
|
||||||
|
|
||||||
// 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()
|
|
||||||
logger.Errorf("[studios] <%s> failed to create: %s", mappingJSON.Checksum, err.Error())
|
logger.Errorf("[studios] <%s> failed to create: %s", mappingJSON.Checksum, err.Error())
|
||||||
return
|
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")
|
logger.Info("[studios] importing")
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
logger.Errorf("[studios] import failed to commit: %s", err.Error())
|
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")
|
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) {
|
func (t *ImportTask) ImportMovies(ctx context.Context) {
|
||||||
tx := database.DB.MustBeginTx(ctx, nil)
|
tx := database.DB.MustBeginTx(ctx, nil)
|
||||||
qb := models.NewMovieQueryBuilder()
|
qb := models.NewMovieQueryBuilder()
|
||||||
|
|||||||
@@ -10,8 +10,20 @@ type Studio struct {
|
|||||||
Checksum string `db:"checksum" json:"checksum"`
|
Checksum string `db:"checksum" json:"checksum"`
|
||||||
Name sql.NullString `db:"name" json:"name"`
|
Name sql.NullString `db:"name" json:"name"`
|
||||||
URL sql.NullString `db:"url" json:"url"`
|
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"`
|
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_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"
|
var DefaultStudioImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgVBQsJl1CMZAAAASJJREFUeNrt3N0JwyAYhlEj3cj9R3Cm5rbkqtAP+qrnGaCYHPwJpLlaa++mmLpbAERAgAgIEAEBIiBABERAgAgIEAEBIiBABERAgAgIEAHZuVflj40x4i94zhk9vqsVvEq6AsQqMP1EjORx20OACAgQRRx7T+zzcFBxcjNDfoB4ntQqTm5Awo7MlqywZxcgYQ+RlqywJ3ozJAQCSBiEJSsQA0gYBpDAgAARECACAkRAgAgIEAERECACAmSjUv6eAOSB8m8YIGGzBUjYbAESBgMkbBkDEjZbgITBAClcxiqQvEoatreYIWEBASIgJ4Gkf11ntXH3nS9uxfGWfJ5J9hAgAgJEQAQEiIAAERAgAgJEQAQEiIAAERAgAgJEQAQEiL7qBuc6RKLHxr0CAAAAAElFTkSuQmCC"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
@@ -148,7 +147,7 @@ func (qb *MovieQueryBuilder) Query(movieFilter *MovieFilterType, findFilter *Fin
|
|||||||
args = append(args, studioID)
|
args = append(args, studioID)
|
||||||
}
|
}
|
||||||
|
|
||||||
whereClause, havingClause := qb.getMultiCriterionClause("studio", "", "studio_id", studiosFilter)
|
whereClause, havingClause := getMultiCriterionClause("movies", "studio", "", "", "studio_id", studiosFilter)
|
||||||
whereClauses = appendClause(whereClauses, whereClause)
|
whereClauses = appendClause(whereClauses, whereClause)
|
||||||
havingClauses = appendClause(havingClauses, havingClause)
|
havingClauses = appendClause(havingClauses, havingClause)
|
||||||
}
|
}
|
||||||
@@ -165,29 +164,6 @@ func (qb *MovieQueryBuilder) Query(movieFilter *MovieFilterType, findFilter *Fin
|
|||||||
return movies, countResult
|
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 {
|
func (qb *MovieQueryBuilder) getMovieSort(findFilter *FindFilterType) string {
|
||||||
var sort string
|
var sort string
|
||||||
var direction string
|
var direction string
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package models_test
|
package models_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -86,6 +87,42 @@ func TestMovieFindByNames(t *testing.T) {
|
|||||||
assert.Equal(t, strings.ToLower(movieNames[movieIdxWithScene]), strings.ToLower(movies[1].Name.String))
|
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 Update
|
||||||
// TODO Destroy
|
// TODO Destroy
|
||||||
// TODO Find
|
// TODO Find
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"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"
|
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.addWhere(whereClause)
|
||||||
query.addHaving(havingClause)
|
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"
|
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.addWhere(whereClause)
|
||||||
query.addHaving(havingClause)
|
query.addHaving(havingClause)
|
||||||
}
|
}
|
||||||
@@ -350,7 +349,7 @@ func (qb *SceneQueryBuilder) Query(sceneFilter *SceneFilterType, findFilter *Fin
|
|||||||
query.addArg(studioID)
|
query.addArg(studioID)
|
||||||
}
|
}
|
||||||
|
|
||||||
whereClause, havingClause := getMultiCriterionClause("studio", "", "studio_id", studiosFilter)
|
whereClause, havingClause := getMultiCriterionClause("scenes", "studio", "", "", "studio_id", studiosFilter)
|
||||||
query.addWhere(whereClause)
|
query.addWhere(whereClause)
|
||||||
query.addHaving(havingClause)
|
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"
|
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.addWhere(whereClause)
|
||||||
query.addHaving(havingClause)
|
query.addHaving(havingClause)
|
||||||
}
|
}
|
||||||
@@ -414,29 +413,6 @@ func getDurationWhereClause(durationFilter IntCriterionInput) (string, []interfa
|
|||||||
return clause, args
|
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) {
|
func (qb *SceneQueryBuilder) QueryAllByPathRegex(regex string) ([]*Scene, error) {
|
||||||
var args []interface{}
|
var args []interface{}
|
||||||
body := selectDistinctIDs("scenes") + " WHERE scenes.path regexp ?"
|
body := selectDistinctIDs("scenes") + " WHERE scenes.path regexp ?"
|
||||||
|
|||||||
@@ -676,6 +676,42 @@ func TestSceneQueryStudio(t *testing.T) {
|
|||||||
assert.Len(t, scenes, 0)
|
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) {
|
func TestSceneQuerySorting(t *testing.T) {
|
||||||
sort := titleField
|
sort := titleField
|
||||||
direction := models.SortDirectionEnumAsc
|
direction := models.SortDirectionEnumAsc
|
||||||
|
|||||||
@@ -239,6 +239,29 @@ func getIntCriterionWhereClause(column string, input IntCriterionInput) (string,
|
|||||||
return column + " " + binding, count
|
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) {
|
func runIdsQuery(query string, args []interface{}) ([]int, error) {
|
||||||
var result []struct {
|
var result []struct {
|
||||||
Int int `db:"id"`
|
Int int `db:"id"`
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ func NewStudioQueryBuilder() StudioQueryBuilder {
|
|||||||
func (qb *StudioQueryBuilder) Create(newStudio Studio, tx *sqlx.Tx) (*Studio, error) {
|
func (qb *StudioQueryBuilder) Create(newStudio Studio, tx *sqlx.Tx) (*Studio, error) {
|
||||||
ensureTx(tx)
|
ensureTx(tx)
|
||||||
result, err := tx.NamedExec(
|
result, err := tx.NamedExec(
|
||||||
`INSERT INTO studios (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, :created_at, :updated_at)
|
VALUES (:image, :checksum, :name, :url, :parent_id, :created_at, :updated_at)
|
||||||
`,
|
`,
|
||||||
newStudio,
|
newStudio,
|
||||||
)
|
)
|
||||||
@@ -35,20 +35,21 @@ func (qb *StudioQueryBuilder) Create(newStudio Studio, tx *sqlx.Tx) (*Studio, er
|
|||||||
return &newStudio, nil
|
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)
|
ensureTx(tx)
|
||||||
_, err := tx.NamedExec(
|
_, err := tx.NamedExec(
|
||||||
`UPDATE studios SET `+SQLGenKeys(updatedStudio)+` WHERE studios.id = :id`,
|
`UPDATE studios SET `+SQLGenKeysPartial(updatedStudio)+` WHERE studios.id = :id`,
|
||||||
updatedStudio,
|
updatedStudio,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 nil, err
|
||||||
}
|
}
|
||||||
return &updatedStudio, nil
|
return &ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *StudioQueryBuilder) Destroy(id string, tx *sqlx.Tx) error {
|
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)
|
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) {
|
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"
|
query := "SELECT studios.* FROM studios JOIN scenes ON studios.id = scenes.studio_id WHERE scenes.id = ? LIMIT 1"
|
||||||
args := []interface{}{sceneID}
|
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)
|
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 {
|
if findFilter == nil {
|
||||||
findFilter = &FindFilterType{}
|
findFilter = &FindFilterType{}
|
||||||
}
|
}
|
||||||
@@ -116,11 +126,26 @@ func (qb *StudioQueryBuilder) Query(findFilter *FindFilterType) ([]*Studio, int)
|
|||||||
|
|
||||||
if q := findFilter.Q; q != nil && *q != "" {
|
if q := findFilter.Q; q != nil && *q != "" {
|
||||||
searchColumns := []string{"studios.name"}
|
searchColumns := []string{"studios.name"}
|
||||||
|
|
||||||
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
|
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
|
||||||
whereClauses = append(whereClauses, clause)
|
whereClauses = append(whereClauses, clause)
|
||||||
args = append(args, thisArgs...)
|
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)
|
sortAndPagination := qb.getStudioSort(findFilter) + getPagination(findFilter)
|
||||||
idsResult, countResult := executeFindQuery("studios", body, args, sortAndPagination, whereClauses, havingClauses)
|
idsResult, countResult := executeFindQuery("studios", body, args, sortAndPagination, whereClauses, havingClauses)
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,13 @@
|
|||||||
package models_test
|
package models_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/database"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stretchr/testify/assert"
|
"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 Create
|
||||||
// TODO Update
|
// TODO Update
|
||||||
// TODO Destroy
|
// TODO Destroy
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ import (
|
|||||||
const totalScenes = 12
|
const totalScenes = 12
|
||||||
const performersNameCase = 3
|
const performersNameCase = 3
|
||||||
const performersNameNoCase = 2
|
const performersNameNoCase = 2
|
||||||
const moviesNameCase = 1
|
const moviesNameCase = 2
|
||||||
const moviesNameNoCase = 1
|
const moviesNameNoCase = 1
|
||||||
const totalGalleries = 1
|
const totalGalleries = 1
|
||||||
const tagsNameNoCase = 2
|
const tagsNameNoCase = 2
|
||||||
const tagsNameCase = 5
|
const tagsNameCase = 5
|
||||||
const studiosNameCase = 1
|
const studiosNameCase = 4
|
||||||
const studiosNameNoCase = 1
|
const studiosNameNoCase = 1
|
||||||
|
|
||||||
var sceneIDs []int
|
var sceneIDs []int
|
||||||
@@ -61,9 +61,10 @@ const performerIdx1WithDupName = 3
|
|||||||
const performerIdxWithDupName = 4
|
const performerIdxWithDupName = 4
|
||||||
|
|
||||||
const movieIdxWithScene = 0
|
const movieIdxWithScene = 0
|
||||||
|
const movieIdxWithStudio = 1
|
||||||
|
|
||||||
// movies with dup names start from the end
|
// movies with dup names start from the end
|
||||||
const movieIdxWithDupName = 1
|
const movieIdxWithDupName = 2
|
||||||
|
|
||||||
const galleryIdxWithScene = 0
|
const galleryIdxWithScene = 0
|
||||||
|
|
||||||
@@ -78,9 +79,12 @@ const tagIdx1WithDupName = 5
|
|||||||
const tagIdxWithDupName = 6
|
const tagIdxWithDupName = 6
|
||||||
|
|
||||||
const studioIdxWithScene = 0
|
const studioIdxWithScene = 0
|
||||||
|
const studioIdxWithMovie = 1
|
||||||
|
const studioIdxWithChildStudio = 2
|
||||||
|
const studioIdxWithParentStudio = 3
|
||||||
|
|
||||||
// studios with dup names start from the end
|
// studios with dup names start from the end
|
||||||
const studioIdxWithDupName = 1
|
const studioIdxWithDupName = 4
|
||||||
|
|
||||||
const markerIdxWithScene = 0
|
const markerIdxWithScene = 0
|
||||||
|
|
||||||
@@ -196,6 +200,16 @@ func populateDB() error {
|
|||||||
return err
|
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 {
|
if err := createMarker(tx, sceneIdxWithMarker, tagIdxWithPrimaryMarker, []int{tagIdxWithMarker}); err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
@@ -312,10 +326,9 @@ func createMovies(tx *sqlx.Tx, n int, o int) error {
|
|||||||
const namePlain = "Name"
|
const namePlain = "Name"
|
||||||
const nameNoCase = "NaMe"
|
const nameNoCase = "NaMe"
|
||||||
|
|
||||||
name := namePlain
|
|
||||||
|
|
||||||
for i := 0; i < n+o; i++ {
|
for i := 0; i < n+o; i++ {
|
||||||
index := i
|
index := i
|
||||||
|
name := namePlain
|
||||||
|
|
||||||
if i >= n { // i<n tags get normal names
|
if i >= n { // i<n tags get normal names
|
||||||
name = nameNoCase // i>=n movies get dup names if case is not checked
|
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
|
} // 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
|
// 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{
|
movie := models.Movie{
|
||||||
Name: sql.NullString{String: getMovieStringValue(index, name), Valid: true},
|
Name: sql.NullString{String: name, Valid: true},
|
||||||
FrontImage: []byte(models.DefaultMovieImage),
|
FrontImage: []byte(models.DefaultMovieImage),
|
||||||
Checksum: utils.MD5FromString(name),
|
Checksum: utils.MD5FromString(name),
|
||||||
}
|
}
|
||||||
@@ -332,7 +346,7 @@ func createMovies(tx *sqlx.Tx, n int, o int) error {
|
|||||||
created, err := mqb.Create(movie, tx)
|
created, err := mqb.Create(movie, tx)
|
||||||
|
|
||||||
if err != nil {
|
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)
|
movieIDs = append(movieIDs, created.ID)
|
||||||
@@ -432,16 +446,35 @@ func getStudioStringValue(index int, field string) string {
|
|||||||
return "studio_" + strconv.FormatInt(int64(index), 10) + "_" + field
|
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
|
//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 {
|
func createStudios(tx *sqlx.Tx, n int, o int) error {
|
||||||
sqb := models.NewStudioQueryBuilder()
|
|
||||||
const namePlain = "Name"
|
const namePlain = "Name"
|
||||||
const nameNoCase = "NaMe"
|
const nameNoCase = "NaMe"
|
||||||
|
|
||||||
name := namePlain
|
|
||||||
|
|
||||||
for i := 0; i < n+o; i++ {
|
for i := 0; i < n+o; i++ {
|
||||||
index := i
|
index := i
|
||||||
|
name := namePlain
|
||||||
|
|
||||||
if i >= n { // i<n studios get normal names
|
if i >= n { // i<n studios get normal names
|
||||||
name = nameNoCase // i>=n studios get dup names if case is not checked
|
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
|
} // 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
|
// studios [ i ] and [ n + o - i - 1 ] should have similar names with only the Name!=NaMe part different
|
||||||
|
|
||||||
tag := models.Studio{
|
name = getStudioStringValue(index, name)
|
||||||
Name: sql.NullString{String: getStudioStringValue(index, name), Valid: true},
|
created, err := createStudio(tx, name, nil)
|
||||||
Image: []byte(models.DefaultStudioImage),
|
|
||||||
Checksum: utils.MD5FromString(name),
|
|
||||||
}
|
|
||||||
|
|
||||||
created, err := sqb.Create(tag, tx)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error creating studio %v+: %s", tag, err.Error())
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
studioIDs = append(studioIDs, created.ID)
|
studioIDs = append(studioIDs, created.ID)
|
||||||
@@ -582,3 +610,27 @@ func linkSceneStudio(tx *sqlx.Tx, sceneIndex, studioIndex int) error {
|
|||||||
|
|
||||||
return err
|
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";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
const markup = `
|
const markup = `
|
||||||
|
### ✨ New Features
|
||||||
|
* Add support for parent/child studios.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
* Show rating as stars in scene page.
|
* Show rating as stars in scene page.
|
||||||
* Add reload scrapers button.
|
* Add reload scrapers button.
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
if (
|
if (
|
||||||
criterion.type !== "performers" &&
|
criterion.type !== "performers" &&
|
||||||
criterion.type !== "studios" &&
|
criterion.type !== "studios" &&
|
||||||
|
criterion.type !== "parent_studios" &&
|
||||||
criterion.type !== "tags" &&
|
criterion.type !== "tags" &&
|
||||||
criterion.type !== "sceneTags" &&
|
criterion.type !== "sceneTags" &&
|
||||||
criterion.type !== "movies"
|
criterion.type !== "movies"
|
||||||
|
|||||||
@@ -23,7 +23,13 @@ type ValidTypes =
|
|||||||
type Option = { value: string; label: string };
|
type Option = { value: string; label: string };
|
||||||
|
|
||||||
interface ITypeProps {
|
interface ITypeProps {
|
||||||
type?: "performers" | "studios" | "tags" | "sceneTags" | "movies";
|
type?:
|
||||||
|
| "performers"
|
||||||
|
| "studios"
|
||||||
|
| "parent_studios"
|
||||||
|
| "tags"
|
||||||
|
| "sceneTags"
|
||||||
|
| "movies";
|
||||||
}
|
}
|
||||||
interface IFilterProps {
|
interface IFilterProps {
|
||||||
ids?: string[];
|
ids?: string[];
|
||||||
@@ -175,7 +181,7 @@ export const MarkerTitleSuggest: React.FC<IMarkerSuggestProps> = (props) => {
|
|||||||
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) =>
|
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) =>
|
||||||
props.type === "performers" ? (
|
props.type === "performers" ? (
|
||||||
<PerformerSelect {...(props as IFilterProps)} />
|
<PerformerSelect {...(props as IFilterProps)} />
|
||||||
) : props.type === "studios" ? (
|
) : props.type === "studios" || props.type === "parent_studios" ? (
|
||||||
<StudioSelect {...(props as IFilterProps)} />
|
<StudioSelect {...(props as IFilterProps)} />
|
||||||
) : props.type === "movies" ? (
|
) : props.type === "movies" ? (
|
||||||
<MovieSelect {...(props as IFilterProps)} />
|
<MovieSelect {...(props as IFilterProps)} />
|
||||||
|
|||||||
@@ -3,12 +3,45 @@ import React from "react";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { FormattedPlural } from "react-intl";
|
import { FormattedPlural } from "react-intl";
|
||||||
|
import { NavUtils } from "src/utils";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
studio: GQL.StudioDataFragment;
|
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 (
|
return (
|
||||||
<Card className="studio-card">
|
<Card className="studio-card">
|
||||||
<Link to={`/studios/${studio.id}`} className="studio-card-header">
|
<Link to={`/studios/${studio.id}`} className="studio-card-header">
|
||||||
@@ -29,6 +62,8 @@ export const StudioCard: React.FC<IProps> = ({ studio }) => {
|
|||||||
/>
|
/>
|
||||||
.
|
.
|
||||||
</span>
|
</span>
|
||||||
|
{maybeRenderParent(studio, hideParent)}
|
||||||
|
{maybeRenderChildren(studio)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable react/no-this-in-sfc */
|
/* 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 React, { useEffect, useState } from "react";
|
||||||
import { useParams, useHistory } from "react-router-dom";
|
import { useParams, useHistory } from "react-router-dom";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
@@ -18,9 +18,11 @@ import {
|
|||||||
DetailsEditNavbar,
|
DetailsEditNavbar,
|
||||||
Modal,
|
Modal,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
|
StudioSelect,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { StudioScenesPanel } from "./StudioScenesPanel";
|
import { StudioScenesPanel } from "./StudioScenesPanel";
|
||||||
|
import { StudioChildrenPanel } from "./StudioChildrenPanel";
|
||||||
|
|
||||||
export const Studio: React.FC = () => {
|
export const Studio: React.FC = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -36,6 +38,7 @@ export const Studio: React.FC = () => {
|
|||||||
const [image, setImage] = useState<string>();
|
const [image, setImage] = useState<string>();
|
||||||
const [name, setName] = useState<string>();
|
const [name, setName] = useState<string>();
|
||||||
const [url, setUrl] = useState<string>();
|
const [url, setUrl] = useState<string>();
|
||||||
|
const [parentStudioId, setParentStudioId] = useState<string>();
|
||||||
|
|
||||||
// Studio state
|
// Studio state
|
||||||
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
||||||
@@ -55,6 +58,7 @@ export const Studio: React.FC = () => {
|
|||||||
function updateStudioEditState(state: Partial<GQL.StudioDataFragment>) {
|
function updateStudioEditState(state: Partial<GQL.StudioDataFragment>) {
|
||||||
setName(state.name);
|
setName(state.name);
|
||||||
setUrl(state.url ?? undefined);
|
setUrl(state.url ?? undefined);
|
||||||
|
setParentStudioId(state?.parent_studio?.id ?? undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStudioData(studioData: Partial<GQL.StudioDataFragment>) {
|
function updateStudioData(studioData: Partial<GQL.StudioDataFragment>) {
|
||||||
@@ -89,6 +93,7 @@ export const Studio: React.FC = () => {
|
|||||||
const input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput> = {
|
const input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput> = {
|
||||||
name,
|
name,
|
||||||
url,
|
url,
|
||||||
|
parent_id: parentStudioId,
|
||||||
image,
|
image,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -189,6 +194,20 @@ export const Studio: React.FC = () => {
|
|||||||
isEditing: !!isEditing,
|
isEditing: !!isEditing,
|
||||||
onChange: setUrl,
|
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>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
<DetailsEditNavbar
|
<DetailsEditNavbar
|
||||||
@@ -205,7 +224,14 @@ export const Studio: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
{!isNew && (
|
{!isNew && (
|
||||||
<div className="col-12 col-sm-8">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{renderDeleteAlert()}
|
{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 { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { StudioCard } from "./StudioCard";
|
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({
|
const listData = useStudiosList({
|
||||||
renderContent,
|
renderContent,
|
||||||
|
subComponent: fromParent,
|
||||||
|
filterHook,
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderContent(
|
function renderContent(
|
||||||
@@ -20,7 +30,11 @@ export const StudioList: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="row px-xl-5 justify-content-center">
|
<div className="row px-xl-5 justify-content-center">
|
||||||
{result.data.findStudios.studios.map((studio) => (
|
{result.data.findStudios.studios.map((studio) => (
|
||||||
<StudioCard key={studio.id} studio={studio} />
|
<StudioCard
|
||||||
|
key={studio.id}
|
||||||
|
studio={studio}
|
||||||
|
hideParent={fromParent}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const useFindStudios = (filter: ListFilterModel) =>
|
|||||||
GQL.useFindStudiosQuery({
|
GQL.useFindStudiosQuery({
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter.makeFindFilter(),
|
||||||
|
studio_filter: filter.makeStudioFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ export type CriterionType =
|
|||||||
| "tattoos"
|
| "tattoos"
|
||||||
| "piercings"
|
| "piercings"
|
||||||
| "aliases"
|
| "aliases"
|
||||||
| "gender";
|
| "gender"
|
||||||
|
| "parent_studios";
|
||||||
|
|
||||||
type Option = string | number | IOptionType;
|
type Option = string | number | IOptionType;
|
||||||
export type CriterionValue = string | number | ILabeledId[];
|
export type CriterionValue = string | number | ILabeledId[];
|
||||||
@@ -93,6 +94,8 @@ export abstract class Criterion {
|
|||||||
return "Aliases";
|
return "Aliases";
|
||||||
case "gender":
|
case "gender":
|
||||||
return "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 label: string = Criterion.getLabel("studios");
|
||||||
public value: CriterionType = "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 { PerformersCriterion } from "./performers";
|
||||||
import { RatingCriterion } from "./rating";
|
import { RatingCriterion } from "./rating";
|
||||||
import { ResolutionCriterion } from "./resolution";
|
import { ResolutionCriterion } from "./resolution";
|
||||||
import { StudiosCriterion } from "./studios";
|
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
|
||||||
import { TagsCriterion } from "./tags";
|
import { TagsCriterion } from "./tags";
|
||||||
import { GenderCriterion } from "./gender";
|
import { GenderCriterion } from "./gender";
|
||||||
import { MoviesCriterion } from "./movies";
|
import { MoviesCriterion } from "./movies";
|
||||||
@@ -50,6 +50,8 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
return new PerformersCriterion();
|
return new PerformersCriterion();
|
||||||
case "studios":
|
case "studios":
|
||||||
return new StudiosCriterion();
|
return new StudiosCriterion();
|
||||||
|
case "parent_studios":
|
||||||
|
return new ParentStudiosCriterion();
|
||||||
case "movies":
|
case "movies":
|
||||||
return new MoviesCriterion();
|
return new MoviesCriterion();
|
||||||
case "birth_year":
|
case "birth_year":
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
SceneMarkerFilterType,
|
SceneMarkerFilterType,
|
||||||
SortDirectionEnum,
|
SortDirectionEnum,
|
||||||
MovieFilterType,
|
MovieFilterType,
|
||||||
|
StudioFilterType,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { stringToGender } from "src/core/StashService";
|
import { stringToGender } from "src/core/StashService";
|
||||||
import {
|
import {
|
||||||
@@ -41,7 +42,12 @@ import {
|
|||||||
ResolutionCriterion,
|
ResolutionCriterion,
|
||||||
ResolutionCriterionOption,
|
ResolutionCriterionOption,
|
||||||
} from "./criteria/resolution";
|
} from "./criteria/resolution";
|
||||||
import { StudiosCriterion, StudiosCriterionOption } from "./criteria/studios";
|
import {
|
||||||
|
StudiosCriterion,
|
||||||
|
StudiosCriterionOption,
|
||||||
|
ParentStudiosCriterion,
|
||||||
|
ParentStudiosCriterionOption,
|
||||||
|
} from "./criteria/studios";
|
||||||
import {
|
import {
|
||||||
SceneTagsCriterionOption,
|
SceneTagsCriterionOption,
|
||||||
TagsCriterion,
|
TagsCriterion,
|
||||||
@@ -159,7 +165,10 @@ export class ListFilterModel {
|
|||||||
this.sortBy = "name";
|
this.sortBy = "name";
|
||||||
this.sortByOptions = ["name", "scenes_count"];
|
this.sortByOptions = ["name", "scenes_count"];
|
||||||
this.displayModeOptions = [DisplayMode.Grid];
|
this.displayModeOptions = [DisplayMode.Grid];
|
||||||
this.criterionOptions = [new NoneCriterionOption()];
|
this.criterionOptions = [
|
||||||
|
new NoneCriterionOption(),
|
||||||
|
new ParentStudiosCriterionOption(),
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
case FilterMode.Movies:
|
case FilterMode.Movies:
|
||||||
this.sortBy = "name";
|
this.sortBy = "name";
|
||||||
@@ -576,4 +585,22 @@ export class ListFilterModel {
|
|||||||
});
|
});
|
||||||
return result;
|
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 * as GQL from "src/core/generated-graphql";
|
||||||
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
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 { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { FilterMode } from "src/models/list-filter/types";
|
import { FilterMode } from "src/models/list-filter/types";
|
||||||
@@ -30,6 +33,17 @@ const makeStudioScenesUrl = (studio: Partial<GQL.StudioDataFragment>) => {
|
|||||||
return `/scenes?${filter.makeQueryParameters()}`;
|
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>) => {
|
const makeMovieScenesUrl = (movie: Partial<GQL.MovieDataFragment>) => {
|
||||||
if (!movie.id) return "#";
|
if (!movie.id) return "#";
|
||||||
const filter = new ListFilterModel(FilterMode.Scenes);
|
const filter = new ListFilterModel(FilterMode.Scenes);
|
||||||
@@ -73,4 +87,5 @@ export default {
|
|||||||
makeTagScenesUrl,
|
makeTagScenesUrl,
|
||||||
makeSceneMarkerUrl,
|
makeSceneMarkerUrl,
|
||||||
makeMovieScenesUrl,
|
makeMovieScenesUrl,
|
||||||
|
makeChildStudiosUrl,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user