mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 20:59:03 +00:00
Merge branch 'master' into improved-timeline
This commit is contained in:
@@ -154,8 +154,10 @@ type ComplexityRoot struct {
|
||||
MoveImageFaces func(childComplexity int, imageFaceIDs []int, destinationFaceGroupID int) int
|
||||
ProtectShareToken func(childComplexity int, token string, password *string) int
|
||||
RecognizeUnlabeledFaces func(childComplexity int) int
|
||||
ResetAlbumCover func(childComplexity int, albumID int) int
|
||||
ScanAll func(childComplexity int) int
|
||||
ScanUser func(childComplexity int, userID int) int
|
||||
SetAlbumCover func(childComplexity 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
|
||||
@@ -314,6 +316,8 @@ 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)
|
||||
ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error)
|
||||
SetAlbumCover(ctx context.Context, 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)
|
||||
@@ -928,6 +932,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Mutation.RecognizeUnlabeledFaces(childComplexity), true
|
||||
|
||||
case "Mutation.resetAlbumCover":
|
||||
if e.complexity.Mutation.ResetAlbumCover == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Mutation_resetAlbumCover_args(context.TODO(), rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.ResetAlbumCover(childComplexity, args["albumID"].(int)), true
|
||||
|
||||
case "Mutation.scanAll":
|
||||
if e.complexity.Mutation.ScanAll == nil {
|
||||
break
|
||||
@@ -947,6 +963,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Mutation.ScanUser(childComplexity, args["userId"].(int)), true
|
||||
|
||||
case "Mutation.setAlbumCover":
|
||||
if e.complexity.Mutation.SetAlbumCover == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Mutation_setAlbumCover_args(context.TODO(), rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.SetAlbumCover(childComplexity, args["coverID"].(int)), true
|
||||
|
||||
case "Mutation.setFaceGroupLabel":
|
||||
if e.complexity.Mutation.SetFaceGroupLabel == nil {
|
||||
break
|
||||
@@ -1783,6 +1811,11 @@ type Mutation {
|
||||
|
||||
changeUserPreferences(language: String): UserPreferences! @isAuthorized
|
||||
|
||||
"Reset the assigned cover photo for an album"
|
||||
resetAlbumCover(albumID: ID!): Album! @isAuthorized
|
||||
"Assign a cover photo to an album"
|
||||
setAlbumCover(coverID: ID!): Album! @isAuthorized
|
||||
|
||||
"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"
|
||||
@@ -1911,7 +1944,7 @@ type Album {
|
||||
paginate: Pagination
|
||||
): [Album!]!
|
||||
|
||||
"The album witch contains this album"
|
||||
"The album which contains this album"
|
||||
parentAlbum: Album
|
||||
"The user who owns this album"
|
||||
owner: User!
|
||||
@@ -1922,6 +1955,8 @@ type Album {
|
||||
path: [Album!]!
|
||||
|
||||
shares: [ShareToken!]!
|
||||
|
||||
#coverID: Int
|
||||
}
|
||||
|
||||
type MediaURL {
|
||||
@@ -2368,6 +2403,21 @@ func (ec *executionContext) field_Mutation_protectShareToken_args(ctx context.Co
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_resetAlbumCover_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
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_scanUser_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
@@ -2383,6 +2433,21 @@ func (ec *executionContext) field_Mutation_scanUser_args(ctx context.Context, ra
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_setAlbumCover_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["coverID"]; ok {
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("coverID"))
|
||||
arg0, err = ec.unmarshalNID2int(ctx, tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
args["coverID"] = arg0
|
||||
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{}{}
|
||||
@@ -5926,6 +5991,130 @@ 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_resetAlbumCover(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_resetAlbumCover_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) {
|
||||
directive0 := func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Mutation().ResetAlbumCover(rctx, args["albumID"].(int))
|
||||
}
|
||||
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.(*models.Album); ok {
|
||||
return data, nil
|
||||
}
|
||||
return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.Album`, tmp)
|
||||
})
|
||||
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_setAlbumCover(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_setAlbumCover_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) {
|
||||
directive0 := func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Mutation().SetAlbumCover(rctx, args["coverID"].(int))
|
||||
}
|
||||
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.(*models.Album); ok {
|
||||
return data, nil
|
||||
}
|
||||
return nil, fmt.Errorf(`unexpected type %T from directive, should be *github.com/photoview/photoview/api/graphql/models.Album`, tmp)
|
||||
})
|
||||
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 {
|
||||
@@ -10779,6 +10968,16 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "resetAlbumCover":
|
||||
out.Values[i] = ec._Mutation_resetAlbumCover(ctx, field)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "setAlbumCover":
|
||||
out.Values[i] = ec._Mutation_setAlbumCover(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 {
|
||||
|
||||
124
api/graphql/models/actions/album_actions.go
Normal file
124
api/graphql/models/actions/album_actions.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func MyAlbums(db *gorm.DB, user *models.User, order *models.Ordering, paginate *models.Pagination, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) {
|
||||
if err := user.FillAlbums(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(user.Albums) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userAlbumIDs := make([]int, len(user.Albums))
|
||||
for i, album := range user.Albums {
|
||||
userAlbumIDs[i] = album.ID
|
||||
}
|
||||
|
||||
query := db.Model(models.Album{}).Where("id IN (?)", userAlbumIDs)
|
||||
|
||||
if onlyRoot != nil && *onlyRoot {
|
||||
query = query.Where("parent_album_id IS NULL")
|
||||
}
|
||||
|
||||
if showEmpty == nil || !*showEmpty {
|
||||
subQuery := db.Model(&models.Media{}).Where("album_id = albums.id")
|
||||
|
||||
if onlyWithFavorites != nil && *onlyWithFavorites {
|
||||
favoritesSubquery := db.
|
||||
Model(&models.UserMediaData{UserID: user.ID}).
|
||||
Where("user_media_data.media_id = media.id").
|
||||
Where("user_media_data.favorite = true")
|
||||
|
||||
subQuery = subQuery.Where("EXISTS (?)", favoritesSubquery)
|
||||
}
|
||||
|
||||
query = query.Where("EXISTS (?)", subQuery)
|
||||
}
|
||||
|
||||
query = models.FormatSQL(query, order, paginate)
|
||||
|
||||
var albums []*models.Album
|
||||
if err := query.Find(&albums).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return albums, nil
|
||||
}
|
||||
|
||||
func Album(db *gorm.DB, user *models.User, id int) (*models.Album, error) {
|
||||
var album models.Album
|
||||
if err := db.First(&album, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("album not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownsAlbum, err := user.OwnsAlbum(db, &album)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !ownsAlbum {
|
||||
return nil, errors.New("forbidden")
|
||||
}
|
||||
|
||||
return &album, nil
|
||||
}
|
||||
|
||||
func SetAlbumCover(db *gorm.DB, user *models.User, mediaID int) (*models.Album, error) {
|
||||
var media models.Media
|
||||
|
||||
if err := db.Find(&media, mediaID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var album models.Album
|
||||
|
||||
if err := db.Find(&album, &media.AlbumID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownsAlbum, err := user.OwnsAlbum(db, &album)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !ownsAlbum {
|
||||
return nil, errors.New("forbidden")
|
||||
}
|
||||
|
||||
if err := db.Model(&album).Update("cover_id", mediaID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &album, nil
|
||||
}
|
||||
|
||||
func ResetAlbumCover(db *gorm.DB, user *models.User, albumID int) (*models.Album, error) {
|
||||
var album models.Album
|
||||
if err := db.Find(&album, albumID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownsAlbum, err := user.OwnsAlbum(db, &album)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !ownsAlbum {
|
||||
return nil, errors.New("forbidden")
|
||||
}
|
||||
|
||||
if err := db.Model(&album).Update("cover_id", nil).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &album, nil
|
||||
}
|
||||
178
api/graphql/models/actions/album_actions_test.go
Normal file
178
api/graphql/models/actions/album_actions_test.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package actions_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/graphql/models/actions"
|
||||
"github.com/photoview/photoview/api/test_utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAlbumCover(t *testing.T) {
|
||||
db := test_utils.DatabaseTest(t)
|
||||
|
||||
rootAlbum := models.Album{
|
||||
Title: "root",
|
||||
Path: "/photos",
|
||||
}
|
||||
|
||||
if !assert.NoError(t, db.Save(&rootAlbum).Error) {
|
||||
return
|
||||
}
|
||||
|
||||
children := []models.Album{
|
||||
{
|
||||
Title: "child1",
|
||||
Path: "/photos/child1",
|
||||
ParentAlbumID: &rootAlbum.ID,
|
||||
},
|
||||
{
|
||||
Title: "child2",
|
||||
Path: "/photos/child2",
|
||||
ParentAlbumID: &rootAlbum.ID,
|
||||
},
|
||||
}
|
||||
|
||||
if !assert.NoError(t, db.Save(&children).Error) {
|
||||
return
|
||||
}
|
||||
|
||||
photos := []models.Media{
|
||||
{
|
||||
Title: "pic1",
|
||||
Path: "/photos/pic1",
|
||||
AlbumID: rootAlbum.ID,
|
||||
},
|
||||
{
|
||||
Title: "pic2",
|
||||
Path: "/photos/pic2",
|
||||
AlbumID: rootAlbum.ID,
|
||||
},
|
||||
{
|
||||
Title: "pic3",
|
||||
Path: "/photos/child1/pic3",
|
||||
AlbumID: children[0].ID,
|
||||
},
|
||||
{
|
||||
Title: "pic4",
|
||||
Path: "/photos/child1/pic4",
|
||||
AlbumID: children[0].ID,
|
||||
},
|
||||
{
|
||||
Title: "pic5",
|
||||
Path: "/photos/child2/pic5",
|
||||
AlbumID: children[1].ID,
|
||||
},
|
||||
{
|
||||
Title: "pic6",
|
||||
Path: "/photos/child2/pic6",
|
||||
AlbumID: children[1].ID,
|
||||
},
|
||||
}
|
||||
|
||||
if !assert.NoError(t, db.Save(&photos).Error) {
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.NoError(t, db.Model(&children[0]).Update("cover_id", &photos[3].ID).Error) {
|
||||
return
|
||||
}
|
||||
|
||||
photoUrls := []models.MediaURL{
|
||||
{
|
||||
MediaID: photos[0].ID,
|
||||
Media: &photos[0],
|
||||
},
|
||||
{
|
||||
MediaID: photos[1].ID,
|
||||
Media: &photos[1],
|
||||
},
|
||||
{
|
||||
MediaID: photos[2].ID,
|
||||
Media: &photos[2],
|
||||
},
|
||||
{
|
||||
MediaID: photos[3].ID,
|
||||
Media: &photos[3],
|
||||
},
|
||||
{
|
||||
MediaID: photos[4].ID,
|
||||
Media: &photos[4],
|
||||
},
|
||||
{
|
||||
MediaID: photos[5].ID,
|
||||
Media: &photos[5],
|
||||
},
|
||||
}
|
||||
|
||||
if !assert.NoError(t, db.Save(&photoUrls).Error) {
|
||||
return
|
||||
}
|
||||
|
||||
user_pass := "password"
|
||||
regularUser, err := models.RegisterUser(db, "user1", &user_pass, false)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.NoError(t, db.Model(®ularUser).Association("Albums").Append(&rootAlbum)) {
|
||||
return
|
||||
}
|
||||
|
||||
if !assert.NoError(t, db.Model(®ularUser).Association("Albums").Append(&children)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Single test since we cannot rely on the tests being performed sequentially
|
||||
t.Run("Album get and reset cover photos", func(t *testing.T) {
|
||||
{
|
||||
album, err := actions.Album(db, regularUser, rootAlbum.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
albumThumb, err := album.Thumbnail(db)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Should return pic1 since no coverID has been set
|
||||
assert.EqualValues(t, "pic1", albumThumb.Title)
|
||||
}
|
||||
|
||||
{
|
||||
album, err := actions.Album(db, regularUser, children[0].ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
albumThumb, err := album.Thumbnail(db)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// coverID has already been set
|
||||
assert.EqualValues(t, "pic4", albumThumb.Title)
|
||||
}
|
||||
|
||||
resetAlbum, err := actions.ResetAlbumCover(db, regularUser, children[0].ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Nil(t, resetAlbum.CoverID)
|
||||
|
||||
resetThumb, err := resetAlbum.Thumbnail(db)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "pic3", resetThumb.Title)
|
||||
})
|
||||
|
||||
t.Run("Album change cover photos", func(t *testing.T) {
|
||||
assert.Nil(t, children[1].CoverID)
|
||||
|
||||
album, err := actions.SetAlbumCover(db, regularUser, photos[4].ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, children[1].ID, album.ID)
|
||||
assert.NotNil(t, album.CoverID)
|
||||
assert.Equal(t, photos[4].ID, *album.CoverID)
|
||||
|
||||
albumThumb, err := album.Thumbnail(db)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, photos[4].ID, albumThumb.ID)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
func (a *Album) FilePath() string {
|
||||
@@ -78,3 +79,31 @@ func GetParentsFromAlbums(db *gorm.DB, filter func(*gorm.DB) *gorm.DB, albumID i
|
||||
|
||||
return parents, err
|
||||
}
|
||||
|
||||
func (a *Album) Thumbnail(db *gorm.DB) (*Media, error) {
|
||||
var media Media
|
||||
|
||||
if a.CoverID == nil {
|
||||
if err := db.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
|
||||
) ORDER BY id LIMIT 1
|
||||
`, a.ID).Find(&media).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := db.Where("id = ?", a.CoverID).Find(&media).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &media, nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
api "github.com/photoview/photoview/api/graphql"
|
||||
"github.com/photoview/photoview/api/graphql/auth"
|
||||
"github.com/photoview/photoview/api/graphql/models"
|
||||
"github.com/photoview/photoview/api/graphql/models/actions"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -16,48 +17,7 @@ func (r *queryResolver) MyAlbums(ctx context.Context, order *models.Ordering, pa
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
|
||||
if err := user.FillAlbums(r.Database); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(user.Albums) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userAlbumIDs := make([]int, len(user.Albums))
|
||||
for i, album := range user.Albums {
|
||||
userAlbumIDs[i] = album.ID
|
||||
}
|
||||
|
||||
query := r.Database.Model(models.Album{}).Where("id IN (?)", userAlbumIDs)
|
||||
|
||||
if onlyRoot != nil && *onlyRoot == true {
|
||||
query = query.Where("parent_album_id IS NULL")
|
||||
}
|
||||
|
||||
if showEmpty == nil || *showEmpty == false {
|
||||
subQuery := r.Database.Model(&models.Media{}).Where("album_id = albums.id")
|
||||
|
||||
if onlyWithFavorites != nil && *onlyWithFavorites == true {
|
||||
favoritesSubquery := r.Database.
|
||||
Model(&models.UserMediaData{UserID: user.ID}).
|
||||
Where("user_media_data.media_id = media.id").
|
||||
Where("user_media_data.favorite = true")
|
||||
|
||||
subQuery = subQuery.Where("EXISTS (?)", favoritesSubquery)
|
||||
}
|
||||
|
||||
query = query.Where("EXISTS (?)", subQuery)
|
||||
}
|
||||
|
||||
query = models.FormatSQL(query, order, paginate)
|
||||
|
||||
var albums []*models.Album
|
||||
if err := query.Find(&albums).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return albums, nil
|
||||
return actions.MyAlbums(r.Database, user, order, paginate, onlyRoot, showEmpty, onlyWithFavorites)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Album(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Album, error) {
|
||||
@@ -89,24 +49,7 @@ func (r *queryResolver) Album(ctx context.Context, id int, tokenCredentials *mod
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
|
||||
var album models.Album
|
||||
if err := r.Database.First(&album, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("album not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ownsAlbum, err := user.OwnsAlbum(r.Database, &album)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !ownsAlbum {
|
||||
return nil, errors.New("forbidden")
|
||||
}
|
||||
|
||||
return &album, nil
|
||||
return actions.Album(r.Database, user, id)
|
||||
}
|
||||
|
||||
func (r *Resolver) Album() api.AlbumResolver {
|
||||
@@ -144,29 +87,8 @@ func (r *albumResolver) Media(ctx context.Context, album *models.Album, order *m
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func (r *albumResolver) Thumbnail(ctx context.Context, obj *models.Album) (*models.Media, error) {
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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 err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &media, nil
|
||||
func (r *albumResolver) Thumbnail(ctx context.Context, album *models.Album) (*models.Media, error) {
|
||||
return album.Thumbnail(r.Database)
|
||||
}
|
||||
|
||||
func (r *albumResolver) SubAlbums(ctx context.Context, parent *models.Album, order *models.Ordering, paginate *models.Pagination) ([]*models.Album, error) {
|
||||
@@ -242,3 +164,22 @@ func (r *albumResolver) Path(ctx context.Context, obj *models.Album) ([]*models.
|
||||
|
||||
return album_path, nil
|
||||
}
|
||||
|
||||
// Takes album_id, resets album.cover_id to 0 (null)
|
||||
func (r *mutationResolver) ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
return actions.ResetAlbumCover(r.Database, user, albumID)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SetAlbumCover(ctx context.Context, mediaID int) (*models.Album, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
return actions.SetAlbumCover(r.Database, user, mediaID)
|
||||
}
|
||||
|
||||
@@ -142,6 +142,11 @@ type Mutation {
|
||||
|
||||
changeUserPreferences(language: String): UserPreferences! @isAuthorized
|
||||
|
||||
"Reset the assigned cover photo for an album"
|
||||
resetAlbumCover(albumID: ID!): Album! @isAuthorized
|
||||
"Assign a cover photo to an album"
|
||||
setAlbumCover(coverID: ID!): Album! @isAuthorized
|
||||
|
||||
"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"
|
||||
@@ -270,7 +275,7 @@ type Album {
|
||||
paginate: Pagination
|
||||
): [Album!]!
|
||||
|
||||
"The album witch contains this album"
|
||||
"The album which contains this album"
|
||||
parentAlbum: Album
|
||||
"The user who owns this album"
|
||||
owner: User!
|
||||
|
||||
Reference in New Issue
Block a user