Merge branch 'master' into improved-timeline

This commit is contained in:
viktorstrate
2021-09-25 13:50:42 +02:00
26 changed files with 1003 additions and 97 deletions

View File

@@ -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 {

View 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
}

View 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(&regularUser).Association("Albums").Append(&rootAlbum)) {
return
}
if !assert.NoError(t, db.Model(&regularUser).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)
})
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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!