Skip to content

Commit

Permalink
Movie/Group tags (#4969)
Browse files Browse the repository at this point in the history
* Combine common tag control code into hook
* Combine common scraped tag row code into hook
  • Loading branch information
WithoutPants authored Jun 18, 2024
1 parent f9a624b commit fda4776
Show file tree
Hide file tree
Showing 63 changed files with 1,586 additions and 450 deletions.
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

0 comments on commit fda4776

Please sign in to comment.