Automatically create albums from folders #260
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
e79abbfee7
commit
ea6ed61d1f
12 changed files with 181 additions and 50 deletions
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ func CreateTestFixtures() {
|
|||
CreateAccountFixtures()
|
||||
CreateLinkFixtures()
|
||||
CreatePhotoAlbumFixtures()
|
||||
CreateFolderFixtures()
|
||||
CreateFileFixtures()
|
||||
CreateKeywordFixtures()
|
||||
CreatePhotoKeywordFixtures()
|
||||
|
|
|
@ -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 {
|
||||
|
|
33
internal/entity/folder_fixtures.go
Normal file
33
internal/entity/folder_fixtures.go
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue