mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 17:09:13 +00:00
Fix gqlgen code (#1099)
This commit is contained in:
@@ -1,10 +1,7 @@
|
|||||||
# .gqlgen.yml example
|
|
||||||
#
|
|
||||||
# Refer to https://gqlgen.com/config/
|
# Refer to https://gqlgen.com/config/
|
||||||
# for detailed .gqlgen.yml documentation.
|
|
||||||
|
|
||||||
schema:
|
schema:
|
||||||
- graphql/schema.graphql
|
- graphql/resolvers/*.graphql
|
||||||
|
|
||||||
exec:
|
exec:
|
||||||
filename: graphql/generated.go
|
filename: graphql/generated.go
|
||||||
@@ -14,8 +11,10 @@ model:
|
|||||||
package: models
|
package: models
|
||||||
|
|
||||||
resolver:
|
resolver:
|
||||||
filename: graphql/resolvers/root.go
|
layout: follow-schema
|
||||||
type: Resolver
|
dir: graphql/resolvers
|
||||||
|
package: resolvers
|
||||||
|
filename_template: "{name}.generated.go"
|
||||||
|
|
||||||
autobind: []
|
autobind: []
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,76 +1,27 @@
|
|||||||
package resolvers
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
api "github.com/photoview/photoview/api/graphql"
|
api "github.com/photoview/photoview/api/graphql"
|
||||||
"github.com/photoview/photoview/api/graphql/auth"
|
"github.com/photoview/photoview/api/graphql/auth"
|
||||||
"github.com/photoview/photoview/api/graphql/models"
|
"github.com/photoview/photoview/api/graphql/models"
|
||||||
"github.com/photoview/photoview/api/graphql/models/actions"
|
"github.com/photoview/photoview/api/graphql/models/actions"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) MyAlbums(ctx context.Context, order *models.Ordering, paginate *models.Pagination, onlyRoot *bool,
|
// Media is the resolver for the media field.
|
||||||
showEmpty *bool, onlyWithFavorites *bool) ([]*models.Album, error) {
|
func (r *albumResolver) Media(ctx context.Context, obj *models.Album, order *models.Ordering, paginate *models.Pagination, onlyFavorites *bool) ([]*models.Media, 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) {
|
|
||||||
|
|
||||||
db := r.DB(ctx)
|
db := r.DB(ctx)
|
||||||
|
|
||||||
query := db.
|
query := db.
|
||||||
Where("media.album_id = ?", album.ID).
|
Where("media.album_id = ?", obj.ID).
|
||||||
Where("media.id IN (?)", db.Model(&models.MediaURL{}).
|
Where("media.id IN (?)", db.Model(&models.MediaURL{}).
|
||||||
Select("media_urls.media_id").
|
Select("media_urls.media_id").
|
||||||
Where("media_urls.media_id = 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
|
return media, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *albumResolver) Thumbnail(ctx context.Context, album *models.Album) (*models.Media, error) {
|
// SubAlbums is the resolver for the subAlbums field.
|
||||||
return album.Thumbnail(r.DB(ctx))
|
func (r *albumResolver) SubAlbums(ctx context.Context, obj *models.Album, order *models.Ordering, paginate *models.Pagination) ([]*models.Album, error) {
|
||||||
}
|
|
||||||
|
|
||||||
func (r *albumResolver) SubAlbums(ctx context.Context, parent *models.Album, order *models.Ordering,
|
|
||||||
paginate *models.Pagination) ([]*models.Album, error) {
|
|
||||||
|
|
||||||
var albums []*models.Album
|
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)
|
query = models.FormatSQL(query, order, paginate)
|
||||||
|
|
||||||
if err := query.Find(&albums).Error; err != nil {
|
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
|
return albums, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *albumResolver) ParentAlbum(ctx context.Context, obj *models.Album) (*models.Album, error) {
|
// Owner is the resolver for the owner field.
|
||||||
panic("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *albumResolver) Owner(ctx context.Context, obj *models.Album) (*models.User, error) {
|
func (r *albumResolver) Owner(ctx context.Context, obj *models.Album) (*models.User, error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *albumResolver) Shares(ctx context.Context, album *models.Album) ([]*models.ShareToken, error) {
|
// Thumbnail is the resolver for the thumbnail field.
|
||||||
|
func (r *albumResolver) Thumbnail(ctx context.Context, obj *models.Album) (*models.Media, error) {
|
||||||
var shareTokens []*models.ShareToken
|
return obj.Thumbnail(r.DB(ctx))
|
||||||
if err := r.DB(ctx).Where("album_id = ?", album.ID).Find(&shareTokens).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return shareTokens, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path is the resolver for the path field.
|
||||||
func (r *albumResolver) Path(ctx context.Context, obj *models.Album) ([]*models.Album, error) {
|
func (r *albumResolver) Path(ctx context.Context, obj *models.Album) ([]*models.Album, error) {
|
||||||
|
|
||||||
user := auth.UserFromContext(ctx)
|
user := auth.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
empty := make([]*models.Album, 0)
|
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)
|
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)
|
// Takes album_id, resets album.cover_id to 0 (null)
|
||||||
func (r *mutationResolver) ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error) {
|
func (r *mutationResolver) ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error) {
|
||||||
user := auth.UserFromContext(ctx)
|
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)
|
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)
|
user := auth.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, errors.New("unauthorized")
|
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 }
|
||||||
60
api/graphql/resolvers/album.graphql
Normal file
60
api/graphql/resolvers/album.graphql
Normal 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
|
||||||
|
}
|
||||||
410
api/graphql/resolvers/faces.generated.go
Normal file
410
api/graphql/resolvers/faces.generated.go
Normal 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 }
|
||||||
@@ -1,432 +1,19 @@
|
|||||||
package resolvers
|
package resolvers
|
||||||
|
|
||||||
import (
|
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"
|
||||||
"github.com/photoview/photoview/api/scanner/face_detection"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const faceGroupIDisQuestion = "face_group_id = ?"
|
const faceGroupIDIsQuestion = "face_group_id = ?"
|
||||||
const mediaAlbumIDinQuestion = "media.album_id IN (?)"
|
const mediaAlbumIDInQuestion = "media.album_id IN (?)"
|
||||||
const imageFacesIDinQuestion = "image_faces.id IN (?)"
|
const imageFacesIDInQuestion = "image_faces.id IN (?)"
|
||||||
|
|
||||||
var ErrFaceDetectorNotInitialized = errors.New("face detector not initialized")
|
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) {
|
func userOwnedFaceGroup(db *gorm.DB, user *models.User, faceGroupID int) (*models.FaceGroup, error) {
|
||||||
if user.Admin {
|
if user.Admin {
|
||||||
var faceGroup models.FaceGroup
|
var faceGroup models.FaceGroup
|
||||||
@@ -451,18 +38,18 @@ func userOwnedFaceGroup(db *gorm.DB, user *models.User, faceGroupID int) (*model
|
|||||||
Select("image_faces.id").
|
Select("image_faces.id").
|
||||||
Table("image_faces").
|
Table("image_faces").
|
||||||
Joins("JOIN media ON media.id = image_faces.media_id").
|
Joins("JOIN media ON media.id = image_faces.media_id").
|
||||||
Where(mediaAlbumIDinQuestion, userAlbumIDs)
|
Where(mediaAlbumIDInQuestion, userAlbumIDs)
|
||||||
|
|
||||||
faceGroupQuery := db.
|
faceGroupQuery := db.
|
||||||
Model(&models.FaceGroup{}).
|
Model(&models.FaceGroup{}).
|
||||||
Joins("JOIN image_faces ON face_groups.id = image_faces.face_group_id").
|
Joins("JOIN image_faces ON face_groups.id = image_faces.face_group_id").
|
||||||
Where("face_groups.id = ?", faceGroupID).
|
Where("face_groups.id = ?", faceGroupID).
|
||||||
Where(imageFacesIDinQuestion, imageFaceQuery)
|
Where(imageFacesIDInQuestion, imageFaceQuery)
|
||||||
|
|
||||||
var faceGroup models.FaceGroup
|
var faceGroup models.FaceGroup
|
||||||
if err := faceGroupQuery.Find(&faceGroup).Error; err != nil {
|
if err := faceGroupQuery.Find(&faceGroup).Error; err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -483,11 +70,27 @@ func getUserOwnedImageFaces(tx *gorm.DB, user *models.User, imageFaceIDs []int)
|
|||||||
var userOwnedImageFaces []*models.ImageFace
|
var userOwnedImageFaces []*models.ImageFace
|
||||||
if err := tx.
|
if err := tx.
|
||||||
Joins("JOIN media ON media.id = image_faces.media_id").
|
Joins("JOIN media ON media.id = image_faces.media_id").
|
||||||
Where(mediaAlbumIDinQuestion, userAlbumIDs).
|
Where(mediaAlbumIDInQuestion, userAlbumIDs).
|
||||||
Where(imageFacesIDinQuestion, imageFaceIDs).
|
Where(imageFacesIDInQuestion, imageFaceIDs).
|
||||||
Find(&userOwnedImageFaces).Error; err != nil {
|
Find(&userOwnedImageFaces).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return userOwnedImageFaces, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
53
api/graphql/resolvers/faces.graphql
Normal file
53
api/graphql/resolvers/faces.graphql
Normal 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
|
||||||
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
package resolvers
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/photoview/photoview/api/dataloader"
|
"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"
|
||||||
"github.com/photoview/photoview/api/graphql/models/actions"
|
"github.com/photoview/photoview/api/graphql/models/actions"
|
||||||
"github.com/photoview/photoview/api/scanner/face_detection"
|
"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) {
|
// Thumbnail is the resolver for the thumbnail field.
|
||||||
|
func (r *mediaResolver) Thumbnail(ctx context.Context, obj *models.Media) (*models.MediaURL, error) {
|
||||||
user := auth.UserFromContext(ctx)
|
return dataloader.For(ctx).MediaThumbnail.Load(obj.ID)
|
||||||
if user == nil {
|
|
||||||
return nil, errors.New("unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions.MyMedia(r.DB(ctx), user, order, paginate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) Media(ctx context.Context, id int, tokenCredentials *models.ShareTokenCredentials) (*models.Media, error) {
|
// HighRes is the resolver for the highRes field.
|
||||||
|
func (r *mediaResolver) HighRes(ctx context.Context, obj *models.Media) (*models.MediaURL, error) {
|
||||||
db := r.DB(ctx)
|
if obj.Type != models.MediaTypePhoto {
|
||||||
if tokenCredentials != nil {
|
return nil, 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)
|
return dataloader.For(ctx).MediaHighres.Load(obj.ID)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) MediaList(ctx context.Context, ids []int) ([]*models.Media, error) {
|
// VideoWeb is the resolver for the videoWeb field.
|
||||||
db := r.DB(ctx)
|
func (r *mediaResolver) VideoWeb(ctx context.Context, obj *models.Media) (*models.MediaURL, error) {
|
||||||
user := auth.UserFromContext(ctx)
|
if obj.Type != models.MediaTypeVideo {
|
||||||
if user == nil {
|
return nil, nil
|
||||||
return nil, auth.ErrUnauthorized
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ids) == 0 {
|
return dataloader.For(ctx).MediaVideoWeb.Load(obj.ID)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Album is the resolver for the album field.
|
||||||
func (r *mediaResolver) Album(ctx context.Context, obj *models.Media) (*models.Album, error) {
|
func (r *mediaResolver) Album(ctx context.Context, obj *models.Media) (*models.Album, error) {
|
||||||
var album models.Album
|
var album models.Album
|
||||||
err := r.DB(ctx).Find(&album, obj.AlbumID).Error
|
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
|
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
|
var shareTokens []*models.ShareToken
|
||||||
if err := r.DB(ctx).Where("media_id = ?", media.ID).Find(&shareTokens).Error; err != nil {
|
if err := r.DB(ctx).Where("media_id = ?", obj.ID).Find(&shareTokens).Error; err != nil {
|
||||||
return nil, errors.Wrapf(err, "get shares for media (%s)", media.Path)
|
return nil, fmt.Errorf("get shares for media (%s): %w", obj.Path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return shareTokens, nil
|
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
|
var mediaUrls []*models.MediaURL
|
||||||
if err := r.DB(ctx).Where("media_id = ?", media.ID).Find(&mediaUrls).Error; err != nil {
|
if err := r.DB(ctx).Where("media_id = ?", obj.ID).Find(&mediaUrls).Error; err != nil {
|
||||||
return nil, errors.Wrapf(err, "get downloads for media (%s)", media.Path)
|
return nil, fmt.Errorf("get downloads for media (%s): %w", obj.Path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloads := make([]*models.MediaDownload, 0)
|
downloads := make([]*models.MediaDownload, 0)
|
||||||
@@ -150,53 +128,26 @@ func (r *mediaResolver) Downloads(ctx context.Context, media *models.Media) ([]*
|
|||||||
return downloads, nil
|
return downloads, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mediaResolver) HighRes(ctx context.Context, media *models.Media) (*models.MediaURL, error) {
|
// Faces is the resolver for the faces field.
|
||||||
if media.Type != models.MediaTypePhoto {
|
func (r *mediaResolver) Faces(ctx context.Context, obj *models.Media) ([]*models.ImageFace, error) {
|
||||||
return nil, nil
|
if face_detection.GlobalFaceDetector == nil {
|
||||||
|
return []*models.ImageFace{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataloader.For(ctx).MediaHighres.Load(media.ID)
|
if obj.Faces != nil {
|
||||||
}
|
return obj.Faces, nil
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataloader.For(ctx).MediaVideoWeb.Load(media.ID)
|
var faces []*models.ImageFace
|
||||||
}
|
if err := r.DB(ctx).Model(obj).Association("Faces").Find(&faces); err != nil {
|
||||||
|
|
||||||
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 {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &exif, nil
|
return faces, 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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FavoriteMedia is the resolver for the favoriteMedia field.
|
||||||
func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favorite bool) (*models.Media, error) {
|
func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favorite bool) (*models.Media, error) {
|
||||||
|
|
||||||
user := auth.UserFromContext(ctx)
|
user := auth.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, auth.ErrUnauthorized
|
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)
|
return user.FavoriteMedia(r.DB(ctx), mediaID, favorite)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mediaResolver) Faces(ctx context.Context, media *models.Media) ([]*models.ImageFace, error) {
|
// MyMedia is the resolver for the myMedia field.
|
||||||
if face_detection.GlobalFaceDetector == nil {
|
func (r *queryResolver) MyMedia(ctx context.Context, order *models.Ordering, paginate *models.Pagination) ([]*models.Media, error) {
|
||||||
return []*models.ImageFace{}, nil
|
user := auth.UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
return nil, errors.New("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
if media.Faces != nil {
|
return actions.MyMedia(r.DB(ctx), user, order, paginate)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 }
|
||||||
100
api/graphql/resolvers/media.graphql
Normal file
100
api/graphql/resolvers/media.graphql
Normal 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
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package resolvers
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
@@ -9,67 +13,8 @@ import (
|
|||||||
"github.com/photoview/photoview/api/utils"
|
"github.com/photoview/photoview/api/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type geoMedia struct {
|
// MyMediaGeoJSON is the resolver for the myMediaGeoJson field.
|
||||||
MediaID int
|
func (r *queryResolver) MyMediaGeoJSON(ctx context.Context) (any, error) {
|
||||||
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) {
|
|
||||||
user := auth.UserFromContext(ctx)
|
user := auth.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, auth.ErrUnauthorized
|
return nil, auth.ErrUnauthorized
|
||||||
@@ -124,6 +69,7 @@ func (r *queryResolver) MyMediaGeoJSON(ctx context.Context) (interface{}, error)
|
|||||||
return featureCollection, nil
|
return featureCollection, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapboxToken is the resolver for the mapboxToken field.
|
||||||
func (r *queryResolver) MapboxToken(ctx context.Context) (*string, error) {
|
func (r *queryResolver) MapboxToken(ctx context.Context) (*string, error) {
|
||||||
mapboxTokenEnv := os.Getenv("MAPBOX_TOKEN")
|
mapboxTokenEnv := os.Getenv("MAPBOX_TOKEN")
|
||||||
if mapboxTokenEnv == "" {
|
if mapboxTokenEnv == "" {
|
||||||
61
api/graphql/resolvers/media_geo_json.go
Normal file
61
api/graphql/resolvers/media_geo_json.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
7
api/graphql/resolvers/media_geo_json.graphql
Normal file
7
api/graphql/resolvers/media_geo_json.graphql
Normal 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
|
||||||
|
}
|
||||||
@@ -44,34 +44,6 @@ type Query {
|
|||||||
"User preferences for the logged in user"
|
"User preferences for the logged in user"
|
||||||
myUserPreferences: UserPreferences! @isAuthorized
|
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.
|
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
|
fromDate: Time
|
||||||
): [Media!]! @isAuthorized
|
): [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`"
|
"Fetch a share token containing an `Album` or `Media`"
|
||||||
shareToken(credentials: ShareTokenCredentials!): ShareToken!
|
shareToken(credentials: ShareTokenCredentials!): ShareToken!
|
||||||
"Check if the `ShareToken` credentials are valid"
|
"Check if the `ShareToken` credentials are valid"
|
||||||
@@ -94,11 +61,6 @@ type Query {
|
|||||||
|
|
||||||
"Perform a search query on the contents of the media library"
|
"Perform a search query on the contents of the media library"
|
||||||
search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult!
|
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 {
|
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"
|
"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
|
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"
|
"Update a user, fields left as `null` will not be changed"
|
||||||
updateUser(
|
updateUser(
|
||||||
id: ID!
|
id: ID!
|
||||||
@@ -168,22 +127,6 @@ type Mutation {
|
|||||||
|
|
||||||
"Change user preferences for the logged in user"
|
"Change user preferences for the logged in user"
|
||||||
changeUserPreferences(language: String): UserPreferences! @isAuthorized
|
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 {
|
type Subscription {
|
||||||
@@ -310,121 +253,6 @@ type UserPreferences {
|
|||||||
language: LanguageTranslation
|
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 {
|
type Coordinates {
|
||||||
"GPS latitude in degrees"
|
"GPS latitude in degrees"
|
||||||
latitude: Float!
|
latitude: Float!
|
||||||
@@ -466,32 +294,3 @@ type TimelineGroup {
|
|||||||
"The day shared for all media in this timeline group"
|
"The day shared for all media in this timeline group"
|
||||||
date: Time!
|
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!
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user