Fix gqlgen code (#1099)

This commit is contained in:
Googol Lee
2024-10-23 20:40:14 +02:00
committed by GitHub
parent c151bdee96
commit 84c642ca79
14 changed files with 3787 additions and 2350 deletions

View File

@@ -1,10 +1,7 @@
# .gqlgen.yml example
#
# Refer to https://gqlgen.com/config/
# for detailed .gqlgen.yml documentation.
schema:
- graphql/schema.graphql
- graphql/resolvers/*.graphql
exec:
filename: graphql/generated.go
@@ -14,8 +11,10 @@ model:
package: models
resolver:
filename: graphql/resolvers/root.go
type: Resolver
layout: follow-schema
dir: graphql/resolvers
package: resolvers
filename_template: "{name}.generated.go"
autobind: []

File diff suppressed because it is too large Load Diff

View File

@@ -1,76 +1,27 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.55
import (
"context"
"errors"
"fmt"
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"
)
func (r *queryResolver) MyAlbums(ctx context.Context, order *models.Ordering, paginate *models.Pagination, onlyRoot *bool,
showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
return actions.MyAlbums(r.DB(ctx), user, order, paginate, onlyRoot, showEmpty, onlyWithFavorites)
}
func (r *queryResolver) Album(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Album, error) {
db := r.DB(ctx)
if tokenCredentials != nil {
shareToken, err := r.ShareToken(ctx, *tokenCredentials)
if err != nil {
return nil, err
}
if shareToken.Album != nil {
if *shareToken.AlbumID == id {
return shareToken.Album, nil
}
subAlbum, err := shareToken.Album.GetChildren(db, func(query *gorm.DB) *gorm.DB {
return query.Where("sub_albums.id = ?", id)
})
if err != nil {
return nil, errors.Wrapf(err, "find sub album of share token (%s)", tokenCredentials.Token)
}
if len(subAlbum) > 0 {
return subAlbum[0], nil
}
}
}
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
return actions.Album(db, user, id)
}
func (r *Resolver) Album() api.AlbumResolver {
return &albumResolver{r}
}
type albumResolver struct{ *Resolver }
func (r *albumResolver) Media(ctx context.Context, album *models.Album, order *models.Ordering,
paginate *models.Pagination, onlyFavorites *bool) ([]*models.Media, error) {
// Media is the resolver for the media field.
func (r *albumResolver) Media(ctx context.Context, obj *models.Album, order *models.Ordering, paginate *models.Pagination, onlyFavorites *bool) ([]*models.Media, error) {
db := r.DB(ctx)
query := db.
Where("media.album_id = ?", album.ID).
Where("media.album_id = ?", obj.ID).
Where("media.id IN (?)", db.Model(&models.MediaURL{}).
Select("media_urls.media_id").
Where("media_urls.media_id = media.id"))
@@ -98,16 +49,11 @@ func (r *albumResolver) Media(ctx context.Context, album *models.Album, order *m
return media, nil
}
func (r *albumResolver) Thumbnail(ctx context.Context, album *models.Album) (*models.Media, error) {
return album.Thumbnail(r.DB(ctx))
}
func (r *albumResolver) SubAlbums(ctx context.Context, parent *models.Album, order *models.Ordering,
paginate *models.Pagination) ([]*models.Album, error) {
// SubAlbums is the resolver for the subAlbums field.
func (r *albumResolver) SubAlbums(ctx context.Context, obj *models.Album, order *models.Ordering, paginate *models.Pagination) ([]*models.Album, error) {
var albums []*models.Album
query := r.DB(ctx).Where("parent_album_id = ?", parent.ID)
query := r.DB(ctx).Where("parent_album_id = ?", obj.ID)
query = models.FormatSQL(query, order, paginate)
if err := query.Find(&albums).Error; err != nil {
@@ -117,26 +63,18 @@ func (r *albumResolver) SubAlbums(ctx context.Context, parent *models.Album, ord
return albums, nil
}
func (r *albumResolver) ParentAlbum(ctx context.Context, obj *models.Album) (*models.Album, error) {
panic("not implemented")
}
// Owner is the resolver for the owner field.
func (r *albumResolver) Owner(ctx context.Context, obj *models.Album) (*models.User, error) {
panic("not implemented")
}
func (r *albumResolver) Shares(ctx context.Context, album *models.Album) ([]*models.ShareToken, error) {
var shareTokens []*models.ShareToken
if err := r.DB(ctx).Where("album_id = ?", album.ID).Find(&shareTokens).Error; err != nil {
return nil, err
}
return shareTokens, nil
// Thumbnail is the resolver for the thumbnail field.
func (r *albumResolver) Thumbnail(ctx context.Context, obj *models.Album) (*models.Media, error) {
return obj.Thumbnail(r.DB(ctx))
}
// Path is the resolver for the path field.
func (r *albumResolver) Path(ctx context.Context, obj *models.Album) ([]*models.Album, error) {
user := auth.UserFromContext(ctx)
if user == nil {
empty := make([]*models.Album, 0)
@@ -146,6 +84,16 @@ func (r *albumResolver) Path(ctx context.Context, obj *models.Album) ([]*models.
return actions.AlbumPath(r.DB(ctx), user, obj)
}
// Shares is the resolver for the shares field.
func (r *albumResolver) Shares(ctx context.Context, obj *models.Album) ([]*models.ShareToken, error) {
var shareTokens []*models.ShareToken
if err := r.DB(ctx).Where("album_id = ?", obj.ID).Find(&shareTokens).Error; err != nil {
return nil, err
}
return shareTokens, 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)
@@ -156,11 +104,63 @@ func (r *mutationResolver) ResetAlbumCover(ctx context.Context, albumID int) (*m
return actions.ResetAlbumCover(r.DB(ctx), user, albumID)
}
func (r *mutationResolver) SetAlbumCover(ctx context.Context, mediaID int) (*models.Album, error) {
// SetAlbumCover is the resolver for the setAlbumCover field.
func (r *mutationResolver) SetAlbumCover(ctx context.Context, coverID int) (*models.Album, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
return actions.SetAlbumCover(r.DB(ctx), user, mediaID)
return actions.SetAlbumCover(r.DB(ctx), user, coverID)
}
// MyAlbums is the resolver for the myAlbums field.
func (r *queryResolver) MyAlbums(ctx context.Context, order *models.Ordering, paginate *models.Pagination, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
return actions.MyAlbums(r.DB(ctx), user, order, paginate, onlyRoot, showEmpty, onlyWithFavorites)
}
// Album is the resolver for the album field.
func (r *queryResolver) Album(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Album, error) {
db := r.DB(ctx)
if tokenCredentials != nil {
shareToken, err := r.ShareToken(ctx, *tokenCredentials)
if err != nil {
return nil, err
}
if shareToken.Album != nil {
if *shareToken.AlbumID == id {
return shareToken.Album, nil
}
subAlbum, err := shareToken.Album.GetChildren(db, func(query *gorm.DB) *gorm.DB {
return query.Where("sub_albums.id = ?", id)
})
if err != nil {
return nil, fmt.Errorf("find sub album of share token (%s): %w", tokenCredentials.Token, err)
}
if len(subAlbum) > 0 {
return subAlbum[0], nil
}
}
}
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
return actions.Album(db, user, id)
}
// Album returns api.AlbumResolver implementation.
func (r *Resolver) Album() api.AlbumResolver { return &albumResolver{r} }
type albumResolver struct{ *Resolver }

View File

@@ -0,0 +1,60 @@
type Album {
id: ID!
title: String!
"The media inside this album"
media(
order: Ordering,
paginate: Pagination
"Return only the favorited media"
onlyFavorites: Boolean
): [Media!]!
"The albums contained in this album"
subAlbums(
order: Ordering,
paginate: Pagination
): [Album!]!
"The album which contains this album"
parentAlbum: Album
"The user who owns this album"
owner: User!
"The path on the filesystem of the server, where this album is located"
filePath: String!
"An image in this album used for previewing this album"
thumbnail: Media
"A breadcrumb list of all parent albums down to this one"
path: [Album!]!
"A list of share tokens pointing to this album, owned by the logged in user"
shares: [ShareToken!]!
}
extend type Query {
"List of albums owned by the logged in user."
myAlbums(
order: Ordering,
paginate: Pagination
"Return only albums from the root directory of the user"
onlyRoot: Boolean
"Return also albums with no media directly in them"
showEmpty: Boolean
"Show only albums having favorites"
onlyWithFavorites: Boolean
): [Album!]! @isAuthorized
"""
Get album by id, user must own the album or be admin
If valid tokenCredentials are provided, the album may be retrived without further authentication
"""
album(id: ID!, tokenCredentials: ShareTokenCredentials): Album!
}
extend type Mutation {
"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
}

View File

@@ -0,0 +1,410 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.55
import (
"context"
"errors"
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/scanner/face_detection"
"gorm.io/gorm"
)
// ImageFaces is the resolver for the imageFaces field.
func (r *faceGroupResolver) ImageFaces(ctx context.Context, obj *models.FaceGroup, paginate *models.Pagination) ([]*models.ImageFace, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
if err := user.FillAlbums(db); err != nil {
return nil, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
query := db.
Joins("Media").
Where(faceGroupIDIsQuestion, obj.ID).
Where("album_id IN (?)", userAlbumIDs)
query = models.FormatSQL(query, nil, paginate)
var imageFaces []*models.ImageFace
if err := query.Find(&imageFaces).Error; err != nil {
return nil, err
}
return imageFaces, nil
}
// ImageFaceCount is the resolver for the imageFaceCount field.
func (r *faceGroupResolver) ImageFaceCount(ctx context.Context, obj *models.FaceGroup) (int, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return -1, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return -1, ErrFaceDetectorNotInitialized
}
if err := user.FillAlbums(db); err != nil {
return -1, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
query := db.
Model(&models.ImageFace{}).
Joins("Media").
Where(faceGroupIDIsQuestion, obj.ID).
Where("album_id IN (?)", userAlbumIDs)
var count int64
if err := query.Count(&count).Error; err != nil {
return -1, err
}
return int(count), nil
}
// Media is the resolver for the media field.
func (r *imageFaceResolver) Media(ctx context.Context, obj *models.ImageFace) (*models.Media, error) {
if err := obj.FillMedia(r.DB(ctx)); err != nil {
return nil, err
}
return &obj.Media, nil
}
// FaceGroup is the resolver for the faceGroup field.
func (r *imageFaceResolver) FaceGroup(ctx context.Context, obj *models.ImageFace) (*models.FaceGroup, error) {
if obj.FaceGroup != nil {
return obj.FaceGroup, nil
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
var faceGroup models.FaceGroup
if err := r.DB(ctx).Model(&obj).Association("FaceGroup").Find(&faceGroup); err != nil {
return nil, err
}
obj.FaceGroup = &faceGroup
return &faceGroup, nil
}
// SetFaceGroupLabel is the resolver for the setFaceGroupLabel field.
func (r *mutationResolver) SetFaceGroupLabel(ctx context.Context, faceGroupID int, label *string) (*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
faceGroup, err := userOwnedFaceGroup(db, user, faceGroupID)
if err != nil {
return nil, err
}
if err := db.Model(faceGroup).Update("label", label).Error; err != nil {
return nil, err
}
return faceGroup, nil
}
// CombineFaceGroups is the resolver for the combineFaceGroups field.
func (r *mutationResolver) CombineFaceGroups(ctx context.Context, destinationFaceGroupID int, sourceFaceGroupID int) (*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
destinationFaceGroup, err := userOwnedFaceGroup(db, user, destinationFaceGroupID)
if err != nil {
return nil, err
}
sourceFaceGroup, err := userOwnedFaceGroup(db, user, sourceFaceGroupID)
if err != nil {
return nil, err
}
updateError := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&models.ImageFace{}).
Where(faceGroupIDIsQuestion, sourceFaceGroup.ID).
Update("face_group_id", destinationFaceGroup.ID).Error; err != nil {
return err
}
if err := tx.Delete(&sourceFaceGroup).Error; err != nil {
return err
}
return nil
})
if updateError != nil {
return nil, updateError
}
face_detection.GlobalFaceDetector.MergeCategories(int32(sourceFaceGroupID), int32(destinationFaceGroupID))
return destinationFaceGroup, nil
}
// MoveImageFaces is the resolver for the moveImageFaces field.
func (r *mutationResolver) MoveImageFaces(ctx context.Context, imageFaceIDs []int, destinationFaceGroupID int) (*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
userOwnedImageFaceIDs := make([]int, 0)
var destFaceGroup *models.FaceGroup
transErr := db.Transaction(func(tx *gorm.DB) error {
var err error
destFaceGroup, err = userOwnedFaceGroup(tx, user, destinationFaceGroupID)
if err != nil {
return err
}
userOwnedImageFaces, err := getUserOwnedImageFaces(tx, user, imageFaceIDs)
if err != nil {
return err
}
for _, imageFace := range userOwnedImageFaces {
userOwnedImageFaceIDs = append(userOwnedImageFaceIDs, imageFace.ID)
}
var sourceFaceGroups []*models.FaceGroup
if err := tx.
Joins("LEFT JOIN image_faces ON image_faces.face_group_id = face_groups.id").
Where(imageFacesIDInQuestion, userOwnedImageFaceIDs).
Find(&sourceFaceGroups).Error; err != nil {
return err
}
if err := tx.
Model(&models.ImageFace{}).
Where("id IN (?)", userOwnedImageFaceIDs).
Update("face_group_id", destFaceGroup.ID).Error; err != nil {
return err
}
// delete face groups if they have become empty
if err := deleteEmptyFaceGroups(sourceFaceGroups, tx); err != nil {
return err
}
return nil
})
if transErr != nil {
return nil, transErr
}
face_detection.GlobalFaceDetector.MergeImageFaces(userOwnedImageFaceIDs, int32(destFaceGroup.ID))
return destFaceGroup, nil
}
// RecognizeUnlabeledFaces is the resolver for the recognizeUnlabeledFaces field.
func (r *mutationResolver) RecognizeUnlabeledFaces(ctx context.Context) ([]*models.ImageFace, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
var updatedImageFaces []*models.ImageFace
transactionError := db.Transaction(func(tx *gorm.DB) error {
var err error
updatedImageFaces, err = face_detection.GlobalFaceDetector.RecognizeUnlabeledFaces(tx, user)
return err
})
if transactionError != nil {
return nil, transactionError
}
return updatedImageFaces, nil
}
// DetachImageFaces is the resolver for the detachImageFaces field.
func (r *mutationResolver) DetachImageFaces(ctx context.Context, imageFaceIDs []int) (*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
userOwnedImageFaceIDs := make([]int, 0)
newFaceGroup := models.FaceGroup{}
transactionError := db.Transaction(func(tx *gorm.DB) error {
userOwnedImageFaces, err := getUserOwnedImageFaces(tx, user, imageFaceIDs)
if err != nil {
return err
}
for _, imageFace := range userOwnedImageFaces {
userOwnedImageFaceIDs = append(userOwnedImageFaceIDs, imageFace.ID)
}
if err := tx.Save(&newFaceGroup).Error; err != nil {
return err
}
if err := tx.
Model(&models.ImageFace{}).
Where("id IN (?)", userOwnedImageFaceIDs).
Update("face_group_id", newFaceGroup.ID).Error; err != nil {
return err
}
return nil
})
if transactionError != nil {
return nil, transactionError
}
face_detection.GlobalFaceDetector.MergeImageFaces(userOwnedImageFaceIDs, int32(newFaceGroup.ID))
return &newFaceGroup, nil
}
// MyFaceGroups is the resolver for the myFaceGroups field.
func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagination) ([]*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
if err := user.FillAlbums(db); err != nil {
return nil, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
faceGroupQuery := db.
Joins("JOIN image_faces ON image_faces.face_group_id = face_groups.id").
Where("image_faces.media_id IN (?)",
db.Select("media.id").Table("media").Where(mediaAlbumIDInQuestion, userAlbumIDs)).
Group("image_faces.face_group_id").
Group("face_groups.id").
Order("CASE WHEN label IS NULL THEN 1 ELSE 0 END").
Order("COUNT(image_faces.id) DESC")
faceGroupQuery = models.FormatSQL(faceGroupQuery, nil, paginate)
var faceGroups []*models.FaceGroup
if err := faceGroupQuery.Find(&faceGroups).Error; err != nil {
return nil, err
}
return faceGroups, nil
}
// FaceGroup is the resolver for the faceGroup field.
func (r *queryResolver) FaceGroup(ctx context.Context, id int) (*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
if err := user.FillAlbums(db); err != nil {
return nil, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
faceGroupQuery := db.
Joins("LEFT JOIN image_faces ON image_faces.face_group_id = face_groups.id").
Joins("LEFT JOIN media ON image_faces.media_id = media.id").
Where("face_groups.id = ?", id).
Where(mediaAlbumIDInQuestion, userAlbumIDs)
var faceGroup models.FaceGroup
if err := faceGroupQuery.Find(&faceGroup).Error; err != nil {
return nil, err
}
return &faceGroup, nil
}
// FaceGroup returns api.FaceGroupResolver implementation.
func (r *Resolver) FaceGroup() api.FaceGroupResolver { return &faceGroupResolver{r} }
// ImageFace returns api.ImageFaceResolver implementation.
func (r *Resolver) ImageFace() api.ImageFaceResolver { return &imageFaceResolver{r} }
type faceGroupResolver struct{ *Resolver }
type imageFaceResolver struct{ *Resolver }

View File

@@ -1,432 +1,19 @@
package resolvers
import (
"context"
"errors"
"fmt"
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/scanner/face_detection"
"github.com/pkg/errors"
"gorm.io/gorm"
)
const faceGroupIDisQuestion = "face_group_id = ?"
const mediaAlbumIDinQuestion = "media.album_id IN (?)"
const imageFacesIDinQuestion = "image_faces.id IN (?)"
const faceGroupIDIsQuestion = "face_group_id = ?"
const mediaAlbumIDInQuestion = "media.album_id IN (?)"
const imageFacesIDInQuestion = "image_faces.id IN (?)"
var ErrFaceDetectorNotInitialized = errors.New("face detector not initialized")
type imageFaceResolver struct {
*Resolver
}
type faceGroupResolver struct {
*Resolver
}
func (r *Resolver) ImageFace() api.ImageFaceResolver {
return imageFaceResolver{r}
}
func (r *Resolver) FaceGroup() api.FaceGroupResolver {
return faceGroupResolver{r}
}
func (r imageFaceResolver) FaceGroup(ctx context.Context, obj *models.ImageFace) (*models.FaceGroup, error) {
if obj.FaceGroup != nil {
return obj.FaceGroup, nil
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
var faceGroup models.FaceGroup
if err := r.DB(ctx).Model(&obj).Association("FaceGroup").Find(&faceGroup); err != nil {
return nil, err
}
obj.FaceGroup = &faceGroup
return &faceGroup, nil
}
func (r imageFaceResolver) Media(ctx context.Context, obj *models.ImageFace) (*models.Media, error) {
if err := obj.FillMedia(r.DB(ctx)); err != nil {
return nil, err
}
return &obj.Media, nil
}
func (r faceGroupResolver) ImageFaces(ctx context.Context, obj *models.FaceGroup,
paginate *models.Pagination) ([]*models.ImageFace, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
if err := user.FillAlbums(db); err != nil {
return nil, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
query := db.
Joins("Media").
Where(faceGroupIDisQuestion, obj.ID).
Where("album_id IN (?)", userAlbumIDs)
query = models.FormatSQL(query, nil, paginate)
var imageFaces []*models.ImageFace
if err := query.Find(&imageFaces).Error; err != nil {
return nil, err
}
return imageFaces, nil
}
func (r faceGroupResolver) ImageFaceCount(ctx context.Context, obj *models.FaceGroup) (int, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return -1, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return -1, ErrFaceDetectorNotInitialized
}
if err := user.FillAlbums(db); err != nil {
return -1, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
query := db.
Model(&models.ImageFace{}).
Joins("Media").
Where(faceGroupIDisQuestion, obj.ID).
Where("album_id IN (?)", userAlbumIDs)
var count int64
if err := query.Count(&count).Error; err != nil {
return -1, err
}
return int(count), nil
}
func (r *queryResolver) FaceGroup(ctx context.Context, id int) (*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
if err := user.FillAlbums(db); err != nil {
return nil, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
faceGroupQuery := db.
Joins("LEFT JOIN image_faces ON image_faces.face_group_id = face_groups.id").
Joins("LEFT JOIN media ON image_faces.media_id = media.id").
Where("face_groups.id = ?", id).
Where(mediaAlbumIDinQuestion, userAlbumIDs)
var faceGroup models.FaceGroup
if err := faceGroupQuery.Find(&faceGroup).Error; err != nil {
return nil, err
}
return &faceGroup, nil
}
func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagination) ([]*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
if err := user.FillAlbums(db); err != nil {
return nil, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
faceGroupQuery := db.
Joins("JOIN image_faces ON image_faces.face_group_id = face_groups.id").
Where("image_faces.media_id IN (?)",
db.Select("media.id").Table("media").Where(mediaAlbumIDinQuestion, userAlbumIDs)).
Group("image_faces.face_group_id").
Group("face_groups.id").
Order("CASE WHEN label IS NULL THEN 1 ELSE 0 END").
Order("COUNT(image_faces.id) DESC")
faceGroupQuery = models.FormatSQL(faceGroupQuery, nil, paginate)
var faceGroups []*models.FaceGroup
if err := faceGroupQuery.Find(&faceGroups).Error; err != nil {
return nil, err
}
return faceGroups, nil
}
func (r *mutationResolver) SetFaceGroupLabel(ctx context.Context, faceGroupID int,
label *string) (*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
faceGroup, err := userOwnedFaceGroup(db, user, faceGroupID)
if err != nil {
return nil, err
}
if err := db.Model(faceGroup).Update("label", label).Error; err != nil {
return nil, err
}
return faceGroup, nil
}
func (r *mutationResolver) CombineFaceGroups(ctx context.Context, destinationFaceGroupID int,
sourceFaceGroupID int) (*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
destinationFaceGroup, err := userOwnedFaceGroup(db, user, destinationFaceGroupID)
if err != nil {
return nil, err
}
sourceFaceGroup, err := userOwnedFaceGroup(db, user, sourceFaceGroupID)
if err != nil {
return nil, err
}
updateError := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&models.ImageFace{}).
Where(faceGroupIDisQuestion, sourceFaceGroup.ID).
Update("face_group_id", destinationFaceGroup.ID).Error; err != nil {
return err
}
if err := tx.Delete(&sourceFaceGroup).Error; err != nil {
return err
}
return nil
})
if updateError != nil {
return nil, updateError
}
face_detection.GlobalFaceDetector.MergeCategories(int32(sourceFaceGroupID), int32(destinationFaceGroupID))
return destinationFaceGroup, nil
}
func (r *mutationResolver) MoveImageFaces(ctx context.Context, imageFaceIDs []int,
destinationFaceGroupID int) (*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
userOwnedImageFaceIDs := make([]int, 0)
var destFaceGroup *models.FaceGroup
transErr := db.Transaction(func(tx *gorm.DB) error {
var err error
destFaceGroup, err = userOwnedFaceGroup(tx, user, destinationFaceGroupID)
if err != nil {
return err
}
userOwnedImageFaces, err := getUserOwnedImageFaces(tx, user, imageFaceIDs)
if err != nil {
return err
}
for _, imageFace := range userOwnedImageFaces {
userOwnedImageFaceIDs = append(userOwnedImageFaceIDs, imageFace.ID)
}
var sourceFaceGroups []*models.FaceGroup
if err := tx.
Joins("LEFT JOIN image_faces ON image_faces.face_group_id = face_groups.id").
Where(imageFacesIDinQuestion, userOwnedImageFaceIDs).
Find(&sourceFaceGroups).Error; err != nil {
return err
}
if err := tx.
Model(&models.ImageFace{}).
Where("id IN (?)", userOwnedImageFaceIDs).
Update("face_group_id", destFaceGroup.ID).Error; err != nil {
return err
}
// delete face groups if they have become empty
if err := deleteEmptyFaceGroups(sourceFaceGroups, tx); err != nil {
return err
}
return nil
})
if transErr != nil {
return nil, transErr
}
face_detection.GlobalFaceDetector.MergeImageFaces(userOwnedImageFaceIDs, int32(destFaceGroup.ID))
return destFaceGroup, nil
}
func deleteEmptyFaceGroups(sourceFaceGroups []*models.FaceGroup, tx *gorm.DB) error {
for _, faceGroup := range sourceFaceGroups {
var count int64
if err := tx.Model(&models.ImageFace{}).Where(faceGroupIDisQuestion, faceGroup.ID).Count(&count).Error; err != nil {
return err
}
if count == 0 {
if err := tx.Delete(&faceGroup).Error; err != nil {
return err
}
}
}
return nil
}
func (r *mutationResolver) RecognizeUnlabeledFaces(ctx context.Context) ([]*models.ImageFace, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
var updatedImageFaces []*models.ImageFace
transactionError := db.Transaction(func(tx *gorm.DB) error {
var err error
updatedImageFaces, err = face_detection.GlobalFaceDetector.RecognizeUnlabeledFaces(tx, user)
return err
})
if transactionError != nil {
return nil, transactionError
}
return updatedImageFaces, nil
}
func (r *mutationResolver) DetachImageFaces(ctx context.Context, imageFaceIDs []int) (*models.FaceGroup, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if face_detection.GlobalFaceDetector == nil {
return nil, ErrFaceDetectorNotInitialized
}
userOwnedImageFaceIDs := make([]int, 0)
newFaceGroup := models.FaceGroup{}
transactionError := db.Transaction(func(tx *gorm.DB) error {
userOwnedImageFaces, err := getUserOwnedImageFaces(tx, user, imageFaceIDs)
if err != nil {
return err
}
for _, imageFace := range userOwnedImageFaces {
userOwnedImageFaceIDs = append(userOwnedImageFaceIDs, imageFace.ID)
}
if err := tx.Save(&newFaceGroup).Error; err != nil {
return err
}
if err := tx.
Model(&models.ImageFace{}).
Where("id IN (?)", userOwnedImageFaceIDs).
Update("face_group_id", newFaceGroup.ID).Error; err != nil {
return err
}
return nil
})
if transactionError != nil {
return nil, transactionError
}
face_detection.GlobalFaceDetector.MergeImageFaces(userOwnedImageFaceIDs, int32(newFaceGroup.ID))
return &newFaceGroup, nil
}
func userOwnedFaceGroup(db *gorm.DB, user *models.User, faceGroupID int) (*models.FaceGroup, error) {
if user.Admin {
var faceGroup models.FaceGroup
@@ -451,18 +38,18 @@ func userOwnedFaceGroup(db *gorm.DB, user *models.User, faceGroupID int) (*model
Select("image_faces.id").
Table("image_faces").
Joins("JOIN media ON media.id = image_faces.media_id").
Where(mediaAlbumIDinQuestion, userAlbumIDs)
Where(mediaAlbumIDInQuestion, userAlbumIDs)
faceGroupQuery := db.
Model(&models.FaceGroup{}).
Joins("JOIN image_faces ON face_groups.id = image_faces.face_group_id").
Where("face_groups.id = ?", faceGroupID).
Where(imageFacesIDinQuestion, imageFaceQuery)
Where(imageFacesIDInQuestion, imageFaceQuery)
var faceGroup models.FaceGroup
if err := faceGroupQuery.Find(&faceGroup).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.Wrap(err, "face group does not exist or is not owned by the user")
return nil, fmt.Errorf("face group does not exist or is not owned by the user: %w", err)
}
return nil, err
}
@@ -483,11 +70,27 @@ func getUserOwnedImageFaces(tx *gorm.DB, user *models.User, imageFaceIDs []int)
var userOwnedImageFaces []*models.ImageFace
if err := tx.
Joins("JOIN media ON media.id = image_faces.media_id").
Where(mediaAlbumIDinQuestion, userAlbumIDs).
Where(imageFacesIDinQuestion, imageFaceIDs).
Where(mediaAlbumIDInQuestion, userAlbumIDs).
Where(imageFacesIDInQuestion, imageFaceIDs).
Find(&userOwnedImageFaces).Error; err != nil {
return nil, err
}
return userOwnedImageFaces, nil
}
func deleteEmptyFaceGroups(sourceFaceGroups []*models.FaceGroup, tx *gorm.DB) error {
for _, faceGroup := range sourceFaceGroups {
var count int64
if err := tx.Model(&models.ImageFace{}).Where(faceGroupIDIsQuestion, faceGroup.ID).Count(&count).Error; err != nil {
return err
}
if count == 0 {
if err := tx.Delete(&faceGroup).Error; err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,53 @@
"A bounding box of where a face is present on an image. The values map from 0 to 1 as a fraction of the image width/height"
type FaceRectangle {
minX: Float!
maxX: Float!
minY: Float!
maxY: Float!
}
"A collection of faces of a particular person"
type FaceGroup {
id: ID!
"The name of the person"
label: String
imageFaces(paginate: Pagination): [ImageFace!]!
"The total number of images in this collection"
imageFaceCount: Int!
}
"A single face on a particular image"
type ImageFace {
id: ID!
"A reference to the image the face appears on"
media: Media!
"A bounding box of where on the image the face is present"
rectangle: FaceRectangle!
"The `FaceGroup` that contains this `ImageFace`"
faceGroup: FaceGroup!
}
extend type Query {
"Get a list of `FaceGroup`s for the logged in user"
myFaceGroups(paginate: Pagination): [FaceGroup!]! @isAuthorized
"Get a particular `FaceGroup` specified by its ID"
faceGroup(id: ID!): FaceGroup! @isAuthorized
}
extend type Mutation {
"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"
combineFaceGroups(destinationFaceGroupID: ID!, sourceFaceGroupID: ID!): FaceGroup! @isAuthorized
"Move a list of ImageFaces to another face group"
moveImageFaces(imageFaceIDs: [ID!]!, destinationFaceGroupID: ID!): FaceGroup! @isAuthorized
"Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match"
recognizeUnlabeledFaces: [ImageFace!]! @isAuthorized
"Move a list of ImageFaces to a new face group"
detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup! @isAuthorized
}

View File

@@ -1,7 +1,13 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.55
import (
"context"
"errors"
"fmt"
"strings"
"github.com/photoview/photoview/api/dataloader"
@@ -10,94 +16,32 @@ import (
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/graphql/models/actions"
"github.com/photoview/photoview/api/scanner/face_detection"
"github.com/pkg/errors"
)
func (r *queryResolver) MyMedia(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
return actions.MyMedia(r.DB(ctx), user, order, paginate)
// Thumbnail is the resolver for the thumbnail field.
func (r *mediaResolver) Thumbnail(ctx context.Context, obj *models.Media) (*models.MediaURL, error) {
return dataloader.For(ctx).MediaThumbnail.Load(obj.ID)
}
func (r *queryResolver) Media(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Media, error) {
db := r.DB(ctx)
if tokenCredentials != nil {
shareToken, err := r.ShareToken(ctx, *tokenCredentials)
if err != nil {
return nil, err
}
if *shareToken.MediaID == id {
return shareToken.Media, nil
}
// HighRes is the resolver for the highRes field.
func (r *mediaResolver) HighRes(ctx context.Context, obj *models.Media) (*models.MediaURL, error) {
if obj.Type != models.MediaTypePhoto {
return nil, nil
}
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
var media models.Media
err := db.
Joins("Album").
Where("media.id = ?", id).
Where("EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = media.album_id AND user_albums.user_id = ?)",
user.ID).
Where("media.id IN (?)", db.Model(&models.MediaURL{}).Select("media_id").Where("media_urls.media_id = media.id")).
First(&media).Error
if err != nil {
return nil, errors.Wrap(err, "could not get media by media_id and user_id from database")
}
return &media, nil
return dataloader.For(ctx).MediaHighres.Load(obj.ID)
}
func (r *queryResolver) MediaList(ctx context.Context, ids []int) ([]*models.Media, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
// VideoWeb is the resolver for the videoWeb field.
func (r *mediaResolver) VideoWeb(ctx context.Context, obj *models.Media) (*models.MediaURL, error) {
if obj.Type != models.MediaTypeVideo {
return nil, nil
}
if len(ids) == 0 {
return nil, errors.New("no ids provided")
}
var media []*models.Media
err := db.Model(&media).
Joins("LEFT JOIN user_albums ON user_albums.album_id = media.album_id").
Where("media.id IN ?", ids).
Where("user_albums.user_id = ?", user.ID).
Find(&media).Error
if err != nil {
return nil, errors.Wrap(err, "could not get media list by media_id and user_id from database")
}
return media, nil
}
type mediaResolver struct {
*Resolver
}
func (r *Resolver) Media() api.MediaResolver {
return &mediaResolver{r}
}
func (r *mediaResolver) Type(ctx context.Context, media *models.Media) (models.MediaType, error) {
formattedType := models.MediaType(strings.Title(string(media.Type)))
return formattedType, nil
return dataloader.For(ctx).MediaVideoWeb.Load(obj.ID)
}
// Album is the resolver for the album field.
func (r *mediaResolver) Album(ctx context.Context, obj *models.Media) (*models.Album, error) {
var album models.Album
err := r.DB(ctx).Find(&album, obj.AlbumID).Error
@@ -107,20 +51,54 @@ func (r *mediaResolver) Album(ctx context.Context, obj *models.Media) (*models.A
return &album, nil
}
func (r *mediaResolver) Shares(ctx context.Context, media *models.Media) ([]*models.ShareToken, error) {
// Exif is the resolver for the exif field.
func (r *mediaResolver) Exif(ctx context.Context, obj *models.Media) (*models.MediaEXIF, error) {
if obj.Exif != nil {
return obj.Exif, nil
}
var exif models.MediaEXIF
if err := r.DB(ctx).Model(obj).Association("Exif").Find(&exif); err != nil {
return nil, err
}
return &exif, nil
}
// Favorite is the resolver for the favorite field.
func (r *mediaResolver) Favorite(ctx context.Context, obj *models.Media) (bool, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return false, auth.ErrUnauthorized
}
return dataloader.For(ctx).UserMediaFavorite.Load(&models.UserMediaData{
UserID: user.ID,
MediaID: obj.ID,
})
}
// Type is the resolver for the type field.
func (r *mediaResolver) Type(ctx context.Context, obj *models.Media) (models.MediaType, error) {
formattedType := models.MediaType(strings.Title(string(obj.Type)))
return formattedType, nil
}
// Shares is the resolver for the shares field.
func (r *mediaResolver) Shares(ctx context.Context, obj *models.Media) ([]*models.ShareToken, error) {
var shareTokens []*models.ShareToken
if err := r.DB(ctx).Where("media_id = ?", media.ID).Find(&shareTokens).Error; err != nil {
return nil, errors.Wrapf(err, "get shares for media (%s)", media.Path)
if err := r.DB(ctx).Where("media_id = ?", obj.ID).Find(&shareTokens).Error; err != nil {
return nil, fmt.Errorf("get shares for media (%s): %w", obj.Path, err)
}
return shareTokens, nil
}
func (r *mediaResolver) Downloads(ctx context.Context, media *models.Media) ([]*models.MediaDownload, error) {
// Downloads is the resolver for the downloads field.
func (r *mediaResolver) Downloads(ctx context.Context, obj *models.Media) ([]*models.MediaDownload, error) {
var mediaUrls []*models.MediaURL
if err := r.DB(ctx).Where("media_id = ?", media.ID).Find(&mediaUrls).Error; err != nil {
return nil, errors.Wrapf(err, "get downloads for media (%s)", media.Path)
if err := r.DB(ctx).Where("media_id = ?", obj.ID).Find(&mediaUrls).Error; err != nil {
return nil, fmt.Errorf("get downloads for media (%s): %w", obj.Path, err)
}
downloads := make([]*models.MediaDownload, 0)
@@ -150,53 +128,26 @@ func (r *mediaResolver) Downloads(ctx context.Context, media *models.Media) ([]*
return downloads, nil
}
func (r *mediaResolver) HighRes(ctx context.Context, media *models.Media) (*models.MediaURL, error) {
if media.Type != models.MediaTypePhoto {
return nil, nil
// Faces is the resolver for the faces field.
func (r *mediaResolver) Faces(ctx context.Context, obj *models.Media) ([]*models.ImageFace, error) {
if face_detection.GlobalFaceDetector == nil {
return []*models.ImageFace{}, nil
}
return dataloader.For(ctx).MediaHighres.Load(media.ID)
}
func (r *mediaResolver) Thumbnail(ctx context.Context, media *models.Media) (*models.MediaURL, error) {
return dataloader.For(ctx).MediaThumbnail.Load(media.ID)
}
func (r *mediaResolver) VideoWeb(ctx context.Context, media *models.Media) (*models.MediaURL, error) {
if media.Type != models.MediaTypeVideo {
return nil, nil
if obj.Faces != nil {
return obj.Faces, nil
}
return dataloader.For(ctx).MediaVideoWeb.Load(media.ID)
}
func (r *mediaResolver) Exif(ctx context.Context, media *models.Media) (*models.MediaEXIF, error) {
if media.Exif != nil {
return media.Exif, nil
}
var exif models.MediaEXIF
if err := r.DB(ctx).Model(&media).Association("Exif").Find(&exif); err != nil {
var faces []*models.ImageFace
if err := r.DB(ctx).Model(obj).Association("Faces").Find(&faces); err != nil {
return nil, err
}
return &exif, nil
}
func (r *mediaResolver) Favorite(ctx context.Context, media *models.Media) (bool, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return false, auth.ErrUnauthorized
}
return dataloader.For(ctx).UserMediaFavorite.Load(&models.UserMediaData{
UserID: user.ID,
MediaID: media.ID,
})
return faces, nil
}
// FavoriteMedia is the resolver for the favoriteMedia field.
func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favorite bool) (*models.Media, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
@@ -205,19 +156,80 @@ func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favor
return user.FavoriteMedia(r.DB(ctx), mediaID, favorite)
}
func (r *mediaResolver) Faces(ctx context.Context, media *models.Media) ([]*models.ImageFace, error) {
if face_detection.GlobalFaceDetector == nil {
return []*models.ImageFace{}, nil
// MyMedia is the resolver for the myMedia field.
func (r *queryResolver) MyMedia(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if media.Faces != nil {
return media.Faces, nil
}
var faces []*models.ImageFace
if err := r.DB(ctx).Model(&media).Association("Faces").Find(&faces); err != nil {
return nil, err
}
return faces, nil
return actions.MyMedia(r.DB(ctx), user, order, paginate)
}
// Media is the resolver for the media field.
func (r *queryResolver) Media(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Media, error) {
db := r.DB(ctx)
if tokenCredentials != nil {
shareToken, err := r.ShareToken(ctx, *tokenCredentials)
if err != nil {
return nil, err
}
if *shareToken.MediaID == id {
return shareToken.Media, nil
}
}
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
var media models.Media
err := db.
Joins("Album").
Where("media.id = ?", id).
Where("EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = media.album_id AND user_albums.user_id = ?)",
user.ID).
Where("media.id IN (?)", db.Model(&models.MediaURL{}).Select("media_id").Where("media_urls.media_id = media.id")).
First(&media).Error
if err != nil {
return nil, fmt.Errorf("could not get media by media_id and user_id from database: %w", err)
}
return &media, nil
}
// MediaList is the resolver for the mediaList field.
func (r *queryResolver) MediaList(ctx context.Context, ids []int) ([]*models.Media, error) {
db := r.DB(ctx)
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
}
if len(ids) == 0 {
return nil, errors.New("no ids provided")
}
var media []*models.Media
err := db.Model(&media).
Joins("LEFT JOIN user_albums ON user_albums.album_id = media.album_id").
Where("media.id IN ?", ids).
Where("user_albums.user_id = ?", user.ID).
Find(&media).Error
if err != nil {
return nil, fmt.Errorf("could not get media list by media_id and user_id from database: %w", err)
}
return media, nil
}
// Media returns api.MediaResolver implementation.
func (r *Resolver) Media() api.MediaResolver { return &mediaResolver{r} }
type mediaResolver struct{ *Resolver }

View File

@@ -0,0 +1,100 @@
type MediaURL {
"URL for previewing the image"
url: String!
"Width of the image in pixels"
width: Int!
"Height of the image in pixels"
height: Int!
"The file size of the resource in bytes"
fileSize: Int!
}
type MediaDownload {
"A description of the role of the media file"
title: String!
mediaUrl: MediaURL!
}
enum MediaType {
Photo
Video
}
"EXIF metadata from the camera"
type MediaEXIF {
id: ID!
media: Media!
"The description of the image"
description: String
"The model name of the camera"
camera: String
"The maker of the camera"
maker: String
"The name of the lens"
lens: String
dateShot: Time
"The exposure time of the image"
exposure: Float
"The aperature stops of the image"
aperture: Float
"The ISO setting of the image"
iso: Int
"The focal length of the lens, when the image was taken"
focalLength: Float
"A formatted description of the flash settings, when the image was taken"
flash: Int
"An index describing the mode for adjusting the exposure of the image"
exposureProgram: Int
"GPS coordinates of where the image was taken"
coordinates: Coordinates
}
type Media {
id: ID!
title: String!
"Local filepath for the media"
path: String!
"URL to display the media in a smaller resolution"
thumbnail: MediaURL
"URL to display the photo in full resolution, will be null for videos"
highRes: MediaURL
"URL to get the video in a web format that can be played in the browser, will be null for photos"
videoWeb: MediaURL
"The album that holds the media"
album: Album!
exif: MediaEXIF
videoMetadata: VideoMetadata
favorite: Boolean!
type: MediaType!
"The date the image was shot or the date it was imported as a fallback"
date: Time!
"A short string that can be used to generate a blured version of the media, to show while the original is loading"
blurhash: String
"A list of share tokens pointing to this media, owned byt the logged in user"
shares: [ShareToken!]!
"A list of different versions of files for this media that can be downloaded by the user"
downloads: [MediaDownload!]!
"A list of faces present on the image"
faces: [ImageFace!]!
}
extend type Query {
"List of media owned by the logged in user"
myMedia(order: Ordering, paginate: Pagination): [Media!]! @isAuthorized
"""
Get media by id, user must own the media or be admin.
If valid tokenCredentials are provided, the media may be retrived without further authentication
"""
media(id: ID!, tokenCredentials: ShareTokenCredentials): Media!
"Get a list of media by their ids, user must own the media or be admin"
mediaList(ids: [ID!]!): [Media!]!
}
extend type Mutation {
"Mark or unmark a media as being a favorite"
favoriteMedia(mediaId: ID!, favorite: Boolean!): Media! @isAuthorized
}

View File

@@ -1,5 +1,9 @@
package resolvers
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.55
import (
"context"
"os"
@@ -9,67 +13,8 @@ import (
"github.com/photoview/photoview/api/utils"
)
type geoMedia struct {
MediaID int
MediaTitle string
ThumbnailName string
ThumbnailWidth int
ThumbnailHeight int
Latitude float64
Longitude float64
}
type geoJSONFeatureCollection struct {
Type string `json:"type"`
Features []geoJSONFeature `json:"features"`
}
type geoJSONFeature struct {
Type string `json:"type"`
Properties interface{} `json:"properties"`
Geometry geoJSONFeatureGeometry `json:"geometry"`
}
type geoJSONMediaProperties struct {
MediaID int `json:"media_id"`
MediaTitle string `json:"media_title"`
Thumbnail struct {
URL string `json:"url"`
Width int `json:"width"`
Height int `json:"height"`
} `json:"thumbnail"`
}
type geoJSONFeatureGeometry struct {
Type string `json:"type"`
Coordinates [2]float64 `json:"coordinates"`
}
func makeGeoJSONFeatureCollection(features []geoJSONFeature) geoJSONFeatureCollection {
return geoJSONFeatureCollection{
Type: "FeatureCollection",
Features: features,
}
}
func makeGeoJSONFeature(properties interface{}, geometry geoJSONFeatureGeometry) geoJSONFeature {
return geoJSONFeature{
Type: "Feature",
Properties: properties,
Geometry: geometry,
}
}
func makeGeoJSONFeatureGeometryPoint(lat float64, long float64) geoJSONFeatureGeometry {
coordinates := [2]float64{long, lat}
return geoJSONFeatureGeometry{
Type: "Point",
Coordinates: coordinates,
}
}
func (r *queryResolver) MyMediaGeoJSON(ctx context.Context) (interface{}, error) {
// MyMediaGeoJSON is the resolver for the myMediaGeoJson field.
func (r *queryResolver) MyMediaGeoJSON(ctx context.Context) (any, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, auth.ErrUnauthorized
@@ -124,6 +69,7 @@ func (r *queryResolver) MyMediaGeoJSON(ctx context.Context) (interface{}, error)
return featureCollection, nil
}
// MapboxToken is the resolver for the mapboxToken field.
func (r *queryResolver) MapboxToken(ctx context.Context) (*string, error) {
mapboxTokenEnv := os.Getenv("MAPBOX_TOKEN")
if mapboxTokenEnv == "" {

View File

@@ -0,0 +1,61 @@
package resolvers
type geoMedia struct {
MediaID int
MediaTitle string
ThumbnailName string
ThumbnailWidth int
ThumbnailHeight int
Latitude float64
Longitude float64
}
type geoJSONFeatureCollection struct {
Type string `json:"type"`
Features []geoJSONFeature `json:"features"`
}
type geoJSONFeature struct {
Type string `json:"type"`
Properties interface{} `json:"properties"`
Geometry geoJSONFeatureGeometry `json:"geometry"`
}
type geoJSONMediaProperties struct {
MediaID int `json:"media_id"`
MediaTitle string `json:"media_title"`
Thumbnail struct {
URL string `json:"url"`
Width int `json:"width"`
Height int `json:"height"`
} `json:"thumbnail"`
}
type geoJSONFeatureGeometry struct {
Type string `json:"type"`
Coordinates [2]float64 `json:"coordinates"`
}
func makeGeoJSONFeatureCollection(features []geoJSONFeature) geoJSONFeatureCollection {
return geoJSONFeatureCollection{
Type: "FeatureCollection",
Features: features,
}
}
func makeGeoJSONFeature(properties interface{}, geometry geoJSONFeatureGeometry) geoJSONFeature {
return geoJSONFeature{
Type: "Feature",
Properties: properties,
Geometry: geometry,
}
}
func makeGeoJSONFeatureGeometryPoint(lat float64, long float64) geoJSONFeatureGeometry {
coordinates := [2]float64{long, lat}
return geoJSONFeatureGeometry{
Type: "Point",
Coordinates: coordinates,
}
}

View File

@@ -0,0 +1,7 @@
extend 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
}

View File

@@ -44,34 +44,6 @@ type Query {
"User preferences for the logged in user"
myUserPreferences: UserPreferences! @isAuthorized
"List of albums owned by the logged in user."
myAlbums(
order: Ordering,
paginate: Pagination
"Return only albums from the root directory of the user"
onlyRoot: Boolean
"Return also albums with no media directly in them"
showEmpty: Boolean
"Show only albums having favorites"
onlyWithFavorites: Boolean
): [Album!]! @isAuthorized
"""
Get album by id, user must own the album or be admin
If valid tokenCredentials are provided, the album may be retrived without further authentication
"""
album(id: ID!, tokenCredentials: ShareTokenCredentials): Album!
"List of media owned by the logged in user"
myMedia(order: Ordering, paginate: Pagination): [Media!]! @isAuthorized
"""
Get media by id, user must own the media or be admin.
If valid tokenCredentials are provided, the media may be retrived without further authentication
"""
media(id: ID!, tokenCredentials: ShareTokenCredentials): Media!
"Get a list of media by their ids, user must own the media or be admin"
mediaList(ids: [ID!]!): [Media!]!
"""
Get a list of media, ordered first by day, then by album if multiple media was found for the same day.
"""
@@ -82,11 +54,6 @@ type Query {
fromDate: Time
): [Media!]! @isAuthorized
"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
"Fetch a share token containing an `Album` or `Media`"
shareToken(credentials: ShareTokenCredentials!): ShareToken!
"Check if the `ShareToken` credentials are valid"
@@ -94,11 +61,6 @@ type Query {
"Perform a search query on the contents of the media library"
search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult!
"Get a list of `FaceGroup`s for the logged in user"
myFaceGroups(paginate: Pagination): [FaceGroup!]! @isAuthorized
"Get a particular `FaceGroup` specified by its ID"
faceGroup(id: ID!): FaceGroup! @isAuthorized
}
type Mutation {
@@ -126,9 +88,6 @@ type Mutation {
"Set a password for a token, if null is passed for the password argument, the password will be cleared"
protectShareToken(token: String!, password: String): ShareToken! @isAuthorized
"Mark or unmark a media as being a favorite"
favoriteMedia(mediaId: ID!, favorite: Boolean!): Media! @isAuthorized
"Update a user, fields left as `null` will not be changed"
updateUser(
id: ID!
@@ -168,22 +127,6 @@ type Mutation {
"Change user preferences for the logged in user"
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"
combineFaceGroups(destinationFaceGroupID: ID!, sourceFaceGroupID: ID!): FaceGroup! @isAuthorized
"Move a list of ImageFaces to another face group"
moveImageFaces(imageFaceIDs: [ID!]!, destinationFaceGroupID: ID!): FaceGroup! @isAuthorized
"Check all unlabeled faces to see if they match a labeled FaceGroup, and move them if they match"
recognizeUnlabeledFaces: [ImageFace!]! @isAuthorized
"Move a list of ImageFaces to a new face group"
detachImageFaces(imageFaceIDs: [ID!]!): FaceGroup! @isAuthorized
}
type Subscription {
@@ -310,121 +253,6 @@ type UserPreferences {
language: LanguageTranslation
}
type Album {
id: ID!
title: String!
"The media inside this album"
media(
order: Ordering,
paginate: Pagination
"Return only the favorited media"
onlyFavorites: Boolean
): [Media!]!
"The albums contained in this album"
subAlbums(
order: Ordering,
paginate: Pagination
): [Album!]!
"The album which contains this album"
parentAlbum: Album
"The user who owns this album"
owner: User!
"The path on the filesystem of the server, where this album is located"
filePath: String!
"An image in this album used for previewing this album"
thumbnail: Media
"A breadcrumb list of all parent albums down to this one"
path: [Album!]!
"A list of share tokens pointing to this album, owned by the logged in user"
shares: [ShareToken!]!
}
type MediaURL {
"URL for previewing the image"
url: String!
"Width of the image in pixels"
width: Int!
"Height of the image in pixels"
height: Int!
"The file size of the resource in bytes"
fileSize: Int!
}
type MediaDownload {
"A description of the role of the media file"
title: String!
mediaUrl: MediaURL!
}
enum MediaType {
Photo
Video
}
type Media {
id: ID!
title: String!
"Local filepath for the media"
path: String!
"URL to display the media in a smaller resolution"
thumbnail: MediaURL
"URL to display the photo in full resolution, will be null for videos"
highRes: MediaURL
"URL to get the video in a web format that can be played in the browser, will be null for photos"
videoWeb: MediaURL
"The album that holds the media"
album: Album!
exif: MediaEXIF
videoMetadata: VideoMetadata
favorite: Boolean!
type: MediaType!
"The date the image was shot or the date it was imported as a fallback"
date: Time!
"A short string that can be used to generate a blured version of the media, to show while the original is loading"
blurhash: String
"A list of share tokens pointing to this media, owned byt the logged in user"
shares: [ShareToken!]!
"A list of different versions of files for this media that can be downloaded by the user"
downloads: [MediaDownload!]!
"A list of faces present on the image"
faces: [ImageFace!]!
}
"EXIF metadata from the camera"
type MediaEXIF {
id: ID!
media: Media!
"The description of the image"
description: String
"The model name of the camera"
camera: String
"The maker of the camera"
maker: String
"The name of the lens"
lens: String
dateShot: Time
"The exposure time of the image"
exposure: Float
"The aperature stops of the image"
aperture: Float
"The ISO setting of the image"
iso: Int
"The focal length of the lens, when the image was taken"
focalLength: Float
"A formatted description of the flash settings, when the image was taken"
flash: Int
"An index describing the mode for adjusting the exposure of the image"
exposureProgram: Int
"GPS coordinates of where the image was taken"
coordinates: Coordinates
}
type Coordinates {
"GPS latitude in degrees"
latitude: Float!
@@ -466,32 +294,3 @@ type TimelineGroup {
"The day shared for all media in this timeline group"
date: Time!
}
"A collection of faces of a particular person"
type FaceGroup {
id: ID!
"The name of the person"
label: String
imageFaces(paginate: Pagination): [ImageFace!]!
"The total number of images in this collection"
imageFaceCount: Int!
}
"A single face on a particular image"
type ImageFace {
id: ID!
"A reference to the image the face appears on"
media: Media!
"A bounding box of where on the image the face is present"
rectangle: FaceRectangle!
"The `FaceGroup` that contains this `ImageFace`"
faceGroup: FaceGroup!
}
"A bounding box of where a face is present on an image. The values map from 0 to 1 as a fraction of the image width/height"
type FaceRectangle {
minX: Float!
maxX: Float!
minY: Float!
maxY: Float!
}