Albums: Remove photo from review when adding it to an album #4229

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2024-05-09 16:00:53 +02:00
parent fa3a0d155a
commit 13255695e5
38 changed files with 371 additions and 163 deletions

View File

@@ -384,22 +384,22 @@ func CloneAlbums(router *gin.RouterGroup) {
var added []entity.PhotoAlbum var added []entity.PhotoAlbum
for _, uid := range f.Albums { for _, albumUid := range f.Albums {
cloneAlbum, err := query.AlbumByUID(uid) cloneAlbum, queryErr := query.AlbumByUID(albumUid)
if err != nil { if queryErr != nil {
log.Errorf("album: %s", err) log.Errorf("album: %s", queryErr)
continue continue
} }
photos, err := search.AlbumPhotos(cloneAlbum, 10000, false) photos, queryErr := search.AlbumPhotos(cloneAlbum, 100000, false)
if err != nil { if queryErr != nil {
log.Errorf("album: %s", err) log.Errorf("album: %s", queryErr)
continue continue
} }
added = append(added, a.AddPhotos(photos.UIDs())...) added = append(added, a.AddPhotos(photos)...)
} }
if len(added) > 0 { if len(added) > 0 {
@@ -466,7 +466,9 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
return return
} }
added := a.AddPhotos(photos.UIDs()) conf := get.Config()
added := a.AddPhotos(photos)
if len(added) > 0 { if len(added) > 0 {
if len(added) == 1 { if len(added) == 1 {
@@ -481,6 +483,34 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
// Update album YAML backup. // Update album YAML backup.
SaveAlbumAsYaml(a) SaveAlbumAsYaml(a)
// Auto-approve photos that have been added to an album,
// see https://github.com/photoprism/photoprism/issues/4229
if conf.Settings().Features.Review {
var approved entity.Photos
for _, p := range photos {
// Skip photos that are not in review.
if p.Approved() {
continue
}
// Approve photo and update YAML backup file.
if err = p.Approve(); err != nil {
log.Errorf("approve: %s", err)
} else {
approved = append(approved, p)
SavePhotoAsYaml(&p)
}
}
// Update client UI and counts if photos has been approved.
if len(approved) > 0 {
UpdateClientConfig()
event.EntitiesUpdated("photos", approved)
}
}
} }
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "message": i18n.Msg(i18n.MsgChangesSaved), "album": a, "photos": photos.UIDs(), "added": added}) c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "message": i18n.Msg(i18n.MsgChangesSaved), "album": a, "photos": photos.UIDs(), "added": added})

View File

@@ -57,10 +57,10 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
} }
for _, p := range photos { for _, p := range photos {
if err := p.Archive(); err != nil { if archiveErr := p.Archive(); archiveErr != nil {
log.Errorf("archive: %s", err) log.Errorf("archive: %s", archiveErr)
} else { } else {
SavePhotoAsYaml(p) SavePhotoAsYaml(&p)
} }
} }
} else if err := entity.Db().Where("photo_uid IN (?)", f.Photos).Delete(&entity.Photo{}).Error; err != nil { } else if err := entity.Db().Where("photo_uid IN (?)", f.Photos).Delete(&entity.Photo{}).Error; err != nil {
@@ -120,10 +120,10 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
} }
for _, p := range photos { for _, p := range photos {
if err := p.Restore(); err != nil { if err = p.Restore(); err != nil {
log.Errorf("restore: %s", err) log.Errorf("restore: %s", err)
} else { } else {
SavePhotoAsYaml(p) SavePhotoAsYaml(&p)
} }
} }
} else if err := entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uid IN (?)", f.Photos). } else if err := entity.Db().Unscoped().Model(&entity.Photo{}).Where("photo_uid IN (?)", f.Photos).
@@ -187,7 +187,7 @@ func BatchPhotosApprove(router *gin.RouterGroup) {
log.Errorf("approve: %s", err) log.Errorf("approve: %s", err)
} else { } else {
approved = append(approved, p) approved = append(approved, p)
SavePhotoAsYaml(p) SavePhotoAsYaml(&p)
} }
} }
@@ -278,7 +278,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
// Fetch selection from index. // Fetch selection from index.
if photos, err := query.SelectedPhotos(f); err == nil { if photos, err := query.SelectedPhotos(f); err == nil {
for _, p := range photos { for _, p := range photos {
SavePhotoAsYaml(p) SavePhotoAsYaml(&p)
} }
event.EntitiesUpdated("photos", photos) event.EntitiesUpdated("photos", photos)
@@ -405,12 +405,12 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
event.AuditWarn([]string{ClientIP(c), s.UserName, "delete", path.Join(p.PhotoPath, p.PhotoName+"*")}) event.AuditWarn([]string{ClientIP(c), s.UserName, "delete", path.Join(p.PhotoPath, p.PhotoName+"*")})
// Remove all related files from storage. // Remove all related files from storage.
n, err := photoprism.DeletePhoto(p, true, true) n, deleteErr := photoprism.DeletePhoto(&p, true, true)
numFiles += n numFiles += n
if err != nil { if deleteErr != nil {
log.Errorf("delete: %s", err) log.Errorf("delete: %s", deleteErr)
} else { } else {
deleted = append(deleted, p) deleted = append(deleted, p)
} }

View File

@@ -19,7 +19,12 @@ import (
) )
// SavePhotoAsYaml saves photo data as YAML file. // SavePhotoAsYaml saves photo data as YAML file.
func SavePhotoAsYaml(p entity.Photo) { func SavePhotoAsYaml(p *entity.Photo) {
if p == nil {
log.Debugf("api: photo is nil (update yaml)")
return
}
c := get.Config() c := get.Config()
// Write YAML sidecar file (optional). // Write YAML sidecar file (optional).
@@ -114,7 +119,7 @@ func UpdatePhoto(router *gin.RouterGroup) {
return return
} }
SavePhotoAsYaml(p) SavePhotoAsYaml(&p)
UpdateClientConfig() UpdateClientConfig()
@@ -225,7 +230,7 @@ func ApprovePhoto(router *gin.RouterGroup) {
return return
} }
SavePhotoAsYaml(m) SavePhotoAsYaml(&m)
PublishPhotoEvent(StatusUpdated, id, c) PublishPhotoEvent(StatusUpdated, id, c)

View File

@@ -44,7 +44,7 @@ func LikePhoto(router *gin.RouterGroup) {
return return
} }
SavePhotoAsYaml(m) SavePhotoAsYaml(&m)
PublishPhotoEvent(StatusUpdated, id, c) PublishPhotoEvent(StatusUpdated, id, c)
} }
@@ -84,7 +84,7 @@ func DislikePhoto(router *gin.RouterGroup) {
return return
} }
SavePhotoAsYaml(m) SavePhotoAsYaml(&m)
PublishPhotoEvent(StatusUpdated, id, c) PublishPhotoEvent(StatusUpdated, id, c)
} }

View File

@@ -60,7 +60,7 @@ func CreateUserPasscode(router *gin.RouterGroup) {
event.AuditErr([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.ErrPasscodeGenerateFailed.Error(), clean.Error(err)}, s.RefID) event.AuditErr([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.ErrPasscodeGenerateFailed.Error(), clean.Error(err)}, s.RefID)
Abort(c, http.StatusInternalServerError, i18n.ErrUnexpected) Abort(c, http.StatusInternalServerError, i18n.ErrUnexpected)
return return
} else if passcode, err = entity.NewPasscode(user.UID(), key.String(), rnd.RecoveryCode()); err != nil { } else if passcode, err = entity.NewPasscode(user.GetUID(), key.String(), rnd.RecoveryCode()); err != nil {
event.AuditErr([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.ErrPasscodeCreateFailed.Error(), clean.Error(err)}, s.RefID) event.AuditErr([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.ErrPasscodeCreateFailed.Error(), clean.Error(err)}, s.RefID)
Abort(c, http.StatusInternalServerError, i18n.ErrUnexpected) Abort(c, http.StatusInternalServerError, i18n.ErrUnexpected)
return return

View File

@@ -93,7 +93,7 @@ func UpdateUserPassword(router *gin.RouterGroup) {
} }
// Update tokens if user matches with session. // Update tokens if user matches with session.
if s.User().UserUID == u.UID() { if s.User().UserUID == u.GetUID() {
s.SetPreviewToken(u.PreviewToken) s.SetPreviewToken(u.PreviewToken)
s.SetDownloadToken(u.DownloadToken) s.SetDownloadToken(u.DownloadToken)
} }

View File

@@ -111,7 +111,7 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
} }
case 4: case 4:
ev = strings.Join(ch[2:4], ".") ev = strings.Join(ch[2:4], ".")
if acl.ChannelUser.Equal(ch[0]) && ch[1] == user.UID() || acl.Events.AllowAll(acl.Resource(ch[2]), user.AclRole(), wsSubscribePerms) { if acl.ChannelUser.Equal(ch[0]) && ch[1] == user.GetUID() || acl.Events.AllowAll(acl.Resource(ch[2]), user.AclRole(), wsSubscribePerms) {
// Send to matching user uid. // Send to matching user uid.
wsSendMessage(ev, msg.Fields, ws, writeMutex) wsSendMessage(ev, msg.Fields, ws, writeMutex)
} else if acl.ChannelSession.Equal(ch[0]) && ch[1] == sid { } else if acl.ChannelSession.Equal(ch[0]) && ch[1] == sid {

View File

@@ -100,7 +100,7 @@ func clientsAddAction(ctx *cli.Context) error {
} }
rows[0] = []string{ rows[0] = []string{
client.UID(), client.GetUID(),
client.Name(), client.Name(),
client.AuthInfo(), client.AuthInfo(),
client.UserInfo(), client.UserInfo(),

View File

@@ -59,7 +59,7 @@ func clientsListAction(ctx *cli.Context) error {
} }
rows[i] = []string{ rows[i] = []string{
client.UID(), client.GetUID(),
client.Name(), client.Name(),
client.AuthInfo(), client.AuthInfo(),
client.UserInfo(), client.UserInfo(),

View File

@@ -51,12 +51,12 @@ func clientsRemoveAction(ctx *cli.Context) error {
if !ctx.Bool("force") { if !ctx.Bool("force") {
actionPrompt := promptui.Prompt{ actionPrompt := promptui.Prompt{
Label: fmt.Sprintf("Delete client %s?", m.UID()), Label: fmt.Sprintf("Delete client %s?", m.GetUID()),
IsConfirm: true, IsConfirm: true,
} }
if _, err := actionPrompt.Run(); err != nil { if _, err := actionPrompt.Run(); err != nil {
log.Infof("client %s was not deleted", m.UID()) log.Infof("client %s was not deleted", m.GetUID())
return nil return nil
} }
} }
@@ -65,7 +65,7 @@ func clientsRemoveAction(ctx *cli.Context) error {
return err return err
} }
log.Infof("client %s has been deleted", m.UID()) log.Infof("client %s has been deleted", m.GetUID())
return nil return nil
}) })

View File

@@ -48,7 +48,7 @@ func usersListAction(ctx *cli.Context) error {
// Display report. // Display report.
for i, user := range users { for i, user := range users {
rows[i] = []string{ rows[i] = []string{
user.UID(), user.GetUID(),
user.Username(), user.Username(),
user.AclRole().Pretty(), user.AclRole().Pretty(),
user.AuthInfo(), user.AuthInfo(),

View File

@@ -783,19 +783,21 @@ func (m *Album) ZipName() string {
} }
// AddPhotos adds photos to an existing album. // AddPhotos adds photos to an existing album.
func (m *Album) AddPhotos(UIDs []string) (added PhotoAlbums) { func (m *Album) AddPhotos(photos PhotosInterface) (added PhotoAlbums) {
if !m.HasID() { if !m.HasID() {
return added return added
} }
// Add album entries. // Add album entries.
for _, uid := range UIDs { for _, photoUid := range photos.UIDs() {
if !rnd.IsUID(uid, PhotoUID) { if !rnd.IsUID(photoUid, PhotoUID) {
continue continue
} }
entry := PhotoAlbum{AlbumUID: m.AlbumUID, PhotoUID: uid, Hidden: false} // Add photo to album.
entry := PhotoAlbum{AlbumUID: m.AlbumUID, PhotoUID: photoUid, Hidden: false}
// Save album entry.
if err := entry.Save(); err != nil { if err := entry.Save(); err != nil {
log.Errorf("album: %s (add to album %s)", err.Error(), m) log.Errorf("album: %s (add to album %s)", err.Error(), m)
} else { } else {

View File

@@ -518,7 +518,12 @@ func TestAlbum_AddPhotos(t *testing.T) {
AlbumType: AlbumManual, AlbumType: AlbumManual,
AlbumTitle: "Test Title", AlbumTitle: "Test Title",
} }
added := album.AddPhotos([]string{"ps6sg6be2lvl0yh7", "ps6sg6be2lvl0yh8"})
photo1 := PhotoFixtures.Get("19800101_000002_D640C559")
photo2 := PhotoFixtures.Get("Photo01")
photos := Photos{photo1, photo2}
added := album.AddPhotos(photos)
var entries PhotoAlbums var entries PhotoAlbums
@@ -532,7 +537,7 @@ func TestAlbum_AddPhotos(t *testing.T) {
} }
if len(entries) < 2 { if len(entries) < 2 {
t.Error("at least one album entry expected") t.Fatal("at least one album entry expected")
} }
var a Album var a Album
@@ -542,17 +547,17 @@ func TestAlbum_AddPhotos(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
first_photo_updatedAt := strings.Split(entries[0].UpdatedAt.String(), ".")[0] firstUpdatedAt := strings.Split(entries[0].UpdatedAt.String(), ".")[0]
second_photo_updatedAt := strings.Split(entries[1].UpdatedAt.String(), ".")[0] secondUpdatedAt := strings.Split(entries[1].UpdatedAt.String(), ".")[0]
album_updatedAt := strings.Split(a.UpdatedAt.String(), ".")[0] albumUpdatedAt := strings.Split(a.UpdatedAt.String(), ".")[0]
assert.Truef( assert.Truef(
t, first_photo_updatedAt <= album_updatedAt, t, firstUpdatedAt <= albumUpdatedAt,
"Expected the UpdatedAt field of an album to be updated when"+ "Expected the UpdatedAt field of an album to be updated when"+
" new photos are added", " new photos are added",
) )
assert.Truef( assert.Truef(
t, second_photo_updatedAt <= album_updatedAt, t, secondUpdatedAt <= albumUpdatedAt,
"Expected the UpdatedAt field of an album to be updated when"+ "Expected the UpdatedAt field of an album to be updated when"+
" new photos are added", " new photos are added",
) )

View File

@@ -100,8 +100,8 @@ func FindClientByUID(uid string) *Client {
return m return m
} }
// UID returns the client uid string. // GetUID returns the client uid string.
func (m *Client) UID() string { func (m *Client) GetUID() string {
return m.ClientUID return m.ClientUID
} }
@@ -135,7 +135,7 @@ func (m *Client) String() string {
if m == nil { if m == nil {
return report.NotAssigned return report.NotAssigned
} else if m.HasUID() { } else if m.HasUID() {
return m.UID() return m.GetUID()
} else if m.HasName() { } else if m.HasName() {
return m.Name() return m.Name()
} }

View File

@@ -30,7 +30,7 @@ func TestFindClient(t *testing.T) {
} }
assert.Equal(t, m.UserUID, UserFixtures.Get("alice").UserUID) assert.Equal(t, m.UserUID, UserFixtures.Get("alice").UserUID)
assert.Equal(t, expected.ClientUID, m.UID()) assert.Equal(t, expected.ClientUID, m.GetUID())
assert.NotEmpty(t, m.CreatedAt) assert.NotEmpty(t, m.CreatedAt)
assert.NotEmpty(t, m.UpdatedAt) assert.NotEmpty(t, m.UpdatedAt)
}) })
@@ -44,7 +44,7 @@ func TestFindClient(t *testing.T) {
} }
assert.Equal(t, m.UserUID, UserFixtures.Get("bob").UserUID) assert.Equal(t, m.UserUID, UserFixtures.Get("bob").UserUID)
assert.Equal(t, expected.ClientUID, m.UID()) assert.Equal(t, expected.ClientUID, m.GetUID())
assert.NotEmpty(t, m.CreatedAt) assert.NotEmpty(t, m.CreatedAt)
assert.NotEmpty(t, m.UpdatedAt) assert.NotEmpty(t, m.UpdatedAt)
}) })
@@ -58,7 +58,7 @@ func TestFindClient(t *testing.T) {
} }
assert.Empty(t, m.UserUID) assert.Empty(t, m.UserUID)
assert.Equal(t, expected.ClientUID, m.UID()) assert.Equal(t, expected.ClientUID, m.GetUID())
assert.NotEmpty(t, m.CreatedAt) assert.NotEmpty(t, m.CreatedAt)
assert.NotEmpty(t, m.UpdatedAt) assert.NotEmpty(t, m.UpdatedAt)
}) })
@@ -411,7 +411,7 @@ func TestClient_VerifySecret(t *testing.T) {
t.Fatal("result should not be nil") t.Fatal("result should not be nil")
} }
assert.Equal(t, expected.ClientUID, m.UID()) assert.Equal(t, expected.ClientUID, m.GetUID())
assert.False(t, m.VerifySecret("xcCbOrw6I0vcoXzhnOmXhjpVSyFq0l0e")) assert.False(t, m.VerifySecret("xcCbOrw6I0vcoXzhnOmXhjpVSyFq0l0e"))
assert.False(t, m.VerifySecret("aaCbOrw6I0vcoXzhnOmXhjpVSyFq0l0e")) assert.False(t, m.VerifySecret("aaCbOrw6I0vcoXzhnOmXhjpVSyFq0l0e"))
assert.False(t, m.VerifySecret("")) assert.False(t, m.VerifySecret(""))
@@ -430,7 +430,7 @@ func TestClient_VerifySecret(t *testing.T) {
t.Fatal("result should not be nil") t.Fatal("result should not be nil")
} }
assert.Equal(t, expected.ClientUID, m.UID()) assert.Equal(t, expected.ClientUID, m.GetUID())
assert.True(t, m.VerifySecret("xcCbOrw6I0vcoXzhnOmXhjpVSyFq0l0e")) assert.True(t, m.VerifySecret("xcCbOrw6I0vcoXzhnOmXhjpVSyFq0l0e"))
assert.False(t, m.VerifySecret("aaCbOrw6I0vcoXzhnOmXhjpVSyFq0l0e")) assert.False(t, m.VerifySecret("aaCbOrw6I0vcoXzhnOmXhjpVSyFq0l0e"))
assert.False(t, m.VerifySecret("")) assert.False(t, m.VerifySecret(""))

View File

@@ -248,7 +248,7 @@ func (m *Session) SetClient(c *Client) *Session {
} }
m.client = c m.client = c
m.ClientUID = c.UID() m.ClientUID = c.GetUID()
m.ClientName = c.ClientName m.ClientName = c.ClientName
m.AuthProvider = c.Provider().String() m.AuthProvider = c.Provider().String()
m.AuthMethod = c.Method().String() m.AuthMethod = c.Method().String()

View File

@@ -75,7 +75,7 @@ func DeleteClientSessions(client *Client, authMethod authn.MethodType, limit int
q := Db() q := Db()
if client.HasUID() { if client.HasUID() {
q = q.Where("client_uid = ?", client.UID()) q = q.Where("client_uid = ?", client.GetUID())
} else if client.HasName() { } else if client.HasName() {
q = q.Where("client_name = ?", client.Name()) q = q.Where("client_name = ?", client.Name())
} else { } else {

View File

@@ -203,8 +203,8 @@ func FindUserByUID(uid string) *User {
return FindUser(User{UserUID: uid}) return FindUser(User{UserUID: uid})
} }
// UID returns the unique id as string. // GetUID returns the unique id as string.
func (m *User) UID() string { func (m *User) GetUID() string {
if m == nil { if m == nil {
return "" return ""
} }
@@ -591,7 +591,7 @@ func (m *User) Passcode(t authn.KeyType) *Passcode {
return nil return nil
} }
return FindPasscode(Passcode{UID: m.UID(), KeyType: t.String()}) return FindPasscode(Passcode{UID: m.GetUID(), KeyType: t.String()})
} }
// Username returns the user's login name as sanitized string. // Username returns the user's login name as sanitized string.
@@ -711,7 +711,7 @@ func (m *User) Settings() *UserSettings {
if m.UserSettings != nil { if m.UserSettings != nil {
m.UserSettings.UserUID = m.UserUID m.UserSettings.UserUID = m.UserUID
return m.UserSettings return m.UserSettings
} else if m.UID() == "" { } else if m.GetUID() == "" {
m.UserSettings = &UserSettings{} m.UserSettings = &UserSettings{}
return m.UserSettings return m.UserSettings
} else if err := CreateUserSettings(m); err != nil { } else if err := CreateUserSettings(m); err != nil {
@@ -726,7 +726,7 @@ func (m *User) Details() *UserDetails {
if m.UserDetails != nil { if m.UserDetails != nil {
m.UserDetails.UserUID = m.UserUID m.UserDetails.UserUID = m.UserUID
return m.UserDetails return m.UserDetails
} else if m.UID() == "" { } else if m.GetUID() == "" {
m.UserDetails = &UserDetails{} m.UserDetails = &UserDetails{}
return m.UserDetails return m.UserDetails
} else if err := CreateUserDetails(m); err != nil { } else if err := CreateUserDetails(m); err != nil {
@@ -1080,7 +1080,7 @@ func (m *User) RegenerateTokens() error {
// RefreshShares updates the list of shares. // RefreshShares updates the list of shares.
func (m *User) RefreshShares() *User { func (m *User) RefreshShares() *User {
m.UserShares = FindUserShares(m.UID()) m.UserShares = FindUserShares(m.GetUID())
return m return m
} }
@@ -1133,8 +1133,8 @@ func (m *User) RedeemToken(token string) (n int) {
// Find shares. // Find shares.
for _, link := range links { for _, link := range links {
if found := FindUserShare(UserShare{UserUID: m.UID(), ShareUID: link.ShareUID}); found == nil { if found := FindUserShare(UserShare{UserUID: m.GetUID(), ShareUID: link.ShareUID}); found == nil {
share := NewUserShare(m.UID(), link.ShareUID, link.Perm, link.ExpiresAt()) share := NewUserShare(m.GetUID(), link.ShareUID, link.Perm, link.ExpiresAt())
share.LinkUID = link.LinkUID share.LinkUID = link.LinkUID
share.Comment = link.Comment share.Comment = link.Comment

View File

@@ -69,13 +69,13 @@ func CreateUserDetails(user *User) error {
return fmt.Errorf("user is nil") return fmt.Errorf("user is nil")
} }
if user.UID() == "" { if user.GetUID() == "" {
return fmt.Errorf("empty user uid") return fmt.Errorf("empty user uid")
} }
user.UserDetails = NewUserDetails(user.UID()) user.UserDetails = NewUserDetails(user.GetUID())
if err := Db().Where("user_uid = ?", user.UID()).First(user.UserDetails).Error; err == nil { if err := Db().Where("user_uid = ?", user.GetUID()).First(user.UserDetails).Error; err == nil {
return nil return nil
} }

View File

@@ -45,13 +45,13 @@ func CreateUserSettings(user *User) error {
return fmt.Errorf("user is nil") return fmt.Errorf("user is nil")
} }
if user.UID() == "" { if user.GetUID() == "" {
return fmt.Errorf("empty user uid") return fmt.Errorf("empty user uid")
} }
user.UserSettings = &UserSettings{} user.UserSettings = &UserSettings{}
if err := Db().Where("user_uid = ?", user.UID()).First(user.UserSettings).Error; err == nil { if err := Db().Where("user_uid = ?", user.GetUID()).First(user.UserSettings).Error; err == nil {
return nil return nil
} }

View File

@@ -26,7 +26,7 @@ func TestUserShares_Contains(t *testing.T) {
func TestNewUserShare(t *testing.T) { func TestNewUserShare(t *testing.T) {
expires := TimeStamp().Add(time.Hour * 48) expires := TimeStamp().Add(time.Hour * 48)
m := NewUserShare(Admin.UID(), AlbumFixtures.Get("berlin-2019").AlbumUID, PermReact, &expires) m := NewUserShare(Admin.GetUID(), AlbumFixtures.Get("berlin-2019").AlbumUID, PermReact, &expires)
assert.True(t, m.HasID()) assert.True(t, m.HasID())
assert.True(t, rnd.IsRefID(m.RefID)) assert.True(t, rnd.IsRefID(m.RefID))
@@ -76,7 +76,7 @@ func TestFindUserShare(t *testing.T) {
func TestFindUserShares(t *testing.T) { func TestFindUserShares(t *testing.T) {
t.Run("Alice", func(t *testing.T) { t.Run("Alice", func(t *testing.T) {
found := FindUserShares(UserFixtures.Pointer("alice").UID()) found := FindUserShares(UserFixtures.Pointer("alice").GetUID())
assert.NotNil(t, found) assert.NotNil(t, found)
assert.Len(t, found, 1) assert.Len(t, found, 1)

View File

@@ -6,7 +6,7 @@ import (
) )
// Map is an alias for map[string]interface{}. // Map is an alias for map[string]interface{}.
type Map map[string]interface{} type Map = map[string]interface{}
// ModelValues extracts Values from an entity model. // ModelValues extracts Values from an entity model.
func ModelValues(m interface{}, omit ...string) (result Map, omitted []interface{}, err error) { func ModelValues(m interface{}, omit ...string) (result Map, omitted []interface{}, err error) {

View File

@@ -32,19 +32,6 @@ var MetadataEstimateInterval = 24 * 7 * time.Hour // 7 Days
var photoMutex = sync.Mutex{} var photoMutex = sync.Mutex{}
type Photos []Photo
// UIDs returns a slice of photo UIDs.
func (m Photos) UIDs() []string {
result := make([]string, len(m))
for i, el := range m {
result[i] = el.PhotoUID
}
return result
}
// MapKey returns a key referencing time and location for indexing. // MapKey returns a key referencing time and location for indexing.
func MapKey(takenAt time.Time, cellId string) string { func MapKey(takenAt time.Time, cellId string) string {
return path.Join(strconv.FormatInt(takenAt.Unix(), 36), cellId) return path.Join(strconv.FormatInt(takenAt.Unix(), 36), cellId)
@@ -222,6 +209,21 @@ func SavePhotoForm(model Photo, form form.Photo) error {
return nil return nil
} }
// GetID returns the numeric entity ID.
func (m *Photo) GetID() uint {
return m.ID
}
// HasID checks if the photo has an id and uid assigned to it.
func (m *Photo) HasID() bool {
return m.ID > 0 && m.PhotoUID != ""
}
// GetUID returns the unique entity id.
func (m *Photo) GetUID() string {
return m.PhotoUID
}
// String returns the id or name as string. // String returns the id or name as string.
func (m *Photo) String() string { func (m *Photo) String() string {
if m.PhotoName != "" { if m.PhotoName != "" {
@@ -527,11 +529,6 @@ func (m *Photo) PreloadMany() {
m.PreloadAlbums() m.PreloadAlbums()
} }
// HasID checks if the photo has an id and uid assigned to it.
func (m *Photo) HasID() bool {
return m.ID > 0 && m.PhotoUID != ""
}
// NoCameraSerial checks if the photo has no CameraSerial // NoCameraSerial checks if the photo has no CameraSerial
func (m *Photo) NoCameraSerial() bool { func (m *Photo) NoCameraSerial() bool {
return m.CameraSerial == "" return m.CameraSerial == ""
@@ -731,11 +728,17 @@ func (m *Photo) AllFiles() (files Files) {
// Archive removes the photo from albums and flags it as archived (soft delete). // Archive removes the photo from albums and flags it as archived (soft delete).
func (m *Photo) Archive() error { func (m *Photo) Archive() error {
if !m.HasID() {
return fmt.Errorf("photo has no id")
} else if m.DeletedAt != nil {
return nil
}
deletedAt := TimeStamp() deletedAt := TimeStamp()
if err := Db().Model(&PhotoAlbum{}).Where("photo_uid = ?", m.PhotoUID).UpdateColumn("hidden", true).Error; err != nil { if err := Db().Model(&PhotoAlbum{}).Where("photo_uid = ?", m.PhotoUID).UpdateColumn("hidden", true).Error; err != nil {
return err return err
} else if err := m.Update("deleted_at", deletedAt); err != nil { } else if err = m.Update("deleted_at", deletedAt); err != nil {
return err return err
} }
@@ -744,8 +747,14 @@ func (m *Photo) Archive() error {
return nil return nil
} }
// Restore removes the archive flag (undo soft delete). // Restore removes the photo from the archive (reverses soft delete).
func (m *Photo) Restore() error { func (m *Photo) Restore() error {
if !m.HasID() {
return fmt.Errorf("photo has no id")
} else if m.DeletedAt == nil {
return nil
}
if err := m.Update("deleted_at", gorm.Expr("NULL")); err != nil { if err := m.Update("deleted_at", gorm.Expr("NULL")); err != nil {
return err return err
} }
@@ -757,7 +766,7 @@ func (m *Photo) Restore() error {
// Delete deletes the photo from the index. // Delete deletes the photo from the index.
func (m *Photo) Delete(permanently bool) (files Files, err error) { func (m *Photo) Delete(permanently bool) (files Files, err error) {
if m.ID < 1 || m.PhotoUID == "" { if !m.HasID() {
return files, fmt.Errorf("invalid photo id %d / uid %s", m.ID, clean.Log(m.PhotoUID)) return files, fmt.Errorf("invalid photo id %d / uid %s", m.ID, clean.Log(m.PhotoUID))
} }
@@ -834,7 +843,7 @@ func (m *Photo) React(user *User, reaction react.Emoji) error {
return m.UnReact(user) return m.UnReact(user)
} }
return NewReaction(m.PhotoUID, user.UID()).React(reaction).Save() return NewReaction(m.PhotoUID, user.GetUID()).React(reaction).Save()
} }
// UnReact deletes a previous user reaction, if any. // UnReact deletes a previous user reaction, if any.
@@ -843,7 +852,7 @@ func (m *Photo) UnReact(user *User) error {
return fmt.Errorf("unknown user") return fmt.Errorf("unknown user")
} }
if r := FindReaction(m.PhotoUID, user.UID()); r != nil { if r := FindReaction(m.PhotoUID, user.GetUID()); r != nil {
return r.Delete() return r.Delete()
} }
@@ -884,13 +893,31 @@ func (m *Photo) SetStack(stack int8) {
} }
} }
// Approve approves a photo in review. // Approved checks if the photo is not in review.
func (m *Photo) Approved() bool {
if !m.HasID() {
return false
} else if m.PhotoQuality >= 3 || m.PhotoType != MediaImage || m.EditedAt != nil {
return true
}
return false
}
// Approve approves the photo if it is in review.
func (m *Photo) Approve() error { func (m *Photo) Approve() error {
if m.PhotoQuality >= 3 { if !m.HasID() {
return fmt.Errorf("photo has no id")
} else if m.PhotoQuality >= 3 {
// Nothing to do. // Nothing to do.
return nil return nil
} }
// Restore photo if archived.
if err := m.Restore(); err != nil {
return err
}
edited := TimeStamp() edited := TimeStamp()
m.EditedAt = &edited m.EditedAt = &edited
m.PhotoQuality = m.QualityScore() m.PhotoQuality = m.QualityScore()

View File

@@ -132,7 +132,7 @@ func (m *Photo) EstimateLocation(force bool) {
m.RemoveLocationLabels() m.RemoveLocationLabels()
m.EstimateCountry() m.EstimateCountry()
} else if len(mostRecent) == 1 || m.UnknownCamera() { } else if len(mostRecent) == 1 || m.UnknownCamera() {
m.AdoptPlace(recentPhoto, SrcEstimate, false) m.AdoptPlace(&recentPhoto, SrcEstimate, false)
} else { } else {
p1 := mostRecent[0] p1 := mostRecent[0]
p2 := mostRecent[1] p2 := mostRecent[1]
@@ -143,7 +143,7 @@ func (m *Photo) EstimateLocation(force bool) {
if estimate := movement.EstimatePosition(m.TakenAt); movement.Km() < 100 && estimate.Accuracy < Accuracy1Km { if estimate := movement.EstimatePosition(m.TakenAt); movement.Km() < 100 && estimate.Accuracy < Accuracy1Km {
m.SetPosition(estimate, SrcEstimate, false) m.SetPosition(estimate, SrcEstimate, false)
} else { } else {
m.AdoptPlace(recentPhoto, SrcEstimate, false) m.AdoptPlace(&recentPhoto, SrcEstimate, false)
} }
} }
} else if recentPhoto.HasCountry() { } else if recentPhoto.HasCountry() {

View File

@@ -0,0 +1,16 @@
package entity
// PhotoInterface represents an abstract Photo entity interface.
type PhotoInterface interface {
GetID() uint
HasID() bool
GetUID() string
Approve() error
Restore() error
}
// PhotosInterface represents a Photo slice provider interface.
type PhotosInterface interface {
UIDs() []string
Photos() []PhotoInterface
}

View File

@@ -86,8 +86,10 @@ func (m *Photo) SetPosition(pos geo.Position, source string, force bool) {
} }
// AdoptPlace sets the place based on another photo. // AdoptPlace sets the place based on another photo.
func (m *Photo) AdoptPlace(other Photo, source string, force bool) { func (m *Photo) AdoptPlace(other *Photo, source string, force bool) {
if SrcPriority[m.PlaceSrc] > SrcPriority[source] && !force { if other == nil {
return
} else if SrcPriority[m.PlaceSrc] > SrcPriority[source] && !force {
return return
} else if other.Place == nil { } else if other.Place == nil {
return return

View File

@@ -32,7 +32,7 @@ func TestPhoto_AdoptPlace(t *testing.T) {
place := PlaceFixtures.Get("mexico") place := PlaceFixtures.Get("mexico")
t.Run("SrcAuto", func(t *testing.T) { t.Run("SrcAuto", func(t *testing.T) {
p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: -1, PhotoLng: 1, PlaceSrc: SrcAuto} p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: -1, PhotoLng: 1, PlaceSrc: SrcAuto}
o := Photo{ID: 1, Place: &place, PlaceID: place.ID, CellID: "s2:479a03fda18c", PhotoLat: 15, PhotoLng: -11, PlaceSrc: SrcManual} o := &Photo{ID: 1, Place: &place, PlaceID: place.ID, CellID: "s2:479a03fda18c", PhotoLat: 15, PhotoLng: -11, PlaceSrc: SrcManual}
assert.Nil(t, p.Place) assert.Nil(t, p.Place)
assert.Equal(t, "", p.PlaceID) assert.Equal(t, "", p.PlaceID)
assert.Equal(t, "s2:479a03fda123", p.CellID) assert.Equal(t, "s2:479a03fda123", p.CellID)
@@ -47,7 +47,7 @@ func TestPhoto_AdoptPlace(t *testing.T) {
}) })
t.Run("SrcManual", func(t *testing.T) { t.Run("SrcManual", func(t *testing.T) {
p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: 0, PhotoLng: 0, PlaceSrc: SrcManual} p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: 0, PhotoLng: 0, PlaceSrc: SrcManual}
o := Photo{ID: 1, Place: &place, PlaceID: place.ID, CellID: "s2:479a03fda18c", PhotoLat: 1, PhotoLng: -1, PlaceSrc: SrcManual} o := &Photo{ID: 1, Place: &place, PlaceID: place.ID, CellID: "s2:479a03fda18c", PhotoLat: 1, PhotoLng: -1, PlaceSrc: SrcManual}
assert.Nil(t, p.Place) assert.Nil(t, p.Place)
assert.Equal(t, "", p.PlaceID) assert.Equal(t, "", p.PlaceID)
assert.Equal(t, "s2:479a03fda123", p.CellID) assert.Equal(t, "s2:479a03fda123", p.CellID)
@@ -62,7 +62,7 @@ func TestPhoto_AdoptPlace(t *testing.T) {
}) })
t.Run("Force", func(t *testing.T) { t.Run("Force", func(t *testing.T) {
p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: 1, PhotoLng: -1, PlaceSrc: SrcManual} p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: 1, PhotoLng: -1, PlaceSrc: SrcManual}
o := Photo{ID: 1, Place: &place, PlaceID: place.ID, CellID: "s2:479a03fda18c", PhotoLat: 0, PhotoLng: 0, PlaceSrc: SrcManual} o := &Photo{ID: 1, Place: &place, PlaceID: place.ID, CellID: "s2:479a03fda18c", PhotoLat: 0, PhotoLng: 0, PlaceSrc: SrcManual}
assert.Nil(t, p.Place) assert.Nil(t, p.Place)
assert.Equal(t, "", p.PlaceID) assert.Equal(t, "", p.PlaceID)
assert.Equal(t, "s2:479a03fda123", p.CellID) assert.Equal(t, "s2:479a03fda123", p.CellID)

View File

@@ -808,8 +808,8 @@ func TestPhoto_AllFiles(t *testing.T) {
} }
func TestPhoto_Archive(t *testing.T) { func TestPhoto_Archive(t *testing.T) {
t.Run("archive not yet archived photo", func(t *testing.T) { t.Run("NotYetArchived", func(t *testing.T) {
m := &Photo{PhotoTitle: "HappyLilly"} m := &Photo{ID: 10000, PhotoUID: "csd7ybn092yzcp52", PhotoTitle: "HappyLilly"}
assert.Empty(t, m.DeletedAt) assert.Empty(t, m.DeletedAt)
err := m.Archive() err := m.Archive()
if err != nil { if err != nil {

26
internal/entity/photos.go Normal file
View File

@@ -0,0 +1,26 @@
package entity
// Photos represents a list of photos.
type Photos []Photo
// Photos returns the result as a slice of Photo.
func (m Photos) Photos() []PhotoInterface {
result := make([]PhotoInterface, len(m))
for i := range m {
result[i] = &m[i]
}
return result
}
// UIDs returns tbe photo UIDs as string slice.
func (m Photos) UIDs() []string {
result := make([]string, len(m))
for i, photo := range m {
result[i] = photo.GetUID()
}
return result
}

View File

@@ -85,9 +85,9 @@ func (w *CleanUp) Start(opt CleanUpOptions) (thumbs int, orphans int, sidecars i
} }
// Deletes the index entry and remaining sidecar files outside the "originals" folder. // Deletes the index entry and remaining sidecar files outside the "originals" folder.
if n, err := DeletePhoto(p, true, false); err != nil { if n, deleteErr := DeletePhoto(&p, true, false); deleteErr != nil {
sidecars += n sidecars += n
log.Errorf("cleanup: %s (remove orphans)", err) log.Errorf("cleanup: %s (remove orphans)", deleteErr)
} else { } else {
orphans++ orphans++
sidecars += n sidecars += n

View File

@@ -1,6 +1,7 @@
package photoprism package photoprism
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
@@ -10,7 +11,11 @@ import (
) )
// DeletePhoto removes a photo from the index and optionally all related media files. // DeletePhoto removes a photo from the index and optionally all related media files.
func DeletePhoto(p entity.Photo, mediaFiles bool, originals bool) (numFiles int, err error) { func DeletePhoto(p *entity.Photo, mediaFiles bool, originals bool) (numFiles int, err error) {
if p == nil {
return 0, errors.New("photo is nil")
}
yamlFileName := p.YamlFileName(Config().OriginalsPath(), Config().SidecarPath()) yamlFileName := p.YamlFileName(Config().OriginalsPath(), Config().SidecarPath())
// Permanently remove photo from index. // Permanently remove photo from index.

View File

@@ -19,7 +19,7 @@ type ImportOptions struct {
// SetUser sets the user who performs the import operation. // SetUser sets the user who performs the import operation.
func (o *ImportOptions) SetUser(user *entity.User) *ImportOptions { func (o *ImportOptions) SetUser(user *entity.User) *ImportOptions {
if o != nil && user != nil { if o != nil && user != nil {
o.UID = user.UID() o.UID = user.GetUID()
} }
return o return o
@@ -28,7 +28,7 @@ func (o *ImportOptions) SetUser(user *entity.User) *ImportOptions {
// ImportOptionsCopy returns import options for copying files to originals (read-only). // ImportOptionsCopy returns import options for copying files to originals (read-only).
func ImportOptionsCopy(importPath, destFolder string) ImportOptions { func ImportOptionsCopy(importPath, destFolder string) ImportOptions {
result := ImportOptions{ result := ImportOptions{
UID: entity.Admin.UID(), UID: entity.Admin.GetUID(),
Action: ActionImport, Action: ActionImport,
Path: importPath, Path: importPath,
Move: false, Move: false,
@@ -45,7 +45,7 @@ func ImportOptionsCopy(importPath, destFolder string) ImportOptions {
// ImportOptionsMove returns import options for moving files to originals (modifies import directory). // ImportOptionsMove returns import options for moving files to originals (modifies import directory).
func ImportOptionsMove(importPath, destFolder string) ImportOptions { func ImportOptionsMove(importPath, destFolder string) ImportOptions {
result := ImportOptions{ result := ImportOptions{
UID: entity.Admin.UID(), UID: entity.Admin.GetUID(),
Action: ActionImport, Action: ActionImport,
Path: importPath, Path: importPath,
Move: true, Move: true,
@@ -62,7 +62,7 @@ func ImportOptionsMove(importPath, destFolder string) ImportOptions {
// ImportOptionsUpload returns options for importing user uploads. // ImportOptionsUpload returns options for importing user uploads.
func ImportOptionsUpload(uploadPath, destFolder string) ImportOptions { func ImportOptionsUpload(uploadPath, destFolder string) ImportOptions {
result := ImportOptions{ result := ImportOptions{
UID: entity.Admin.UID(), UID: entity.Admin.GetUID(),
Action: ActionUpload, Action: ActionUpload,
Path: uploadPath, Path: uploadPath,
Move: true, Move: true,

View File

@@ -72,7 +72,7 @@ func TestIndex_MediaFile(t *testing.T) {
} }
assert.Equal(t, "", mediaFile.metaData.Title) assert.Equal(t, "", mediaFile.metaData.Title)
result := ind.UserMediaFile(mediaFile, indexOpt, "blue-go-video.mp4", "", entity.Admin.UID()) result := ind.UserMediaFile(mediaFile, indexOpt, "blue-go-video.mp4", "", entity.Admin.GetUID())
assert.Equal(t, "Blue Gopher", mediaFile.metaData.Title) assert.Equal(t, "Blue Gopher", mediaFile.metaData.Title)
assert.Equal(t, IndexStatus("added"), result.Status) assert.Equal(t, IndexStatus("added"), result.Status)

View File

@@ -19,7 +19,7 @@ type IndexOptions struct {
// NewIndexOptions returns new index options instance. // NewIndexOptions returns new index options instance.
func NewIndexOptions(path string, rescan, convert, stack, facesOnly, skipArchived bool) IndexOptions { func NewIndexOptions(path string, rescan, convert, stack, facesOnly, skipArchived bool) IndexOptions {
result := IndexOptions{ result := IndexOptions{
UID: entity.Admin.UID(), UID: entity.Admin.GetUID(),
Action: ActionIndex, Action: ActionIndex,
Path: path, Path: path,
Rescan: rescan, Rescan: rescan,
@@ -42,7 +42,7 @@ func (o *IndexOptions) SkipUnchanged() bool {
// SetUser sets the user who performs the index operation. // SetUser sets the user who performs the index operation.
func (o *IndexOptions) SetUser(user *entity.User) *IndexOptions { func (o *IndexOptions) SetUser(user *entity.User) *IndexOptions {
if o != nil && user != nil { if o != nil && user != nil {
o.UID = user.UID() o.UID = user.GetUID()
} }
return o return o

View File

@@ -5,9 +5,11 @@ import (
"time" "time"
"github.com/gosimple/slug" "github.com/gosimple/slug"
"github.com/jinzhu/gorm"
"github.com/ulule/deepcopier" "github.com/ulule/deepcopier"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/txt" "github.com/photoprism/photoprism/pkg/txt"
) )
@@ -98,14 +100,91 @@ type Photo struct {
UpdatedAt time.Time `json:"UpdatedAt" select:"photos.updated_at"` UpdatedAt time.Time `json:"UpdatedAt" select:"photos.updated_at"`
EditedAt time.Time `json:"EditedAt,omitempty" select:"photos.edited_at"` EditedAt time.Time `json:"EditedAt,omitempty" select:"photos.edited_at"`
CheckedAt time.Time `json:"CheckedAt,omitempty" select:"photos.checked_at"` CheckedAt time.Time `json:"CheckedAt,omitempty" select:"photos.checked_at"`
DeletedAt time.Time `json:"DeletedAt,omitempty" select:"photos.deleted_at"` DeletedAt *time.Time `json:"DeletedAt,omitempty" select:"photos.deleted_at"`
Files []entity.File `json:"Files"` Files []entity.File `json:"Files"`
} }
// GetID returns the numeric entity ID.
func (m *Photo) GetID() uint {
return m.ID
}
// HasID checks if the photo has an id and uid assigned to it.
func (m *Photo) HasID() bool {
return m.ID > 0 && m.PhotoUID != ""
}
// GetUID returns the unique entity id.
func (m *Photo) GetUID() string {
return m.PhotoUID
}
// Approve approves the photo if it is in review.
func (m *Photo) Approve() error {
if !m.HasID() {
return fmt.Errorf("photo has no id")
} else if m.PhotoQuality >= 3 {
// Nothing to do.
return nil
}
// Restore photo if archived.
if err := m.Restore(); err != nil {
return err
}
edited := entity.TimeStamp()
if err := UnscopedDb().
Table(entity.Photo{}.TableName()).
Where("photo_uid = ?", m.GetUID()).
UpdateColumns(entity.Map{
"deleted_at": gorm.Expr("NULL"),
"edited_at": &edited,
"photo_quality": 3}).Error; err != nil {
return err
}
m.EditedAt = edited
m.PhotoQuality = 3
m.DeletedAt = nil
// Update precalculated photo and file counts.
if err := entity.UpdateCounts(); err != nil {
log.Warnf("index: %s (update counts)", err)
}
event.Publish("count.review", event.Data{
"count": -1,
})
return nil
}
// Restore removes the photo from the archive (reverses soft delete).
func (m *Photo) Restore() error {
if !m.HasID() {
return fmt.Errorf("photo has no id")
} else if m.DeletedAt == nil {
return nil
}
if err := UnscopedDb().
Table(entity.Photo{}.TableName()).
Where("photo_uid = ?", m.GetUID()).
UpdateColumn("deleted_at", gorm.Expr("NULL")).Error; err != nil {
return err
}
m.DeletedAt = nil
return nil
}
// IsPlayable returns true if the photo has a related video/animation that is playable. // IsPlayable returns true if the photo has a related video/animation that is playable.
func (photo *Photo) IsPlayable() bool { func (m *Photo) IsPlayable() bool {
switch photo.PhotoType { switch m.PhotoType {
case entity.MediaVideo, entity.MediaLive, entity.MediaAnimated: case entity.MediaVideo, entity.MediaLive, entity.MediaAnimated:
return true return true
default: default:
@@ -114,31 +193,42 @@ func (photo *Photo) IsPlayable() bool {
} }
// ShareBase returns a meaningful file name for sharing. // ShareBase returns a meaningful file name for sharing.
func (photo *Photo) ShareBase(seq int) string { func (m *Photo) ShareBase(seq int) string {
var name string var name string
if photo.PhotoTitle != "" { if m.PhotoTitle != "" {
name = txt.Title(slug.MakeLang(photo.PhotoTitle, "en")) name = txt.Title(slug.MakeLang(m.PhotoTitle, "en"))
} else { } else {
name = photo.PhotoUID name = m.PhotoUID
} }
taken := photo.TakenAtLocal.Format("20060102-150405") taken := m.TakenAtLocal.Format("20060102-150405")
if seq > 0 { if seq > 0 {
return fmt.Sprintf("%s-%s (%d).%s", taken, name, seq, photo.FileType) return fmt.Sprintf("%s-%s (%d).%s", taken, name, seq, m.FileType)
} }
return fmt.Sprintf("%s-%s.%s", taken, name, photo.FileType) return fmt.Sprintf("%s-%s.%s", taken, name, m.FileType)
} }
type PhotoResults []Photo type PhotoResults []Photo
// UIDs returns a slice of photo UIDs. // Photos returns the result as a slice of Photo.
func (photos PhotoResults) UIDs() []string { func (m PhotoResults) Photos() []entity.PhotoInterface {
result := make([]string, len(photos)) result := make([]entity.PhotoInterface, len(m))
for i, el := range photos { for i := range m {
result[i] = &m[i]
}
return result
}
// UIDs returns a slice of photo UIDs.
func (m PhotoResults) UIDs() []string {
result := make([]string, len(m))
for i, el := range m {
result[i] = el.PhotoUID result[i] = el.PhotoUID
} }
@@ -146,14 +236,14 @@ func (photos PhotoResults) UIDs() []string {
} }
// Merge consecutive file results that belong to the same photo. // Merge consecutive file results that belong to the same photo.
func (photos PhotoResults) Merge() (merged PhotoResults, count int, err error) { func (m PhotoResults) Merge() (merged PhotoResults, count int, err error) {
count = len(photos) count = len(m)
merged = make(PhotoResults, 0, count) merged = make(PhotoResults, 0, count)
var i int var i int
var photoId uint var photoId uint
for _, photo := range photos { for _, photo := range m {
file := entity.File{} file := entity.File{}
if err = deepcopier.Copy(&file).From(photo); err != nil { if err = deepcopier.Copy(&file).From(photo); err != nil {

View File

@@ -12,7 +12,7 @@ func TestPhotosResults_Merged(t *testing.T) {
ID: 111111, ID: 111111,
CreatedAt: time.Time{}, CreatedAt: time.Time{},
UpdatedAt: time.Time{}, UpdatedAt: time.Time{},
DeletedAt: time.Time{}, DeletedAt: &time.Time{},
TakenAt: time.Time{}, TakenAt: time.Time{},
TakenAtLocal: time.Time{}, TakenAtLocal: time.Time{},
TakenSrc: "", TakenSrc: "",
@@ -71,7 +71,7 @@ func TestPhotosResults_Merged(t *testing.T) {
ID: 22222, ID: 22222,
CreatedAt: time.Time{}, CreatedAt: time.Time{},
UpdatedAt: time.Time{}, UpdatedAt: time.Time{},
DeletedAt: time.Time{}, DeletedAt: &time.Time{},
TakenAt: time.Time{}, TakenAt: time.Time{},
TakenAtLocal: time.Time{}, TakenAtLocal: time.Time{},
TakenSrc: "", TakenSrc: "",
@@ -141,7 +141,7 @@ func TestPhotosResults_UIDs(t *testing.T) {
ID: 111111, ID: 111111,
CreatedAt: time.Time{}, CreatedAt: time.Time{},
UpdatedAt: time.Time{}, UpdatedAt: time.Time{},
DeletedAt: time.Time{}, DeletedAt: &time.Time{},
TakenAt: time.Time{}, TakenAt: time.Time{},
TakenAtLocal: time.Time{}, TakenAtLocal: time.Time{},
TakenSrc: "", TakenSrc: "",
@@ -200,7 +200,7 @@ func TestPhotosResults_UIDs(t *testing.T) {
ID: 22222, ID: 22222,
CreatedAt: time.Time{}, CreatedAt: time.Time{},
UpdatedAt: time.Time{}, UpdatedAt: time.Time{},
DeletedAt: time.Time{}, DeletedAt: &time.Time{},
TakenAt: time.Time{}, TakenAt: time.Time{},
TakenAtLocal: time.Time{}, TakenAtLocal: time.Time{},
TakenSrc: "", TakenSrc: "",
@@ -267,7 +267,7 @@ func TestPhotosResult_ShareFileName(t *testing.T) {
ID: 111111, ID: 111111,
CreatedAt: time.Time{}, CreatedAt: time.Time{},
UpdatedAt: time.Time{}, UpdatedAt: time.Time{},
DeletedAt: time.Time{}, DeletedAt: &time.Time{},
TakenAt: time.Date(2015, 11, 11, 9, 7, 18, 0, time.UTC), TakenAt: time.Date(2015, 11, 11, 9, 7, 18, 0, time.UTC),
TakenAtLocal: time.Date(2013, 11, 11, 9, 7, 18, 0, time.UTC), TakenAtLocal: time.Date(2013, 11, 11, 9, 7, 18, 0, time.UTC),
TakenSrc: "", TakenSrc: "",
@@ -330,7 +330,7 @@ func TestPhotosResult_ShareFileName(t *testing.T) {
ID: 111111, ID: 111111,
CreatedAt: time.Time{}, CreatedAt: time.Time{},
UpdatedAt: time.Time{}, UpdatedAt: time.Time{},
DeletedAt: time.Time{}, DeletedAt: &time.Time{},
TakenAt: time.Date(2013, 11, 11, 9, 7, 18, 0, time.UTC), TakenAt: time.Date(2013, 11, 11, 9, 7, 18, 0, time.UTC),
TakenAtLocal: time.Date(2015, 11, 11, 9, 7, 18, 0, time.UTC), TakenAtLocal: time.Date(2015, 11, 11, 9, 7, 18, 0, time.UTC),
TakenSrc: "", TakenSrc: "",
@@ -394,7 +394,7 @@ func TestPhotosResult_ShareFileName(t *testing.T) {
ID: 111111, ID: 111111,
CreatedAt: time.Time{}, CreatedAt: time.Time{},
UpdatedAt: time.Time{}, UpdatedAt: time.Time{},
DeletedAt: time.Time{}, DeletedAt: &time.Time{},
TakenAt: time.Date(2022, 11, 11, 9, 7, 18, 0, time.UTC), TakenAt: time.Date(2022, 11, 11, 9, 7, 18, 0, time.UTC),
TakenAtLocal: time.Date(2022, 11, 11, 9, 7, 18, 0, time.UTC), TakenAtLocal: time.Date(2022, 11, 11, 9, 7, 18, 0, time.UTC),
TakenSrc: "", TakenSrc: "",

View File

@@ -25,40 +25,40 @@ func UserPhotosViewerResults(f form.SearchPhotos, sess *entity.Session, contentU
} }
// ViewerResult returns a new photo viewer result. // ViewerResult returns a new photo viewer result.
func (photo Photo) ViewerResult(contentUri, apiUri, previewToken, downloadToken string) viewer.Result { func (m Photo) ViewerResult(contentUri, apiUri, previewToken, downloadToken string) viewer.Result {
return viewer.Result{ return viewer.Result{
UID: photo.PhotoUID, UID: m.PhotoUID,
Title: photo.PhotoTitle, Title: m.PhotoTitle,
TakenAtLocal: photo.TakenAtLocal, TakenAtLocal: m.TakenAtLocal,
Description: photo.PhotoDescription, Description: m.PhotoDescription,
Favorite: photo.PhotoFavorite, Favorite: m.PhotoFavorite,
Playable: photo.IsPlayable(), Playable: m.IsPlayable(),
DownloadUrl: viewer.DownloadUrl(photo.FileHash, apiUri, downloadToken), DownloadUrl: viewer.DownloadUrl(m.FileHash, apiUri, downloadToken),
Width: photo.FileWidth, Width: m.FileWidth,
Height: photo.FileHeight, Height: m.FileHeight,
Thumbs: thumb.Public{ Thumbs: thumb.Public{
Fit720: thumb.New(photo.FileWidth, photo.FileHeight, photo.FileHash, thumb.Sizes[thumb.Fit720], contentUri, previewToken), Fit720: thumb.New(m.FileWidth, m.FileHeight, m.FileHash, thumb.Sizes[thumb.Fit720], contentUri, previewToken),
Fit1280: thumb.New(photo.FileWidth, photo.FileHeight, photo.FileHash, thumb.Sizes[thumb.Fit1280], contentUri, previewToken), Fit1280: thumb.New(m.FileWidth, m.FileHeight, m.FileHash, thumb.Sizes[thumb.Fit1280], contentUri, previewToken),
Fit1920: thumb.New(photo.FileWidth, photo.FileHeight, photo.FileHash, thumb.Sizes[thumb.Fit1920], contentUri, previewToken), Fit1920: thumb.New(m.FileWidth, m.FileHeight, m.FileHash, thumb.Sizes[thumb.Fit1920], contentUri, previewToken),
Fit2048: thumb.New(photo.FileWidth, photo.FileHeight, photo.FileHash, thumb.Sizes[thumb.Fit2048], contentUri, previewToken), Fit2048: thumb.New(m.FileWidth, m.FileHeight, m.FileHash, thumb.Sizes[thumb.Fit2048], contentUri, previewToken),
Fit2560: thumb.New(photo.FileWidth, photo.FileHeight, photo.FileHash, thumb.Sizes[thumb.Fit2560], contentUri, previewToken), Fit2560: thumb.New(m.FileWidth, m.FileHeight, m.FileHash, thumb.Sizes[thumb.Fit2560], contentUri, previewToken),
Fit3840: thumb.New(photo.FileWidth, photo.FileHeight, photo.FileHash, thumb.Sizes[thumb.Fit3840], contentUri, previewToken), Fit3840: thumb.New(m.FileWidth, m.FileHeight, m.FileHash, thumb.Sizes[thumb.Fit3840], contentUri, previewToken),
Fit4096: thumb.New(photo.FileWidth, photo.FileHeight, photo.FileHash, thumb.Sizes[thumb.Fit4096], contentUri, previewToken), Fit4096: thumb.New(m.FileWidth, m.FileHeight, m.FileHash, thumb.Sizes[thumb.Fit4096], contentUri, previewToken),
Fit7680: thumb.New(photo.FileWidth, photo.FileHeight, photo.FileHash, thumb.Sizes[thumb.Fit7680], contentUri, previewToken), Fit7680: thumb.New(m.FileWidth, m.FileHeight, m.FileHash, thumb.Sizes[thumb.Fit7680], contentUri, previewToken),
}, },
} }
} }
// ViewerJSON returns the results as photo viewer JSON. // ViewerJSON returns the results as photo viewer JSON.
func (photos PhotoResults) ViewerJSON(contentUri, apiUri, previewToken, downloadToken string) ([]byte, error) { func (m PhotoResults) ViewerJSON(contentUri, apiUri, previewToken, downloadToken string) ([]byte, error) {
return json.Marshal(photos.ViewerResults(contentUri, apiUri, previewToken, downloadToken)) return json.Marshal(m.ViewerResults(contentUri, apiUri, previewToken, downloadToken))
} }
// ViewerResults returns the results photo viewer formatted. // ViewerResults returns the results photo viewer formatted.
func (photos PhotoResults) ViewerResults(contentUri, apiUri, previewToken, downloadToken string) (results viewer.Results) { func (m PhotoResults) ViewerResults(contentUri, apiUri, previewToken, downloadToken string) (results viewer.Results) {
results = make(viewer.Results, 0, len(photos)) results = make(viewer.Results, 0, len(m))
for _, p := range photos { for _, p := range m {
results = append(results, p.ViewerResult(contentUri, apiUri, previewToken, downloadToken)) results = append(results, p.ViewerResult(contentUri, apiUri, previewToken, downloadToken))
} }

View File

@@ -12,7 +12,7 @@ func TestPhotoResults_ViewerJSON(t *testing.T) {
ID: 111111, ID: 111111,
CreatedAt: time.Time{}, CreatedAt: time.Time{},
UpdatedAt: time.Time{}, UpdatedAt: time.Time{},
DeletedAt: time.Time{}, DeletedAt: &time.Time{},
TakenAt: time.Time{}, TakenAt: time.Time{},
TakenAtLocal: time.Time{}, TakenAtLocal: time.Time{},
TakenSrc: "", TakenSrc: "",
@@ -71,7 +71,7 @@ func TestPhotoResults_ViewerJSON(t *testing.T) {
ID: 22222, ID: 22222,
CreatedAt: time.Time{}, CreatedAt: time.Time{},
UpdatedAt: time.Time{}, UpdatedAt: time.Time{},
DeletedAt: time.Time{}, DeletedAt: &time.Time{},
TakenAt: time.Time{}, TakenAt: time.Time{},
TakenAtLocal: time.Time{}, TakenAtLocal: time.Time{},
TakenSrc: "", TakenSrc: "",