Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Movie/Group tags #4969

Merged
merged 7 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions graphql/schema/types/filters.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ input MovieFilterType {
url: StringCriterionInput
"Filter to only include movies where performer appears in a scene"
performers: MultiCriterionInput
"Filter to only include movies with these tags"
tags: HierarchicalMultiCriterionInput
"Filter by tag count"
tag_count: IntCriterionInput
"Filter by date"
date: DateCriterionInput
"Filter by creation time"
Expand Down Expand Up @@ -494,6 +498,9 @@ input TagFilterType {
"Filter by number of performers with this tag"
performer_count: IntCriterionInput

"Filter by number of movies with this tag"
movie_count: IntCriterionInput

"Filter by number of markers with this tag"
marker_count: IntCriterionInput

Expand Down
4 changes: 4 additions & 0 deletions graphql/schema/types/movie.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Movie {
synopsis: String
url: String @deprecated(reason: "Use urls")
urls: [String!]!
tags: [Tag!]!
created_at: Time!
updated_at: Time!

Expand All @@ -34,6 +35,7 @@ input MovieCreateInput {
synopsis: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
front_image: String
"This should be a URL or a base64 encoded data URL"
Expand All @@ -53,6 +55,7 @@ input MovieUpdateInput {
synopsis: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL"
front_image: String
"This should be a URL or a base64 encoded data URL"
Expand All @@ -67,6 +70,7 @@ input BulkMovieUpdateInput {
studio_id: ID
director: String
urls: BulkUpdateStrings
tag_ids: BulkUpdateIds
}

input MovieDestroyInput {
Expand Down
2 changes: 2 additions & 0 deletions graphql/schema/types/scraped-movie.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type ScrapedMovie {
urls: [String!]
synopsis: String
studio: ScrapedStudio
tags: [ScrapedTag!]

"This should be a base64 encoded data URL"
front_image: String
Expand All @@ -28,4 +29,5 @@ input ScrapedMovieInput {
url: String @deprecated(reason: "use urls")
urls: [String!]
synopsis: String
# not including tags for the input
}
1 change: 1 addition & 0 deletions graphql/schema/types/tag.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Tag {
image_count(depth: Int): Int! # Resolver
gallery_count(depth: Int): Int! # Resolver
performer_count(depth: Int): Int! # Resolver
movie_count(depth: Int): Int! # Resolver
parents: [Tag!]!
children: [Tag!]!

Expand Down
14 changes: 14 additions & 0 deletions internal/api/resolver_model_movie.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *mod
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
}

func (r movieResolver) Tags(ctx context.Context, obj *models.Movie) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadTagIDs(ctx, r.repository.Movie)
}); err != nil {
return nil, err
}
}

var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
return ret, firstError(errs)
}

func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
var hasImage bool
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
Expand Down
12 changes: 12 additions & 0 deletions internal/api/resolver_model_tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/movie"
"github.com/stashapp/stash/pkg/performer"
"github.com/stashapp/stash/pkg/scene"
)
Expand Down Expand Up @@ -107,6 +108,17 @@ func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag, depth
return ret, nil
}

func (r *tagResolver) MovieCount(ctx context.Context, obj *models.Tag, depth *int) (ret int, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = movie.CountByTagID(ctx, r.repository.Movie, obj.ID, depth)
return err
}); err != nil {
return 0, err
}

return ret, nil
}

func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
var hasImage bool
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
Expand Down
16 changes: 16 additions & 0 deletions internal/api/resolver_mutation_movie.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input MovieCreateInp
return nil, fmt.Errorf("converting studio id: %w", err)
}

newMovie.TagIDs, err = translator.relatedIds(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}

if input.Urls != nil {
newMovie.URLs = models.NewRelatedStrings(input.Urls)
} else if input.URL != nil {
Expand Down Expand Up @@ -140,6 +145,11 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input MovieUpdateInp
return nil, fmt.Errorf("converting studio id: %w", err)
}

updatedMovie.TagIDs, err = translator.updateIds(input.TagIds, "tag_ids")
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}

updatedMovie.URLs = translator.optionalURLs(input.Urls, input.URL)

var frontimageData []byte
Expand Down Expand Up @@ -211,6 +221,12 @@ func (r *mutationResolver) BulkMovieUpdate(ctx context.Context, input BulkMovieU
if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err)
}

updatedMovie.TagIDs, err = translator.updateIdsBulk(input.TagIds, "tag_ids")
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}

updatedMovie.URLs = translator.optionalURLsBulk(input.Urls, nil)

ret := []*models.Movie{}
Expand Down
26 changes: 25 additions & 1 deletion internal/api/resolver_query_scraper.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,23 @@ func filterPerformerTags(p []*models.ScrapedPerformer) {
}
}

// filterMovieTags removes tags matching excluded tag patterns from the provided scraped movies
func filterMovieTags(p []*models.ScrapedMovie) {
excludeRegexps := compileRegexps(manager.GetInstance().Config.GetScraperExcludeTagPatterns())

var ignoredTags []string

for _, s := range p {
var ignored []string
s.Tags, ignored = filterTags(excludeRegexps, s.Tags)
ignoredTags = sliceutil.AppendUniques(ignoredTags, ignored)
}

if len(ignoredTags) > 0 {
logger.Debugf("Scraping ignored tags: %s", strings.Join(ignoredTags, ", "))
}
}

func (r *queryResolver) ScrapeSceneURL(ctx context.Context, url string) (*scraper.ScrapedScene, error) {
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeScene)
if err != nil {
Expand Down Expand Up @@ -186,7 +203,14 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models
return nil, err
}

return marshalScrapedMovie(content)
ret, err := marshalScrapedMovie(content)
if err != nil {
return nil, err
}

filterMovieTags([]*models.ScrapedMovie{ret})

return ret, nil
}

func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.Source, input ScrapeSingleSceneInput) ([]*scraper.ScrapedScene, error) {
Expand Down
9 changes: 9 additions & 0 deletions internal/manager/task_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,7 @@ func (t *ExportTask) exportMovie(ctx context.Context, wg *sync.WaitGroup, jobCha
r := t.repository
movieReader := r.Movie
studioReader := r.Studio
tagReader := r.Tag

for m := range jobChan {
if err := m.LoadURLs(ctx, r.Movie); err != nil {
Expand All @@ -1121,6 +1122,14 @@ func (t *ExportTask) exportMovie(ctx context.Context, wg *sync.WaitGroup, jobCha
continue
}

tags, err := tagReader.FindByMovieID(ctx, m.ID)
if err != nil {
logger.Errorf("[movies] <%s> error getting image tag names: %v", m.Name, err)
continue
}

newMovieJSON.Tags = tag.GetNames(tags)

if t.includeDependencies {
if m.StudioID != nil {
t.studios.IDs = sliceutil.AppendUnique(t.studios.IDs, *m.StudioID)
Expand Down
1 change: 1 addition & 0 deletions internal/manager/task_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ func (t *ImportTask) ImportMovies(ctx context.Context) {
movieImporter := &movie.Importer{
ReaderWriter: r.Movie,
StudioWriter: r.Studio,
TagWriter: r.Tag,
Input: *movieJSON,
MissingRefBehaviour: t.MissingRefBehaviour,
}
Expand Down
1 change: 1 addition & 0 deletions pkg/models/jsonschema/movie.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Movie struct {
BackImage string `json:"back_image,omitempty"`
URLs []string `json:"urls,omitempty"`
Studio string `json:"studio,omitempty"`
Tags []string `json:"tags,omitempty"`
CreatedAt json.JSONTime `json:"created_at,omitempty"`
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`

Expand Down
23 changes: 23 additions & 0 deletions pkg/models/mocks/MovieReaderWriter.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions pkg/models/mocks/TagReaderWriter.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 12 additions & 4 deletions pkg/models/model_movie.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ type Movie struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`

URLs RelatedStrings `json:"urls"`
URLs RelatedStrings `json:"urls"`
TagIDs RelatedIDs `json:"tag_ids"`
}

func NewMovie() Movie {
Expand All @@ -30,9 +31,15 @@ func NewMovie() Movie {
}
}

func (g *Movie) LoadURLs(ctx context.Context, l URLLoader) error {
return g.URLs.load(func() ([]string, error) {
return l.GetURLs(ctx, g.ID)
func (m *Movie) LoadURLs(ctx context.Context, l URLLoader) error {
return m.URLs.load(func() ([]string, error) {
return l.GetURLs(ctx, m.ID)
})
}

func (m *Movie) LoadTagIDs(ctx context.Context, l TagIDLoader) error {
return m.TagIDs.load(func() ([]int, error) {
return l.GetTagIDs(ctx, m.ID)
})
}

Expand All @@ -47,6 +54,7 @@ type MoviePartial struct {
Director OptionalString
Synopsis OptionalString
URLs *UpdateStrings
TagIDs *UpdateIDs
CreatedAt OptionalTime
UpdatedAt OptionalTime
}
Expand Down
1 change: 1 addition & 0 deletions pkg/models/model_scraped_item.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ type ScrapedMovie struct {
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
Expand Down
4 changes: 4 additions & 0 deletions pkg/models/movie.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ type MovieFilterType struct {
URL *StringCriterionInput `json:"url"`
// Filter to only include movies where performer appears in a scene
Performers *MultiCriterionInput `json:"performers"`
// Filter to only include performers with these tags
Tags *HierarchicalMultiCriterionInput `json:"tags"`
// Filter by tag count
TagCount *IntCriterionInput `json:"tag_count"`
// Filter by date
Date *DateCriterionInput `json:"date"`
// Filter by related scenes that meet this criteria
Expand Down
1 change: 1 addition & 0 deletions pkg/models/repository_movie.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type MovieReader interface {
MovieQueryer
MovieCounter
URLLoader
TagIDLoader

All(ctx context.Context) ([]*Movie, error)
GetFrontImage(ctx context.Context, movieID int) ([]byte, error)
Expand Down
1 change: 1 addition & 0 deletions pkg/models/repository_tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type TagFinder interface {
FindByImageID(ctx context.Context, imageID int) ([]*Tag, error)
FindByGalleryID(ctx context.Context, galleryID int) ([]*Tag, error)
FindByPerformerID(ctx context.Context, performerID int) ([]*Tag, error)
FindByMovieID(ctx context.Context, movieID int) ([]*Tag, error)
FindBySceneMarkerID(ctx context.Context, sceneMarkerID int) ([]*Tag, error)
FindByName(ctx context.Context, name string, nocase bool) (*Tag, error)
FindByNames(ctx context.Context, names []string, nocase bool) ([]*Tag, error)
Expand Down
2 changes: 2 additions & 0 deletions pkg/models/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type TagFilterType struct {
GalleryCount *IntCriterionInput `json:"gallery_count"`
// Filter by number of performers with this tag
PerformerCount *IntCriterionInput `json:"performer_count"`
// Filter by number of movies with this tag
MovieCount *IntCriterionInput `json:"movie_count"`
// Filter by number of markers with this tag
MarkerCount *IntCriterionInput `json:"marker_count"`
// Filter by parent tags
Expand Down
Loading
Loading