From 193e05e7e030c25832c459a00d56f1bb7ddb4243 Mon Sep 17 00:00:00 2001 From: Peter - Ubuntu dual boot Date: Fri, 17 Sep 2021 15:33:44 +0100 Subject: [PATCH] Proof of concept for variable album covers - GQL playground working. --- api/graphql/generated.go | 164 ++++++++++++++++++++++++++++++++- api/graphql/models/album.go | 1 + api/graphql/resolvers/album.go | 94 ++++++++++++++++--- api/graphql/schema.graphql | 5 + 4 files changed, 247 insertions(+), 17 deletions(-) diff --git a/api/graphql/generated.go b/api/graphql/generated.go index ee6d77a5..2a2c8efe 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -57,6 +57,7 @@ type DirectiveRoot struct { type ComplexityRoot struct { Album struct { + CoverID func(childComplexity int) int FilePath func(childComplexity int) int ID func(childComplexity int) int Media func(childComplexity int, order *models.Ordering, paginate *models.Pagination, onlyFavorites *bool) int @@ -155,6 +156,7 @@ type ComplexityRoot struct { RecognizeUnlabeledFaces func(childComplexity int) int ScanAll func(childComplexity int) int ScanUser func(childComplexity int, userID int) int + SetAlbumCoverID func(childComplexity int, albumID int, coverID *int) int SetFaceGroupLabel func(childComplexity int, faceGroupID int, label *string) int SetPeriodicScanInterval func(childComplexity int, interval int) int SetScannerConcurrentWorkers func(childComplexity int, workers int) int @@ -312,6 +314,7 @@ type MutationResolver interface { SetPeriodicScanInterval(ctx context.Context, interval int) (int, error) SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error) ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error) + SetAlbumCoverID(ctx context.Context, albumID int, coverID *int) (*models.Album, error) SetFaceGroupLabel(ctx context.Context, faceGroupID int, label *string) (*models.FaceGroup, error) CombineFaceGroups(ctx context.Context, destinationFaceGroupID int, sourceFaceGroupID int) (*models.FaceGroup, error) MoveImageFaces(ctx context.Context, imageFaceIDs []int, destinationFaceGroupID int) (*models.FaceGroup, error) @@ -366,6 +369,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { + case "Album.coverID": + if e.complexity.Album.CoverID == nil { + break + } + + return e.complexity.Album.CoverID(childComplexity), true + case "Album.filePath": if e.complexity.Album.FilePath == nil { break @@ -938,6 +948,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.ScanUser(childComplexity, args["userId"].(int)), true + case "Mutation.setAlbumCoverID": + if e.complexity.Mutation.SetAlbumCoverID == nil { + break + } + + args, err := ec.field_Mutation_setAlbumCoverID_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.SetAlbumCoverID(childComplexity, args["albumID"].(int), args["coverID"].(*int)), true + case "Mutation.setFaceGroupLabel": if e.complexity.Mutation.SetFaceGroupLabel == nil { break @@ -1700,7 +1722,7 @@ type Query { "Get media owned by the logged in user, returned in GeoJson format" myMediaGeoJson: Any! @isAuthorized "Get the mapbox api token, returns null if mapbox is not enabled" - mapboxToken: String + mapboxToken: String @isAuthorized shareToken(credentials: ShareTokenCredentials!): ShareToken! shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean! @@ -1766,6 +1788,9 @@ type Mutation { changeUserPreferences(language: String): UserPreferences! @isAuthorized + "Assign a cover image to an album, set coverID to -1 to remove the current one" + setAlbumCoverID(albumID: ID!, coverID: Int): Album! + "Assign a label to a face group, set label to null to remove the current one" setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup! @isAuthorized "Merge two face groups into a single one, all ImageFaces from source will be moved to destination" @@ -1905,6 +1930,8 @@ type Album { path: [Album!]! shares: [ShareToken!]! + + coverID: Int! } type MediaURL { @@ -2364,6 +2391,30 @@ func (ec *executionContext) field_Mutation_scanUser_args(ctx context.Context, ra return args, nil } +func (ec *executionContext) field_Mutation_setAlbumCoverID_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 int + if tmp, ok := rawArgs["albumID"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("albumID")) + arg0, err = ec.unmarshalNID2int(ctx, tmp) + if err != nil { + return nil, err + } + } + args["albumID"] = arg0 + var arg1 *int + if tmp, ok := rawArgs["coverID"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("coverID")) + arg1, err = ec.unmarshalOInt2ᚖint(ctx, tmp) + if err != nil { + return nil, err + } + } + args["coverID"] = arg1 + return args, nil +} + func (ec *executionContext) field_Mutation_setFaceGroupLabel_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -3264,6 +3315,41 @@ func (ec *executionContext) _Album_shares(ctx context.Context, field graphql.Col return ec.marshalNShareToken2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _Album_coverID(ctx context.Context, field graphql.CollectedField, obj *models.Album) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Album", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.CoverID, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + func (ec *executionContext) _AuthorizeResult_success(ctx context.Context, field graphql.CollectedField, obj *models.AuthorizeResult) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5863,6 +5949,48 @@ func (ec *executionContext) _Mutation_changeUserPreferences(ctx context.Context, return ec.marshalNUserPreferences2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐUserPreferences(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_setAlbumCoverID(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Mutation", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Mutation_setAlbumCoverID_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().SetAlbumCoverID(rctx, args["albumID"].(int), args["coverID"].(*int)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*models.Album) + fc.Result = res + return ec.marshalNAlbum2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐAlbum(ctx, field.Selections, res) +} + func (ec *executionContext) _Mutation_setFaceGroupLabel(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -7031,8 +7159,28 @@ func (ec *executionContext) _Query_mapboxToken(ctx context.Context, field graphq ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().MapboxToken(rctx) + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().MapboxToken(rctx) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsAuthorized == nil { + return nil, errors.New("directive isAuthorized is not implemented") + } + return ec.directives.IsAuthorized(ctx, nil, directive0) + } + + tmp, err := directive1(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.(*string); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp) }) if err != nil { ec.Error(ctx, err) @@ -10112,6 +10260,11 @@ func (ec *executionContext) _Album(ctx context.Context, sel ast.SelectionSet, ob } return res }) + case "coverID": + out.Values[i] = ec._Album_coverID(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&invalids, 1) + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -10682,6 +10835,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "setAlbumCoverID": + out.Values[i] = ec._Mutation_setAlbumCoverID(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } case "setFaceGroupLabel": out.Values[i] = ec._Mutation_setFaceGroupLabel(ctx, field) if out.Values[i] == graphql.Null { diff --git a/api/graphql/models/album.go b/api/graphql/models/album.go index d09f4b79..1474a8f5 100644 --- a/api/graphql/models/album.go +++ b/api/graphql/models/album.go @@ -17,6 +17,7 @@ type Album struct { Owners []User `gorm:"many2many:user_albums;constraint:OnDelete:CASCADE;"` Path string `gorm:"not null"` PathHash string `gorm:"unique"` + CoverID int `gorm:"index;default:-1;"` } func (a *Album) FilePath() string { diff --git a/api/graphql/resolvers/album.go b/api/graphql/resolvers/album.go index 8592f915..0f275013 100644 --- a/api/graphql/resolvers/album.go +++ b/api/graphql/resolvers/album.go @@ -3,6 +3,8 @@ package resolvers import ( "context" + "fmt" + api "github.com/photoview/photoview/api/graphql" "github.com/photoview/photoview/api/graphql/auth" "github.com/photoview/photoview/api/graphql/models" @@ -148,24 +150,45 @@ func (r *albumResolver) Thumbnail(ctx context.Context, obj *models.Album) (*mode var media models.Media - err := r.Database.Raw(` - WITH recursive sub_albums AS ( - SELECT * FROM albums AS root WHERE id = ? - UNION ALL - SELECT child.* FROM albums AS child JOIN sub_albums ON child.parent_album_id = sub_albums.id - ) + fmt.Print(obj.CoverID) - SELECT * FROM media WHERE media.album_id IN ( - SELECT id FROM sub_albums - ) AND media.id IN ( - SELECT media_id FROM media_urls WHERE media_urls.media_id = media.id - ) LIMIT 1 - `, obj.ID).Find(&media).Error + if obj.CoverID == -1 { + if err := r.Database.Raw(` + WITH recursive sub_albums AS ( + SELECT * FROM albums AS root WHERE id = ? + UNION ALL + SELECT child.* FROM albums AS child JOIN sub_albums ON child.parent_album_id = sub_albums.id + ) - if err != nil { - return nil, err + SELECT * FROM media WHERE media.album_id IN ( + SELECT id FROM sub_albums + ) AND media.id IN ( + SELECT media_id FROM media_urls WHERE media_urls.media_id = media.id + ) LIMIT 1 + `, obj.ID).Find(&media).Error; err != nil { + return nil, err + } + } else { + if err := r.Database.Where("id = ?", obj.CoverID).Find(&media).Error; err != nil { + return nil, err + } } + + // err := r.Database.Raw(` + // WITH recursive sub_albums AS ( + // SELECT * FROM albums AS root WHERE id = ? + // UNION ALL + // SELECT child.* FROM albums AS child JOIN sub_albums ON child.parent_album_id = sub_albums.id + // ) + // + // SELECT * FROM media WHERE media.album_id IN ( + // SELECT id FROM sub_albums + // ) AND media.id IN ( + // SELECT media_id FROM media_urls WHERE media_urls.media_id = media.id + // ) LIMIT 1 + // `, obj.ID).Find(&media).Error + return &media, nil } @@ -242,3 +265,46 @@ func (r *albumResolver) Path(ctx context.Context, obj *models.Album) ([]*models. return album_path, nil } + +func (r *mutationResolver) SetAlbumCoverID(ctx context.Context, albumID int, coverID *int) (*models.Album, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, errors.New("unauthorized") + } + + var album models.Album + if err := r.Database.Find(&album, albumID).Error; err != nil { + return nil, err + } + + // + // var album models.Album + // + ownsAlbum, err := user.OwnsAlbum(r.Database, &album) + if err != nil { + return nil, err + } + + if !ownsAlbum { + return nil, errors.New("forbidden") + } + + + + if err := r.Database.Model(&album).Update("cover_id", coverID).Error; err != nil { + return nil, err + } + + // var faceGroup models.FaceGroup + // if err := db.Where("id = ?", faceGroupID).Find(&faceGroup).Error; err != nil { + // return nil, err + // } + // + // return &faceGroup, nil + // + // if err := r.Database.Model(faceGroup).Update("label", label).Error; err != nil { + // return nil, err + // } + + return &album, nil +} diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index b0828dde..67b6e7a9 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -134,6 +134,9 @@ type Mutation { changeUserPreferences(language: String): UserPreferences! @isAuthorized + "Assign a cover image to an album, set coverID to -1 to remove the current one" + setAlbumCoverID(albumID: ID!, coverID: Int): Album! + "Assign a label to a face group, set label to null to remove the current one" setFaceGroupLabel(faceGroupID: ID!, label: String): FaceGroup! @isAuthorized "Merge two face groups into a single one, all ImageFaces from source will be moved to destination" @@ -273,6 +276,8 @@ type Album { path: [Album!]! shares: [ShareToken!]! + + coverID: Int! } type MediaURL {