2020-05-29 12:21:17 +02:00
|
|
|
package query
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gosimple/slug"
|
2021-11-09 11:42:10 +01:00
|
|
|
|
2021-11-24 17:51:39 +01:00
|
|
|
"github.com/photoprism/photoprism/internal/entity"
|
2020-05-29 12:21:17 +02:00
|
|
|
"github.com/photoprism/photoprism/internal/maps"
|
2021-12-15 12:24:05 +01:00
|
|
|
"github.com/photoprism/photoprism/pkg/sanitize"
|
2021-11-09 11:42:10 +01:00
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
2020-05-29 12:21:17 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
// Moment contains photo counts per month and year
|
|
|
|
type Moment struct {
|
2020-05-30 01:41:47 +02:00
|
|
|
Label string `json:"Label"`
|
2020-05-29 18:04:30 +02:00
|
|
|
Country string `json:"Country"`
|
|
|
|
State string `json:"State"`
|
|
|
|
Year int `json:"Year"`
|
|
|
|
Month int `json:"Month"`
|
|
|
|
PhotoCount int `json:"PhotoCount"`
|
2020-05-29 12:21:17 +02:00
|
|
|
}
|
|
|
|
|
2020-05-30 01:41:47 +02:00
|
|
|
var MomentLabels = map[string]string{
|
2021-02-21 17:18:36 +01:00
|
|
|
"park": "Parks & Gardens",
|
|
|
|
"botanical-garden": "Parks & Gardens",
|
|
|
|
"water-park": "Parks & Gardens",
|
|
|
|
"alpine": "Outdoor Adventures",
|
|
|
|
"hiking": "Outdoor Adventures",
|
|
|
|
"mountain": "Outdoor Adventures",
|
|
|
|
"mountains": "Outdoor Adventures",
|
|
|
|
"camping": "Outdoor Adventures",
|
|
|
|
"camper": "Outdoor Adventures",
|
|
|
|
"bench": "Outdoor Adventures",
|
|
|
|
"bunker": "Outdoor Adventures",
|
|
|
|
"castle": "Outdoor Adventures",
|
|
|
|
"viewpoint": "Outdoor Adventures",
|
2020-05-29 18:04:30 +02:00
|
|
|
"nature-reserve": "Nature & Landscape",
|
|
|
|
"landscape": "Nature & Landscape",
|
|
|
|
"nature": "Nature & Landscape",
|
2021-02-21 17:18:36 +01:00
|
|
|
"flower": "Nature & Landscape",
|
|
|
|
"field": "Nature & Landscape",
|
|
|
|
"forest": "Nature & Landscape",
|
|
|
|
"rocks": "Nature & Landscape",
|
|
|
|
"valley": "Nature & Landscape",
|
2020-05-29 18:04:30 +02:00
|
|
|
"bay": "Bays, Capes & Beaches",
|
|
|
|
"beach": "Bays, Capes & Beaches",
|
2020-05-30 21:11:56 +02:00
|
|
|
"seashore": "Bays, Capes & Beaches",
|
2020-05-29 18:04:30 +02:00
|
|
|
"cape": "Bays, Capes & Beaches",
|
2021-02-21 17:18:36 +01:00
|
|
|
"ship": "Water, Ships & Boats",
|
|
|
|
"water": "Water, Ships & Boats",
|
|
|
|
"pier": "Water, Ships & Boats",
|
|
|
|
"boat": "Water, Ships & Boats",
|
|
|
|
"boathouse": "Water, Ships & Boats",
|
|
|
|
"lakeside": "Water, Ships & Boats",
|
|
|
|
"shark": "Water, Ships & Boats",
|
|
|
|
"fish": "Water, Ships & Boats",
|
|
|
|
"jellyfish": "Water, Ships & Boats",
|
|
|
|
"submarine": "Water, Ships & Boats",
|
|
|
|
"diving": "Water, Ships & Boats",
|
|
|
|
"festival": "Festivals & Entertainment",
|
|
|
|
"nightclub": "Festivals & Entertainment",
|
|
|
|
"microphone": "Festivals & Entertainment",
|
|
|
|
"stage": "Festivals & Entertainment",
|
|
|
|
"theater": "Festivals & Entertainment",
|
|
|
|
"theme park": "Festivals & Entertainment",
|
|
|
|
"event": "Festivals & Entertainment",
|
|
|
|
"wine": "Festivals & Entertainment",
|
2020-06-05 11:09:08 +02:00
|
|
|
"cat": "Pets",
|
|
|
|
"dog": "Pets",
|
2021-02-21 17:18:36 +01:00
|
|
|
"rabbit": "Pets",
|
|
|
|
"hamster": "Pets",
|
2020-05-29 12:21:17 +02:00
|
|
|
}
|
|
|
|
|
2021-11-18 00:46:34 +01:00
|
|
|
// MomentLabelsFilter returns the smart filter string for a moment based on a matching label.
|
|
|
|
func MomentLabelsFilter(label string) string {
|
|
|
|
// TODO: Needs refactoring
|
|
|
|
label = strings.SplitN(label, txt.Or, 2)[0]
|
|
|
|
|
|
|
|
title := MomentLabels[label]
|
|
|
|
|
|
|
|
if title == "" {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
var l []string
|
|
|
|
|
|
|
|
for i := range MomentLabels {
|
|
|
|
if MomentLabels[i] == title {
|
|
|
|
l = append(l, i)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return strings.Join(txt.UniqueWords(l), txt.Or)
|
|
|
|
}
|
|
|
|
|
2021-11-18 13:13:48 +01:00
|
|
|
// CountryName returns the country name if any.
|
|
|
|
func (m Moment) CountryName() string {
|
|
|
|
if m.Country == "" {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return maps.CountryName(m.Country)
|
|
|
|
}
|
|
|
|
|
2020-05-29 12:21:17 +02:00
|
|
|
// Slug returns an identifier string for a moment.
|
2021-11-18 00:46:34 +01:00
|
|
|
func (m Moment) Slug() (s string) {
|
2021-12-15 12:24:05 +01:00
|
|
|
state := sanitize.State(m.State, m.Country)
|
2021-11-18 00:46:34 +01:00
|
|
|
|
|
|
|
if state == "" {
|
|
|
|
return m.TitleSlug()
|
|
|
|
}
|
|
|
|
|
|
|
|
country := maps.CountryName(m.Country)
|
|
|
|
|
|
|
|
if m.Year > 1900 && m.Month == 0 {
|
|
|
|
s = fmt.Sprintf("%s-%s-%04d", country, state, m.Year)
|
|
|
|
} else if m.Year > 1900 && m.Month > 0 && m.Month <= 12 {
|
|
|
|
s = fmt.Sprintf("%s-%s-%04d-%02d", country, state, m.Year, m.Month)
|
|
|
|
} else {
|
|
|
|
s = fmt.Sprintf("%s-%s", country, state)
|
|
|
|
}
|
|
|
|
|
|
|
|
return slug.Make(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TitleSlug returns an identifier string based on the title.
|
|
|
|
func (m Moment) TitleSlug() string {
|
2020-05-29 12:21:17 +02:00
|
|
|
return slug.Make(m.Title())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Title returns an english title for the moment.
|
|
|
|
func (m Moment) Title() string {
|
2021-12-15 12:24:05 +01:00
|
|
|
state := sanitize.State(m.State, m.Country)
|
2021-11-09 11:42:10 +01:00
|
|
|
|
2020-05-29 18:04:30 +02:00
|
|
|
if m.Year == 0 && m.Month == 0 {
|
2020-05-30 01:41:47 +02:00
|
|
|
if m.Label != "" {
|
2021-11-18 00:46:34 +01:00
|
|
|
return MomentLabels[strings.SplitN(m.Label, txt.Or, 2)[0]]
|
2020-05-29 12:21:17 +02:00
|
|
|
}
|
2021-11-18 13:13:48 +01:00
|
|
|
if state != "" {
|
2021-11-09 11:42:10 +01:00
|
|
|
return state
|
2020-05-29 12:21:17 +02:00
|
|
|
}
|
|
|
|
|
2021-11-18 13:13:48 +01:00
|
|
|
return m.CountryName()
|
2020-05-29 12:21:17 +02:00
|
|
|
}
|
|
|
|
|
2020-05-29 18:04:30 +02:00
|
|
|
if m.Country != "" && m.Year > 1900 && m.Month == 0 {
|
2021-11-09 11:42:10 +01:00
|
|
|
if state != "" {
|
2021-11-18 13:13:48 +01:00
|
|
|
return fmt.Sprintf("%s / %d", state, m.Year)
|
2020-05-29 12:21:17 +02:00
|
|
|
}
|
|
|
|
|
2021-11-18 13:13:48 +01:00
|
|
|
return fmt.Sprintf("%s %d", m.CountryName(), m.Year)
|
2020-05-29 12:21:17 +02:00
|
|
|
}
|
|
|
|
|
2020-05-29 18:04:30 +02:00
|
|
|
if m.Year > 1900 && m.Month > 0 && m.Month <= 12 {
|
|
|
|
date := time.Date(m.Year, time.Month(m.Month), 1, 0, 0, 0, 0, time.UTC)
|
2020-05-29 12:21:17 +02:00
|
|
|
|
2021-11-18 13:13:48 +01:00
|
|
|
if state != "" {
|
|
|
|
return fmt.Sprintf("%s / %s", state, date.Format("January 2006"))
|
|
|
|
}
|
|
|
|
|
2020-05-29 18:04:30 +02:00
|
|
|
if m.Country == "" {
|
2020-05-29 12:21:17 +02:00
|
|
|
return date.Format("January 2006")
|
|
|
|
}
|
|
|
|
|
2021-11-18 13:13:48 +01:00
|
|
|
return fmt.Sprintf("%s / %s", m.CountryName(), date.Format("January 2006"))
|
2020-05-29 12:21:17 +02:00
|
|
|
}
|
|
|
|
|
2020-05-29 18:04:30 +02:00
|
|
|
if m.Month > 0 && m.Month <= 12 {
|
|
|
|
return time.Month(m.Month).String()
|
2020-05-29 12:21:17 +02:00
|
|
|
}
|
|
|
|
|
2021-11-18 13:13:48 +01:00
|
|
|
return m.CountryName()
|
2020-05-29 12:21:17 +02:00
|
|
|
}
|
|
|
|
|
2021-08-29 19:54:50 +02:00
|
|
|
// Moments represents a list of moments.
|
2020-05-29 12:21:17 +02:00
|
|
|
type Moments []Moment
|
|
|
|
|
|
|
|
// MomentsTime counts photos by month and year.
|
|
|
|
func MomentsTime(threshold int) (results Moments, err error) {
|
|
|
|
db := UnscopedDb().Table("photos").
|
2020-05-29 18:04:30 +02:00
|
|
|
Select("photos.photo_year AS year, photos.photo_month AS month, COUNT(*) AS photo_count").
|
2020-06-05 10:59:59 +02:00
|
|
|
Where("photos.photo_quality >= 3 AND deleted_at IS NULL AND photo_private = 0 AND photos.photo_year > 0 AND photos.photo_month > 0").
|
2020-05-29 12:21:17 +02:00
|
|
|
Group("photos.photo_year, photos.photo_month").
|
|
|
|
Order("photos.photo_year DESC, photos.photo_month DESC").
|
|
|
|
Having("photo_count >= ?", threshold)
|
|
|
|
|
|
|
|
if err := db.Scan(&results).Error; err != nil {
|
|
|
|
return results, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return results, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// MomentsCountries returns the most popular countries by year.
|
|
|
|
func MomentsCountries(threshold int) (results Moments, err error) {
|
|
|
|
db := UnscopedDb().Table("photos").
|
2020-05-29 18:04:30 +02:00
|
|
|
Select("photo_country AS country, photo_year AS year, COUNT(*) AS photo_count ").
|
2020-06-05 10:59:59 +02:00
|
|
|
Where("photos.photo_quality >= 3 AND deleted_at IS NULL AND photo_private = 0 AND photo_country <> 'zz' AND photo_year > 0").
|
2020-05-29 12:21:17 +02:00
|
|
|
Group("photo_country, photo_year").
|
|
|
|
Having("photo_count >= ?", threshold)
|
|
|
|
|
|
|
|
if err := db.Scan(&results).Error; err != nil {
|
|
|
|
return results, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return results, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// MomentsStates returns the most popular states and countries by year.
|
|
|
|
func MomentsStates(threshold int) (results Moments, err error) {
|
|
|
|
db := UnscopedDb().Table("photos").
|
2020-07-12 08:27:05 +02:00
|
|
|
Select("p.place_country AS country, p.place_state AS state, COUNT(*) AS photo_count").
|
2020-05-29 12:56:24 +02:00
|
|
|
Joins("JOIN places p ON p.id = photos.place_id").
|
2020-07-12 08:27:05 +02:00
|
|
|
Where("photos.photo_quality >= 3 AND photos.deleted_at IS NULL AND photo_private = 0 AND p.place_state <> '' AND p.place_country <> 'zz'").
|
|
|
|
Group("p.place_country, p.place_state").
|
2020-05-29 12:21:17 +02:00
|
|
|
Having("photo_count >= ?", threshold)
|
|
|
|
|
|
|
|
if err := db.Scan(&results).Error; err != nil {
|
|
|
|
return results, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return results, nil
|
|
|
|
}
|
|
|
|
|
2020-05-30 01:41:47 +02:00
|
|
|
// MomentsLabels returns the most popular photo labels.
|
|
|
|
func MomentsLabels(threshold int) (results Moments, err error) {
|
2020-05-29 12:21:17 +02:00
|
|
|
var cats []string
|
|
|
|
|
2022-01-08 12:12:00 +01:00
|
|
|
for cat := range MomentLabels {
|
2020-05-29 12:21:17 +02:00
|
|
|
cats = append(cats, cat)
|
|
|
|
}
|
|
|
|
|
2021-11-18 00:46:34 +01:00
|
|
|
m := Moments{}
|
|
|
|
|
2020-05-29 12:21:17 +02:00
|
|
|
db := UnscopedDb().Table("photos").
|
2020-05-30 01:41:47 +02:00
|
|
|
Select("l.label_slug AS label, COUNT(*) AS photo_count").
|
2020-05-29 18:04:30 +02:00
|
|
|
Joins("JOIN photos_labels pl ON pl.photo_id = photos.id AND pl.uncertainty < 100").
|
|
|
|
Joins("JOIN labels l ON l.id = pl.label_id").
|
2020-06-05 10:59:59 +02:00
|
|
|
Where("photos.photo_quality >= 3 AND photos.deleted_at IS NULL AND photo_private = 0 AND l.label_slug IN (?)", cats).
|
2020-05-29 18:04:30 +02:00
|
|
|
Group("l.label_slug").
|
2020-05-29 12:21:17 +02:00
|
|
|
Having("photo_count >= ?", threshold)
|
|
|
|
|
2021-11-18 00:46:34 +01:00
|
|
|
if err := db.Scan(&m).Error; err != nil {
|
|
|
|
return m, err
|
|
|
|
}
|
|
|
|
|
|
|
|
done := make(map[string]bool)
|
|
|
|
|
|
|
|
for i := 0; i < len(m); i++ {
|
|
|
|
f := MomentLabelsFilter(m[i].Label)
|
|
|
|
|
|
|
|
if _, ok := done[f]; ok {
|
|
|
|
continue
|
|
|
|
} else {
|
|
|
|
done[f] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
m[i].Label = f
|
|
|
|
results = append(results, m[i])
|
2020-05-29 12:21:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return results, nil
|
|
|
|
}
|
2021-11-24 17:51:39 +01:00
|
|
|
|
|
|
|
// RemoveDuplicateMoments deletes generated albums with duplicate slug or filter.
|
|
|
|
func RemoveDuplicateMoments() (removed int, err error) {
|
|
|
|
if res := UnscopedDb().Exec(`DELETE FROM links WHERE share_uid
|
|
|
|
IN (SELECT a.album_uid FROM albums a JOIN albums b ON a.album_type = b.album_type
|
|
|
|
AND a.album_type <> ? AND a.id > b.id WHERE (a.album_slug = b.album_slug
|
|
|
|
OR a.album_filter = b.album_filter) GROUP BY a.album_uid)`, entity.AlbumDefault); res.Error != nil {
|
2021-11-24 17:58:08 +01:00
|
|
|
return removed, res.Error
|
2021-11-24 17:51:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if res := UnscopedDb().Exec(`DELETE FROM albums WHERE id
|
|
|
|
IN (SELECT a.id FROM albums a JOIN albums b ON a.album_type = b.album_type
|
|
|
|
AND a.album_type <> ? AND a.id > b.id WHERE (a.album_slug = b.album_slug
|
|
|
|
OR a.album_filter = b.album_filter) GROUP BY a.album_uid)`, entity.AlbumDefault); res.Error != nil {
|
2021-11-24 17:58:08 +01:00
|
|
|
return removed, res.Error
|
2021-11-24 17:51:39 +01:00
|
|
|
} else if res.RowsAffected > 0 {
|
|
|
|
removed = int(res.RowsAffected)
|
|
|
|
}
|
|
|
|
|
|
|
|
return removed, nil
|
|
|
|
}
|