Automatically create albums from folders #260

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-05-30 15:42:04 +02:00
parent e79abbfee7
commit ea6ed61d1f
12 changed files with 181 additions and 50 deletions

View file

@ -50,7 +50,7 @@
v-show="searchExpanded">
<v-card-text>
<v-layout row wrap>
<v-flex xs12 pa-2>
<!-- v-flex xs12 pa-2>
<v-text-field flat solo hide-details
browser-autocomplete="off"
:label="labels.search"
@ -61,17 +61,24 @@
v-model="filter.q"
@keyup.enter.native="filterChange"
></v-text-field>
</v-flex>
</v-flex -->
<v-flex xs12 sm6 md3 pa-2 class="p-countries-select">
<v-select @change="dropdownChange"
:label="labels.country"
flat solo hide-details
color="secondary-dark"
item-value="ID"
item-text="Name"
v-model="filter.country"
:items="options.countries">
</v-select>
<v-autocomplete
v-model="album.Category"
browser-autocomplete="off"
hint="Category"
:items="items"
:search-input.sync="search"
:loading="loading"
hide-details
hide-no-data
item-text="Title"
item-value="UID"
:label="labels.category"
color="secondary-dark"
flat solo
>
</v-autocomplete>
</v-flex>
<v-flex xs12 sm6 md3 pa-2 class="p-camera-select">
<v-select @change="dropdownChange"
@ -121,6 +128,8 @@
</v-form>
</template>
<script>
import Album from "../model/album";
export default {
name: 'p-album-toolbar',
props: {
@ -141,6 +150,9 @@
}].concat(this.$config.get('countries'));
return {
items: [],
search: null,
loading: false,
searchExpanded: false,
options: {
'views': [
@ -167,11 +179,30 @@
country: this.$gettext("Country"),
camera: this.$gettext("Camera"),
sort: this.$gettext("Sort By"),
category: this.$gettext("Category"),
},
titleRule: v => v.length <= this.$config.get('clip') || this.$gettext("Name too long"),
growDesc: false,
};
},
watch: {
search (q) {
const exists = this.albums.findIndex((album) => album.Title === q);
if (exists !== -1 || !q) {
this.items = this.albums;
this.newAlbum = null;
} else {
this.newAlbum = new Album({Title: q, UID: "", Favorite: true});
this.items = this.albums.concat([this.newAlbum]);
}
},
show: function (show) {
if (show) {
this.queryServer("");
}
}
},
methods: {
expand() {
this.searchExpanded = !this.searchExpanded;

View file

@ -167,15 +167,6 @@
class="p-navigation-count">{{ config.count.months }}</span></v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile v-for="(album, index) in config.albums"
:key="index"
:to="{ name: 'album', params: { uid: album.UID, slug: album.Slug } }">
<v-list-tile-content>
<v-list-tile-title v-if="album.Title">{{ album.Title }}</v-list-tile-title>
<v-list-tile-title v-else>Untitled</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list-group>
<v-list-tile :to="{ name: 'moments' }" @click="" class="p-navigation-moments"
@ -386,13 +377,6 @@
auth() {
return this.session.auth || this.public
},
albumExpandIcon() {
if (this.config.count.albums > 0) {
return this.$vuetify.icons.expand
}
return ""
},
},
methods: {
feature(name) {

View file

@ -42,7 +42,7 @@ export default [
},
{
name: "moment",
path: "/moment/:uid",
path: "/moments/:uid",
component: AlbumPhotos,
meta: {title: "Moments", auth: true},
},
@ -68,7 +68,7 @@ export default [
},
{
name: "month",
path: "/month/:uid",
path: "/months/:uid",
component: AlbumPhotos,
meta: {title: "Months", auth: true},
},
@ -81,7 +81,7 @@ export default [
},
{
name: "folder",
path: "/folder/:uid",
path: "/folders/:uid",
component: AlbumPhotos,
meta: {title: "Folders", auth: true},
},

View file

@ -70,8 +70,30 @@ func NewAlbum(albumTitle, albumType string) *Album {
return result
}
// NewMoment creates a new moment.
func NewMoment(albumTitle, albumSlug, albumFilter string) *Album {
// NewFolderAlbum creates a new folder album.
func NewFolderAlbum(albumTitle, albumSlug, albumFilter string) *Album {
if albumTitle == "" || albumSlug == "" || albumFilter == "" {
return nil
}
now := time.Now().UTC()
result := &Album{
AlbumUID: rnd.PPID('a'),
AlbumOrder: SortOrderOldest,
AlbumType: TypeFolder,
AlbumTitle: albumTitle,
AlbumSlug: albumSlug,
AlbumFilter: albumFilter,
CreatedAt: now,
UpdatedAt: now,
}
return result
}
// NewMomentsAlbum creates a new moment.
func NewMomentsAlbum(albumTitle, albumSlug, albumFilter string) *Album {
if albumTitle == "" || albumSlug == "" || albumFilter == "" {
return nil
}
@ -92,8 +114,8 @@ func NewMoment(albumTitle, albumSlug, albumFilter string) *Album {
return result
}
// NewMonth creates a new month album.
func NewMonth(albumTitle, albumSlug string, year, month int) *Album {
// NewMonthAlbum creates a new month album.
func NewMonthAlbum(albumTitle, albumSlug string, year, month int) *Album {
if albumTitle == "" || albumSlug == "" || year == 0 || month == 0 {
return nil
}
@ -186,10 +208,10 @@ func (m *Album) Create() error {
}
// FindAlbum finds a matching album or returns nil.
func FindAlbum(slug string) *Album {
func FindAlbum(slug, albumType string) *Album {
result := Album{}
if err := Db().Where("album_slug = ?", slug).First(&result).Error; err != nil {
if err := Db().Where("album_slug = ? AND album_type = ?", slug, albumType).First(&result).Error; err != nil {
return nil
}

View file

@ -10,6 +10,7 @@ func CreateTestFixtures() {
CreateAccountFixtures()
CreateLinkFixtures()
CreatePhotoAlbumFixtures()
CreateFolderFixtures()
CreateFileFixtures()
CreateKeywordFixtures()
CreatePhotoKeywordFixtures()

View file

@ -6,6 +6,7 @@ import (
"strings"
"time"
"github.com/gosimple/slug"
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
@ -120,6 +121,16 @@ func (m *Folder) SetValuesFromPath() {
}
}
// Slug returns a slug based on the folder title.
func (m *Folder) Slug() string {
return slug.Make(m.FolderTitle)
}
// Title returns a human readable folder title.
func (m *Folder) Title() string {
return m.FolderTitle
}
// Saves the complete entity in the database.
func (m *Folder) Create() error {
if err := Db().Create(m).Error; err != nil {

View file

@ -0,0 +1,33 @@
package entity
import (
"time"
)
var FolderFixtures = map[string]Folder{
"1990": {
Path: "1990",
FolderYear: 1990,
FolderMonth: 0,
FolderCountry: "zz",
CreatedAt: time.Date(2020, 3, 6, 2, 6, 51, 0, time.UTC),
UpdatedAt: time.Date(2020, 3, 28, 14, 6, 0, 0, time.UTC),
DeletedAt: nil,
},
"1990/04": {
Path: "1990/04",
FolderYear: 1990,
FolderMonth: 4,
FolderCountry: "zz",
CreatedAt: time.Date(2020, 3, 6, 2, 6, 51, 0, time.UTC),
UpdatedAt: time.Date(2020, 3, 28, 14, 6, 0, 0, time.UTC),
DeletedAt: nil,
},
}
// CreateFolderFixtures inserts known entities into the database for testing.
func CreateFolderFixtures() {
for _, entity := range FolderFixtures {
Db().Create(&entity)
}
}

View file

@ -786,7 +786,7 @@ var PhotoFixtures = PhotoMap{
PhotoTitle: "TitleToBeSet",
TitleSrc: "location",
PhotoDescription: "photo description blacklist",
PhotoPath: "",
PhotoPath: "1990",
PhotoName: "Photo15",
PhotoQuality: 0,
PhotoResolution: 0,
@ -835,7 +835,7 @@ var PhotoFixtures = PhotoMap{
TakenSrc: "",
PhotoTitle: "ForDeletion",
TitleSrc: "",
PhotoPath: "",
PhotoPath: "1990",
PhotoName: "Photo16",
PhotoQuality: 0,
PhotoResolution: 0,
@ -884,7 +884,7 @@ var PhotoFixtures = PhotoMap{
TakenSrc: "",
PhotoTitle: "Quality1FavoriteTrue",
TitleSrc: "",
PhotoPath: "",
PhotoPath: "1990/04",
PhotoName: "Photo17",
PhotoQuality: 0,
PhotoResolution: 0,
@ -935,7 +935,7 @@ var PhotoFixtures = PhotoMap{
TakenSrc: "",
PhotoTitle: "ArchivedChroma0",
TitleSrc: "",
PhotoPath: "",
PhotoPath: "1990/04",
PhotoName: "Photo18",
PhotoQuality: 0,
PhotoResolution: 0,

View file

@ -31,10 +31,10 @@ func (m *Photo) EstimatePosition() {
log.Errorf("photo: %s", err.Error())
} else {
if days := recentPhoto.TakenAt.Sub(m.TakenAt) / (time.Hour * 24); days < -7 {
log.Debugf("prism: can't estimate position of %s, time difference too big (%d days)", m.PhotoUID, -1*days)
log.Debugf("prism: can't estimate position of %s, %d days time difference", m.PhotoUID, -1*days)
return
} else if days > -7 {
log.Debugf("prism: can't estimate position of %s, time difference too big (%d days)", m.PhotoUID, days)
log.Debugf("prism: can't estimate position of %s, %d days time difference", m.PhotoUID, days)
return
}

View file

@ -56,14 +56,35 @@ func (m *Moments) Start() (err error) {
log.Infof("moments: index contains %d photos and videos, threshold %d", indexSize, threshold)
// Important folders.
if results, err := query.AlbumFolders(threshold); err != nil {
log.Errorf("moments: %s", err.Error())
} else {
for _, mom := range results {
f := form.PhotoSearch{
Path: mom.Path,
}
if a := entity.FindAlbum(mom.Slug(), entity.TypeFolder); a != nil {
log.Infof("moments: %s already exists (%s)", txt.Quote(mom.Title()), f.Serialize())
} else if a := entity.NewFolderAlbum(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
if err := a.Create(); err != nil {
log.Errorf("moments: %s", err)
} else {
log.Infof("moments: added %s (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
}
}
}
}
// Important years and months.
if results, err := query.MomentsTime(threshold); err != nil {
log.Errorf("moments: %s", err.Error())
} else {
for _, mom := range results {
if a := entity.FindAlbum(mom.Slug()); a != nil {
if a := entity.FindAlbum(mom.Slug(), entity.TypeMonth); a != nil {
log.Infof("moments: %s already exists (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
} else if a := entity.NewMonth(mom.Title(), mom.Slug(), mom.Year, mom.Month); a != nil {
} else if a := entity.NewMonthAlbum(mom.Title(), mom.Slug(), mom.Year, mom.Month); a != nil {
if err := a.Create(); err != nil {
log.Errorf("moments: %s", err)
} else {
@ -83,9 +104,9 @@ func (m *Moments) Start() (err error) {
Year: mom.Year,
}
if a := entity.FindAlbum(mom.Slug()); a != nil {
if a := entity.FindAlbum(mom.Slug(), entity.TypeMoment); a != nil {
log.Infof("moments: %s already exists (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
} else if a := entity.NewMoment(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
} else if a := entity.NewMomentsAlbum(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
a.AlbumYear = mom.Year
a.AlbumCountry = mom.Country
@ -108,9 +129,9 @@ func (m *Moments) Start() (err error) {
State: mom.State,
}
if a := entity.FindAlbum(mom.Slug()); a != nil {
if a := entity.FindAlbum(mom.Slug(), entity.TypeMoment); a != nil {
log.Infof("moments: %s already exists (%s)", txt.Quote(a.AlbumTitle), a.AlbumFilter)
} else if a := entity.NewMoment(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
} else if a := entity.NewMomentsAlbum(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
a.AlbumCountry = mom.Country
if err := a.Create(); err != nil {
@ -131,7 +152,7 @@ func (m *Moments) Start() (err error) {
Label: mom.Label,
}
if a := entity.FindAlbum(mom.Slug()); a != nil {
if a := entity.FindAlbum(mom.Slug(), entity.TypeMoment); a != nil {
log.Infof("moments: %s already exists (%s)", txt.Quote(mom.Title()), f.Serialize())
if err := form.ParseQueryString(&f); err != nil {
@ -147,7 +168,7 @@ func (m *Moments) Start() (err error) {
} else {
log.Infof("moments: updated %s (%s)", txt.Quote(a.AlbumTitle), f.Serialize())
}
} else if a := entity.NewMoment(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
} else if a := entity.NewMomentsAlbum(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
if err := a.Create(); err != nil {
log.Errorf("moments: %s", err.Error())
} else {

View file

@ -37,3 +37,18 @@ func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders Fol
return folders, nil
}
// AlbumFolders returns folders that should be added as album.
func AlbumFolders(threshold int) (folders Folders, err error) {
db := UnscopedDb().LogMode(true).Table("folders").
Select("folders.*, COUNT(photos.id) AS photo_count").
Joins("JOIN photos ON photos.photo_path = folders.path AND photos.deleted_at IS NULL AND photos.photo_quality >= 3").
Group("folders.path").
Having("photo_count >= ?", threshold)
if err := db.Scan(&folders).Error; err != nil {
return folders, err
}
return folders, nil
}

View file

@ -32,3 +32,16 @@ func TestFoldersByPath(t *testing.T) {
assert.Len(t, folders, 2)
})
}
func TestAlbumFolders(t *testing.T) {
t.Run("root", func(t *testing.T) {
folders, err := AlbumFolders(1)
if err != nil {
t.Fatal(err)
}
assert.Len(t, folders, 1)
t.Logf("folders: %+v", folders)
})
}