diff --git a/api/gqlgen.yml b/api/gqlgen.yml index 2837525d..f0601de8 100644 --- a/api/gqlgen.yml +++ b/api/gqlgen.yml @@ -36,6 +36,8 @@ models: resolver: true faces: resolver: true + mediaType: + resolver: true MediaURL: model: github.com/photoview/photoview/api/graphql/models.MediaURL MediaEXIF: @@ -60,3 +62,5 @@ models: model: github.com/photoview/photoview/api/graphql/models.FaceRectangle SiteInfo: model: github.com/photoview/photoview/api/graphql/models.SiteInfo + MediaType: + model: github.com/photoview/photoview/api/graphql/models.MediaType diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 93539f96..5fb9a782 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -11993,13 +11993,19 @@ func (ec *executionContext) marshalNMediaDownload2ᚖgithubᚗcomᚋphotoviewᚋ } func (ec *executionContext) unmarshalNMediaType2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaType(ctx context.Context, v interface{}) (models.MediaType, error) { - var res models.MediaType - err := res.UnmarshalGQL(v) + tmp, err := graphql.UnmarshalString(v) + res := models.MediaType(tmp) return res, graphql.ErrorOnPath(ctx, err) } func (ec *executionContext) marshalNMediaType2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaType(ctx context.Context, sel ast.SelectionSet, v models.MediaType) graphql.Marshaler { - return v + res := graphql.MarshalString(string(v)) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + } + return res } func (ec *executionContext) marshalNMediaURL2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaURL(ctx context.Context, sel ast.SelectionSet, v *models.MediaURL) graphql.Marshaler { diff --git a/api/graphql/models/album.go b/api/graphql/models/album.go index f6543d4a..9be3833c 100644 --- a/api/graphql/models/album.go +++ b/api/graphql/models/album.go @@ -32,7 +32,10 @@ func (a *Album) BeforeSave(tx *gorm.DB) (err error) { // GetChildren performs a recursive query to get all the children of the album. // An optional filter can be provided that can be used to modify the query on the children. func (a *Album) GetChildren(db *gorm.DB, filter func(*gorm.DB) *gorm.DB) (children []*Album, err error) { - // SELECT * FROM sub_albums + return GetChildrenFromAlbums(db, filter, []int{a.ID}) +} + +func GetChildrenFromAlbums(db *gorm.DB, filter func(*gorm.DB) *gorm.DB, albumIDs []int) (children []*Album, err error) { query := db.Model(&Album{}).Table("sub_albums") if filter != nil { @@ -41,13 +44,13 @@ func (a *Album) GetChildren(db *gorm.DB, filter func(*gorm.DB) *gorm.DB) (childr err = db.Raw(` WITH recursive sub_albums AS ( - SELECT * FROM albums AS root WHERE id = ? + SELECT * FROM albums AS root WHERE id IN (?) UNION ALL SELECT child.* FROM albums AS child JOIN sub_albums ON child.parent_album_id = sub_albums.id ) ? - `, a.ID, query).Find(&children).Error + `, albumIDs, query).Find(&children).Error return children, err } diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go index 4741dd70..73da4be7 100644 --- a/api/graphql/models/generated.go +++ b/api/graphql/models/generated.go @@ -121,47 +121,6 @@ func (e LanguageTranslation) MarshalGQL(w io.Writer) { fmt.Fprint(w, strconv.Quote(e.String())) } -type MediaType string - -const ( - MediaTypePhoto MediaType = "Photo" - MediaTypeVideo MediaType = "Video" -) - -var AllMediaType = []MediaType{ - MediaTypePhoto, - MediaTypeVideo, -} - -func (e MediaType) IsValid() bool { - switch e { - case MediaTypePhoto, MediaTypeVideo: - return true - } - return false -} - -func (e MediaType) String() string { - return string(e) -} - -func (e *MediaType) UnmarshalGQL(v interface{}) error { - str, ok := v.(string) - if !ok { - return fmt.Errorf("enums must be strings") - } - - *e = MediaType(str) - if !e.IsValid() { - return fmt.Errorf("%s is not a valid MediaType", str) - } - return nil -} - -func (e MediaType) MarshalGQL(w io.Writer) { - fmt.Fprint(w, strconv.Quote(e.String())) -} - type NotificationType string const ( diff --git a/api/graphql/models/media.go b/api/graphql/models/media.go index b6c6bb9c..259d6b31 100644 --- a/api/graphql/models/media.go +++ b/api/graphql/models/media.go @@ -42,30 +42,19 @@ func (m *Media) BeforeSave(tx *gorm.DB) error { // Update path hash m.PathHash = MD5Hash(m.Path) - // Save media type as lowercase for better compatibility - m.Type = MediaType(strings.ToLower(string(m.Type))) - return nil } -func (m *Media) AfterFind(tx *gorm.DB) error { +type MediaType string - // Convert lowercased media type back - lowercasedType := strings.ToLower(string(m.Type)) - foundType := false - for _, t := range AllMediaType { - if strings.ToLower(string(t)) == lowercasedType { - m.Type = t - foundType = true - break - } - } +const ( + MediaTypePhoto MediaType = "photo" + MediaTypeVideo MediaType = "video" +) - if foundType == false { - return errors.New(fmt.Sprintf("Failed to parse media from DB: Invalid media type: %s", m.Type)) - } - - return nil +var AllMediaType = []MediaType{ + MediaTypePhoto, + MediaTypeVideo, } type MediaPurpose string diff --git a/api/graphql/models/user.go b/api/graphql/models/user.go index b3879a19..777e7068 100644 --- a/api/graphql/models/user.go +++ b/api/graphql/models/user.go @@ -173,11 +173,29 @@ func (user *User) FillAlbums(db *gorm.DB) error { } func (user *User) OwnsAlbum(db *gorm.DB, album *Album) (bool, error) { - // TODO: Implement this - panic("not implemented") + + if err := user.FillAlbums(db); err != nil { + return false, err + } + + albumIDs := make([]int, 0) + for _, a := range user.Albums { + albumIDs = append(albumIDs, a.ID) + } + + filter := func(query *gorm.DB) *gorm.DB { + return query.Where("id = ?", album.ID) + } + + ownedAlbum, err := GetChildrenFromAlbums(db, filter, albumIDs) + if err != nil { + return false, err + } + + return len(ownedAlbum) > 0, nil } -func (user *User) OwnsMedia(db *gorm.DB, media *Media) (bool, error) { - // TODO: implement this - panic("not implemented") -} +// func (user *User) OwnsMedia(db *gorm.DB, media *Media) (bool, error) { +// // TODO: implement this +// panic("not implemented") +// } diff --git a/api/graphql/resolvers/faces.go b/api/graphql/resolvers/faces.go index db2e12ef..c431a13d 100644 --- a/api/graphql/resolvers/faces.go +++ b/api/graphql/resolvers/faces.go @@ -69,15 +69,6 @@ func (r faceGroupResolver) ImageFaces(ctx context.Context, obj *models.FaceGroup return nil, err } - r.Database.Transaction(func(tx *gorm.DB) error { - for i := range imageFaces { - if err := imageFaces[i].Media.AfterFind(tx); err != nil { - return err - } - } - return nil - }) - return imageFaces, nil } diff --git a/api/graphql/resolvers/media.go b/api/graphql/resolvers/media.go index 2cc0cf52..882d6bd5 100644 --- a/api/graphql/resolvers/media.go +++ b/api/graphql/resolvers/media.go @@ -2,6 +2,7 @@ package resolvers import ( "context" + "strings" api "github.com/photoview/photoview/api/graphql" "github.com/photoview/photoview/api/graphql/auth" @@ -108,6 +109,11 @@ func (r *Resolver) Media() api.MediaResolver { return &mediaResolver{r} } +func (r *mediaResolver) MediaType(ctx context.Context, media *models.Media) (*models.MediaType, error) { + formattedType := models.MediaType(strings.Title(string(media.Type))) + return &formattedType, nil +} + func (r *mediaResolver) Shares(ctx context.Context, media *models.Media) ([]*models.ShareToken, error) { var shareTokens []*models.ShareToken if err := r.Database.Where("media_id = ?", media.ID).Find(&shareTokens).Error; err != nil { diff --git a/api/scanner/face_detection/face_detector.go b/api/scanner/face_detection/face_detector.go index 09e44a1c..32c133c1 100644 --- a/api/scanner/face_detection/face_detector.go +++ b/api/scanner/face_detection/face_detector.go @@ -2,11 +2,11 @@ package face_detection import ( "log" - "path/filepath" "sync" "github.com/Kagami/go-face" "github.com/photoview/photoview/api/graphql/models" + "github.com/photoview/photoview/api/utils" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -25,7 +25,7 @@ func InitializeFaceDetector(db *gorm.DB) error { log.Println("Initializing face detector") - rec, err := face.NewRecognizer(filepath.Join("data", "models")) + rec, err := face.NewRecognizer(utils.FaceRecognitionModelsPath()) if err != nil { return errors.Wrap(err, "initialize facedetect recognizer") } @@ -107,6 +107,8 @@ func (fd *FaceDetector) DetectFaces(db *gorm.DB, media *models.Media) error { return err } + log.Printf("Face thumb path: %v %v\n", thumbnailPath, fd) + fd.mutex.Lock() faces, err := fd.rec.RecognizeFile(thumbnailPath) fd.mutex.Unlock() diff --git a/api/scanner/scanner_test.go b/api/scanner/scanner_test.go index 7216599d..4ccd45d2 100644 --- a/api/scanner/scanner_test.go +++ b/api/scanner/scanner_test.go @@ -3,9 +3,11 @@ package scanner_test import ( "os" "testing" + "time" "github.com/photoview/photoview/api/graphql/models" "github.com/photoview/photoview/api/scanner" + "github.com/photoview/photoview/api/scanner/face_detection" "github.com/photoview/photoview/api/test_utils" "github.com/stretchr/testify/assert" ) @@ -42,6 +44,10 @@ func TestFullScan(t *testing.T) { return } + if !assert.NoError(t, face_detection.InitializeFaceDetector(db)) { + return + } + if !assert.NoError(t, scanner.AddUserToQueue(user)) { return } @@ -54,6 +60,32 @@ func TestFullScan(t *testing.T) { return } - assert.Equal(t, 10, len(all_media)) + assert.Equal(t, 9, len(all_media)) + + var all_media_url []*models.MediaURL + if !assert.NoError(t, db.Find(&all_media_url).Error) { + return + } + + assert.Equal(t, 18, len(all_media_url)) + + // Verify that faces was recognized + assert.Eventually(t, func() bool { + var all_face_groups []*models.FaceGroup + if !assert.NoError(t, db.Find(&all_face_groups).Error) { + return false + } + + return len(all_face_groups) == 3 + }, time.Second*5, time.Millisecond*500) + + assert.Eventually(t, func() bool { + var all_image_faces []*models.ImageFace + if !assert.NoError(t, db.Find(&all_image_faces).Error) { + return false + } + + return len(all_image_faces) == 6 + }, time.Second*5, time.Millisecond*500) } diff --git a/api/scanner/test_data/faces/girl_black_hair1.jpg b/api/scanner/test_data/faces/girl_black_hair1.jpg deleted file mode 100644 index 62fd6156..00000000 Binary files a/api/scanner/test_data/faces/girl_black_hair1.jpg and /dev/null differ diff --git a/api/test_utils/integration_setup.go b/api/test_utils/integration_setup.go index 06cd7205..eca5a16d 100644 --- a/api/test_utils/integration_setup.go +++ b/api/test_utils/integration_setup.go @@ -32,11 +32,12 @@ func UnitTestRun(m *testing.M) int { func IntegrationTestRun(m *testing.M) int { flag.Parse() + _, file, _, ok := runtime.Caller(0) + if !ok { + log.Fatal("could not get runtime file path") + } + if *integration_flags.Database { - _, file, _, ok := runtime.Caller(0) - if !ok { - log.Fatal("could not get runtime file path") - } envPath := path.Join(path.Dir(file), "..", "testing.env") @@ -45,6 +46,9 @@ func IntegrationTestRun(m *testing.M) int { } } + faceModelsPath := path.Join(path.Dir(file), "..", "data", "models") + utils.ConfigureTestFaceRecognitionModelsPath(faceModelsPath) + result := m.Run() test_dbm.Close() diff --git a/api/utils/environment_variables.go b/api/utils/environment_variables.go index 32c75d06..f0777f39 100644 --- a/api/utils/environment_variables.go +++ b/api/utils/environment_variables.go @@ -7,10 +7,11 @@ type EnvironmentVariable string // General options const ( - EnvDevelopmentMode EnvironmentVariable = "PHOTOVIEW_DEVELOPMENT_MODE" - EnvServeUI EnvironmentVariable = "PHOTOVIEW_SERVE_UI" - EnvUIPath EnvironmentVariable = "PHOTOVIEW_UI_PATH" - EnvMediaCachePath EnvironmentVariable = "PHOTOVIEW_MEDIA_CACHE" + EnvDevelopmentMode EnvironmentVariable = "PHOTOVIEW_DEVELOPMENT_MODE" + EnvServeUI EnvironmentVariable = "PHOTOVIEW_SERVE_UI" + EnvUIPath EnvironmentVariable = "PHOTOVIEW_UI_PATH" + EnvMediaCachePath EnvironmentVariable = "PHOTOVIEW_MEDIA_CACHE" + EnvFaceRecognitionModelsPath EnvironmentVariable = "PHOTOVIEW_FACE_RECOGNITION_MODELS_PATH" ) // Network related diff --git a/api/utils/utils.go b/api/utils/utils.go index 45fc3960..5ee1bfd9 100644 --- a/api/utils/utils.go +++ b/api/utils/utils.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "math/big" + "path" ) func GenerateToken() string { @@ -61,3 +62,21 @@ func MediaCachePath() string { return photoCache } + +var test_face_recognition_models_path string = "" + +func ConfigureTestFaceRecognitionModelsPath(path string) { + test_face_recognition_models_path = path +} + +func FaceRecognitionModelsPath() string { + if test_face_recognition_models_path != "" { + return test_face_recognition_models_path + } + + if EnvFaceRecognitionModelsPath.GetValue() == "" { + return path.Join("data", "models") + } + + return EnvFaceRecognitionModelsPath.GetValue() +}