mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Add group graphql interfaces (#5017)
* Deprecate movie and add group interfaces * UI changes
This commit is contained in:
@@ -15,6 +15,7 @@ const (
|
||||
FilterModeGalleries FilterMode = "GALLERIES"
|
||||
FilterModeSceneMarkers FilterMode = "SCENE_MARKERS"
|
||||
FilterModeMovies FilterMode = "MOVIES"
|
||||
FilterModeGroups FilterMode = "GROUPS"
|
||||
FilterModeTags FilterMode = "TAGS"
|
||||
FilterModeImages FilterMode = "IMAGES"
|
||||
)
|
||||
@@ -25,6 +26,7 @@ var AllFilterMode = []FilterMode{
|
||||
FilterModeStudios,
|
||||
FilterModeGalleries,
|
||||
FilterModeSceneMarkers,
|
||||
FilterModeGroups,
|
||||
FilterModeMovies,
|
||||
FilterModeTags,
|
||||
FilterModeImages,
|
||||
@@ -32,7 +34,7 @@ var AllFilterMode = []FilterMode{
|
||||
|
||||
func (e FilterMode) IsValid() bool {
|
||||
switch e {
|
||||
case FilterModeScenes, FilterModePerformers, FilterModeStudios, FilterModeGalleries, FilterModeSceneMarkers, FilterModeMovies, FilterModeTags, FilterModeImages:
|
||||
case FilterModeScenes, FilterModePerformers, FilterModeStudios, FilterModeGalleries, FilterModeSceneMarkers, FilterModeMovies, FilterModeGroups, FilterModeTags, FilterModeImages:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -414,3 +414,24 @@ type ScrapedMovie struct {
|
||||
}
|
||||
|
||||
func (ScrapedMovie) IsScrapedContent() {}
|
||||
|
||||
// ScrapedGroup is a group from a scraping operation
|
||||
type ScrapedGroup struct {
|
||||
StoredID *string `json:"stored_id"`
|
||||
Name *string `json:"name"`
|
||||
Aliases *string `json:"aliases"`
|
||||
Duration *string `json:"duration"`
|
||||
Date *string `json:"date"`
|
||||
Rating *string `json:"rating"`
|
||||
Director *string `json:"director"`
|
||||
URLs []string `json:"urls"`
|
||||
Synopsis *string `json:"synopsis"`
|
||||
Studio *ScrapedStudio `json:"studio"`
|
||||
Tags []*ScrapedTag `json:"tags"`
|
||||
// This should be a base64 encoded data URL
|
||||
FrontImage *string `json:"front_image"`
|
||||
// This should be a base64 encoded data URL
|
||||
BackImage *string `json:"back_image"`
|
||||
}
|
||||
|
||||
func (ScrapedGroup) IsScrapedContent() {}
|
||||
|
||||
@@ -55,6 +55,8 @@ type SceneFilterType struct {
|
||||
IsMissing *string `json:"is_missing"`
|
||||
// Filter to only include scenes with this studio
|
||||
Studios *HierarchicalMultiCriterionInput `json:"studios"`
|
||||
// Filter to only include scenes with this group
|
||||
Groups *MultiCriterionInput `json:"groups"`
|
||||
// Filter to only include scenes with this movie
|
||||
Movies *MultiCriterionInput `json:"movies"`
|
||||
// Filter to only include scenes with this gallery
|
||||
@@ -103,6 +105,8 @@ type SceneFilterType struct {
|
||||
StudiosFilter *StudioFilterType `json:"studios_filter"`
|
||||
// Filter by related tags that meet this criteria
|
||||
TagsFilter *TagFilterType `json:"tags_filter"`
|
||||
// Filter by related groups that meet this criteria
|
||||
GroupsFilter *MovieFilterType `json:"groups_filter"`
|
||||
// Filter by related movies that meet this criteria
|
||||
MoviesFilter *MovieFilterType `json:"movies_filter"`
|
||||
// Filter by related markers that meet this criteria
|
||||
@@ -131,11 +135,17 @@ type SceneQueryResult struct {
|
||||
resolveErr error
|
||||
}
|
||||
|
||||
// SceneMovieInput is used for groups and movies
|
||||
type SceneMovieInput struct {
|
||||
MovieID string `json:"movie_id"`
|
||||
SceneIndex *int `json:"scene_index"`
|
||||
}
|
||||
|
||||
type SceneGroupInput struct {
|
||||
GroupID string `json:"group_id"`
|
||||
SceneIndex *int `json:"scene_index"`
|
||||
}
|
||||
|
||||
type SceneCreateInput struct {
|
||||
Title *string `json:"title"`
|
||||
Code *string `json:"code"`
|
||||
@@ -150,6 +160,7 @@ type SceneCreateInput struct {
|
||||
GalleryIds []string `json:"gallery_ids"`
|
||||
PerformerIds []string `json:"performer_ids"`
|
||||
Movies []SceneMovieInput `json:"movies"`
|
||||
Groups []SceneGroupInput `json:"groups"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
// This should be a URL or a base64 encoded data URL
|
||||
CoverImage *string `json:"cover_image"`
|
||||
@@ -177,6 +188,7 @@ type SceneUpdateInput struct {
|
||||
GalleryIds []string `json:"gallery_ids"`
|
||||
PerformerIds []string `json:"performer_ids"`
|
||||
Movies []SceneMovieInput `json:"movies"`
|
||||
Groups []SceneGroupInput `json:"groups"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
// This should be a URL or a base64 encoded data URL
|
||||
CoverImage *string `json:"cover_image"`
|
||||
|
||||
@@ -22,6 +22,8 @@ type TagFilterType struct {
|
||||
PerformerCount *IntCriterionInput `json:"performer_count"`
|
||||
// Filter by number of studios with this tag
|
||||
StudioCount *IntCriterionInput `json:"studio_count"`
|
||||
// Filter by number of groups with this tag
|
||||
GroupCount *IntCriterionInput `json:"group_count"`
|
||||
// Filter by number of movies with this tag
|
||||
MovieCount *IntCriterionInput `json:"movie_count"`
|
||||
// Filter by number of markers with this tag
|
||||
|
||||
@@ -26,10 +26,16 @@ const (
|
||||
GalleryChapterUpdatePost TriggerEnum = "GalleryChapter.Update.Post"
|
||||
GalleryChapterDestroyPost TriggerEnum = "GalleryChapter.Destroy.Post"
|
||||
|
||||
// deprecated - use Group hooks instead
|
||||
// for now, both movie and group hooks will be executed
|
||||
MovieCreatePost TriggerEnum = "Movie.Create.Post"
|
||||
MovieUpdatePost TriggerEnum = "Movie.Update.Post"
|
||||
MovieDestroyPost TriggerEnum = "Movie.Destroy.Post"
|
||||
|
||||
GroupCreatePost TriggerEnum = "Group.Create.Post"
|
||||
GroupUpdatePost TriggerEnum = "Group.Update.Post"
|
||||
GroupDestroyPost TriggerEnum = "Group.Destroy.Post"
|
||||
|
||||
PerformerCreatePost TriggerEnum = "Performer.Create.Post"
|
||||
PerformerUpdatePost TriggerEnum = "Performer.Update.Post"
|
||||
PerformerDestroyPost TriggerEnum = "Performer.Destroy.Post"
|
||||
|
||||
@@ -299,6 +299,7 @@ func (c config) spec() Scraper {
|
||||
|
||||
if len(movie.SupportedScrapes) > 0 {
|
||||
ret.Movie = &movie
|
||||
ret.Group = &movie
|
||||
}
|
||||
|
||||
return ret
|
||||
@@ -312,7 +313,7 @@ func (c config) supports(ty ScrapeContentType) bool {
|
||||
return (c.SceneByName != nil && c.SceneByQueryFragment != nil) || c.SceneByFragment != nil || len(c.SceneByURL) > 0
|
||||
case ScrapeContentTypeGallery:
|
||||
return c.GalleryByFragment != nil || len(c.GalleryByURL) > 0
|
||||
case ScrapeContentTypeMovie:
|
||||
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||
return len(c.MovieByURL) > 0
|
||||
}
|
||||
|
||||
@@ -339,7 +340,7 @@ func (c config) matchesURL(url string, ty ScrapeContentType) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case ScrapeContentTypeMovie:
|
||||
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||
for _, scraper := range c.MovieByURL {
|
||||
if scraper.matchesURL(url) {
|
||||
return true
|
||||
|
||||
@@ -81,7 +81,7 @@ func loadUrlCandidates(c config, ty ScrapeContentType) []*scrapeByURLConfig {
|
||||
return c.PerformerByURL
|
||||
case ScrapeContentTypeScene:
|
||||
return c.SceneByURL
|
||||
case ScrapeContentTypeMovie:
|
||||
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||
return c.MovieByURL
|
||||
case ScrapeContentTypeGallery:
|
||||
return c.GalleryByURL
|
||||
|
||||
@@ -102,7 +102,7 @@ func (s *jsonScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCont
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
case ScrapeContentTypeMovie:
|
||||
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||
ret, err := scraper.scrapeMovie(ctx, q)
|
||||
if err != nil || ret == nil {
|
||||
return nil, err
|
||||
|
||||
@@ -18,6 +18,7 @@ type ScrapedScene struct {
|
||||
Studio *models.ScrapedStudio `json:"studio"`
|
||||
Tags []*models.ScrapedTag `json:"tags"`
|
||||
Performers []*models.ScrapedPerformer `json:"performers"`
|
||||
Groups []*models.ScrapedGroup `json:"groups"`
|
||||
Movies []*models.ScrapedMovie `json:"movies"`
|
||||
RemoteSiteID *string `json:"remote_site_id"`
|
||||
Duration *int `json:"duration"`
|
||||
|
||||
@@ -31,6 +31,7 @@ type ScrapeContentType string
|
||||
const (
|
||||
ScrapeContentTypeGallery ScrapeContentType = "GALLERY"
|
||||
ScrapeContentTypeMovie ScrapeContentType = "MOVIE"
|
||||
ScrapeContentTypeGroup ScrapeContentType = "GROUP"
|
||||
ScrapeContentTypePerformer ScrapeContentType = "PERFORMER"
|
||||
ScrapeContentTypeScene ScrapeContentType = "SCENE"
|
||||
)
|
||||
@@ -38,13 +39,14 @@ const (
|
||||
var AllScrapeContentType = []ScrapeContentType{
|
||||
ScrapeContentTypeGallery,
|
||||
ScrapeContentTypeMovie,
|
||||
ScrapeContentTypeGroup,
|
||||
ScrapeContentTypePerformer,
|
||||
ScrapeContentTypeScene,
|
||||
}
|
||||
|
||||
func (e ScrapeContentType) IsValid() bool {
|
||||
switch e {
|
||||
case ScrapeContentTypeGallery, ScrapeContentTypeMovie, ScrapeContentTypePerformer, ScrapeContentTypeScene:
|
||||
case ScrapeContentTypeGallery, ScrapeContentTypeMovie, ScrapeContentTypeGroup, ScrapeContentTypePerformer, ScrapeContentTypeScene:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -81,6 +83,8 @@ type Scraper struct {
|
||||
// Details for gallery scraper
|
||||
Gallery *ScraperSpec `json:"gallery"`
|
||||
// Details for movie scraper
|
||||
Group *ScraperSpec `json:"group"`
|
||||
// Details for movie scraper
|
||||
Movie *ScraperSpec `json:"movie"`
|
||||
}
|
||||
|
||||
|
||||
@@ -384,7 +384,7 @@ func (s *scriptScraper) scrape(ctx context.Context, input string, ty ScrapeConte
|
||||
var scene *ScrapedScene
|
||||
err := s.runScraperScript(ctx, input, &scene)
|
||||
return scene, err
|
||||
case ScrapeContentTypeMovie:
|
||||
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||
var movie *models.ScrapedMovie
|
||||
err := s.runScraperScript(ctx, input, &movie)
|
||||
return movie, err
|
||||
|
||||
@@ -83,7 +83,7 @@ func (s *xpathScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCon
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
case ScrapeContentTypeMovie:
|
||||
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
|
||||
ret, err := scraper.scrapeMovie(ctx, q)
|
||||
if err != nil || ret == nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1881,7 +1881,7 @@ func TestGalleryQueryIsMissingPerformers(t *testing.T) {
|
||||
|
||||
assert.True(t, len(galleries) > 0)
|
||||
|
||||
// ensure non of the ids equal the one with movies
|
||||
// ensure non of the ids equal the one with galleries
|
||||
for _, gallery := range galleries {
|
||||
assert.NotEqual(t, galleryIDs[galleryIdxWithPerformer], gallery.ID)
|
||||
}
|
||||
|
||||
@@ -2053,7 +2053,7 @@ func TestImageQueryIsMissingPerformers(t *testing.T) {
|
||||
|
||||
assert.True(t, len(images) > 0)
|
||||
|
||||
// ensure non of the ids equal the one with movies
|
||||
// ensure non of the ids equal the one with performers
|
||||
for _, image := range images {
|
||||
assert.NotEqual(t, imageIDs[imageIdxWithPerformer], image.ID)
|
||||
}
|
||||
|
||||
@@ -1330,7 +1330,7 @@ func verifyPerformerQuery(t *testing.T, filter models.PerformerFilterType, verif
|
||||
|
||||
for _, performer := range performers {
|
||||
if err := performer.LoadURLs(ctx, db.Performer); err != nil {
|
||||
t.Errorf("Error loading movie relationships: %v", err)
|
||||
t.Errorf("Error loading url relationships: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -228,10 +228,21 @@ func (qb *SavedFilterStore) getMany(ctx context.Context, q *goqu.SelectDataset)
|
||||
func (qb *SavedFilterStore) FindByMode(ctx context.Context, mode models.FilterMode) ([]*models.SavedFilter, error) {
|
||||
// SELECT * FROM %s WHERE mode = ? AND name != ? ORDER BY name ASC
|
||||
table := qb.table()
|
||||
sq := qb.selectDataset().Prepared(true).Where(
|
||||
table.Col("mode").Eq(mode),
|
||||
table.Col("name").Neq(savedFilterDefaultName),
|
||||
).Order(table.Col("name").Asc())
|
||||
|
||||
// TODO - querying on groups needs to include movies
|
||||
// remove this when we migrate to remove the movies filter mode in the database
|
||||
var whereClause exp.Expression
|
||||
|
||||
if mode == models.FilterModeGroups || mode == models.FilterModeMovies {
|
||||
whereClause = goqu.Or(
|
||||
table.Col("mode").Eq(models.FilterModeGroups),
|
||||
table.Col("mode").Eq(models.FilterModeMovies),
|
||||
)
|
||||
} else {
|
||||
whereClause = table.Col("mode").Eq(mode)
|
||||
}
|
||||
|
||||
sq := qb.selectDataset().Prepared(true).Where(whereClause).Order(table.Col("name").Asc())
|
||||
ret, err := qb.getMany(ctx, sq)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -1074,6 +1074,7 @@ var sceneSortOptions = sortOptions{
|
||||
"duration",
|
||||
"file_mod_time",
|
||||
"framerate",
|
||||
"group_scene_number",
|
||||
"id",
|
||||
"interactive",
|
||||
"interactive_speed",
|
||||
@@ -1140,7 +1141,7 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF
|
||||
|
||||
direction := findFilter.GetDirection()
|
||||
switch sort {
|
||||
case "movie_scene_number":
|
||||
case "movie_scene_number", "group_scene_number":
|
||||
query.join(moviesScenesTable, "", "scenes.id = movies_scenes.scene_id")
|
||||
query.sortAndPagination += getSort("scene_index", direction, moviesScenesTable)
|
||||
case "tag_count":
|
||||
|
||||
@@ -147,7 +147,10 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
|
||||
qb.performersCriterionHandler(sceneFilter.Performers),
|
||||
qb.performerCountCriterionHandler(sceneFilter.PerformerCount),
|
||||
studioCriterionHandler(sceneTable, sceneFilter.Studios),
|
||||
qb.moviesCriterionHandler(sceneFilter.Movies),
|
||||
|
||||
qb.groupsCriterionHandler(sceneFilter.Groups),
|
||||
qb.groupsCriterionHandler(sceneFilter.Movies),
|
||||
|
||||
qb.galleriesCriterionHandler(sceneFilter.Galleries),
|
||||
qb.performerTagsCriterionHandler(sceneFilter.PerformerTags),
|
||||
qb.performerFavoriteCriterionHandler(sceneFilter.PerformerFavorite),
|
||||
@@ -480,7 +483,7 @@ func (qb *sceneFilterHandler) performerAgeCriterionHandler(performerAge *models.
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *sceneFilterHandler) moviesCriterionHandler(movies *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
func (qb *sceneFilterHandler) groupsCriterionHandler(movies *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
sceneRepository.movies.join(f, "", "scenes.id")
|
||||
f.addLeftJoin("movies", "", "movies_scenes.movie_id = movies.id")
|
||||
|
||||
@@ -278,9 +278,7 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
savedFilterIdxDefaultScene = iota
|
||||
savedFilterIdxDefaultImage
|
||||
savedFilterIdxScene
|
||||
savedFilterIdxScene = iota
|
||||
savedFilterIdxImage
|
||||
|
||||
// new indexes above
|
||||
@@ -1777,9 +1775,9 @@ func createChapter(ctx context.Context, mqb models.GalleryChapterReaderWriter, c
|
||||
|
||||
func getSavedFilterMode(index int) models.FilterMode {
|
||||
switch index {
|
||||
case savedFilterIdxScene, savedFilterIdxDefaultScene:
|
||||
case savedFilterIdxScene:
|
||||
return models.FilterModeScenes
|
||||
case savedFilterIdxImage, savedFilterIdxDefaultImage:
|
||||
case savedFilterIdxImage:
|
||||
return models.FilterModeImages
|
||||
default:
|
||||
return models.FilterModeScenes
|
||||
@@ -1787,11 +1785,6 @@ func getSavedFilterMode(index int) models.FilterMode {
|
||||
}
|
||||
|
||||
func getSavedFilterName(index int) string {
|
||||
if index <= savedFilterIdxDefaultImage {
|
||||
// empty string for default filters
|
||||
return ""
|
||||
}
|
||||
|
||||
if index <= savedFilterIdxImage {
|
||||
// use the same name for the first two - should be possible
|
||||
return firstSavedFilterName
|
||||
|
||||
@@ -683,7 +683,7 @@ func (qb *TagStore) getTagSort(query *queryBuilder, findFilter *models.FindFilte
|
||||
sortQuery += getCountSort(tagTable, performersTagsTable, tagIDColumn, direction)
|
||||
case "studios_count":
|
||||
sortQuery += getCountSort(tagTable, studiosTagsTable, tagIDColumn, direction)
|
||||
case "movies_count":
|
||||
case "movies_count", "groups_count":
|
||||
sortQuery += getCountSort(tagTable, moviesTagsTable, tagIDColumn, direction)
|
||||
default:
|
||||
sortQuery += getSort(sort, direction, "tags")
|
||||
|
||||
@@ -67,7 +67,10 @@ func (qb *tagFilterHandler) criterionHandler() criterionHandler {
|
||||
qb.galleryCountCriterionHandler(tagFilter.GalleryCount),
|
||||
qb.performerCountCriterionHandler(tagFilter.PerformerCount),
|
||||
qb.studioCountCriterionHandler(tagFilter.StudioCount),
|
||||
qb.movieCountCriterionHandler(tagFilter.MovieCount),
|
||||
|
||||
qb.groupCountCriterionHandler(tagFilter.GroupCount),
|
||||
qb.groupCountCriterionHandler(tagFilter.MovieCount),
|
||||
|
||||
qb.markerCountCriterionHandler(tagFilter.MarkerCount),
|
||||
qb.parentsCriterionHandler(tagFilter.Parents),
|
||||
qb.childrenCriterionHandler(tagFilter.Children),
|
||||
@@ -187,7 +190,7 @@ func (qb *tagFilterHandler) studioCountCriterionHandler(studioCount *models.IntC
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagFilterHandler) movieCountCriterionHandler(movieCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
func (qb *tagFilterHandler) groupCountCriterionHandler(movieCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if movieCount != nil {
|
||||
f.addLeftJoin("movies_tags", "", "movies_tags.tag_id = tags.id")
|
||||
|
||||
Reference in New Issue
Block a user