Indexing: Add "convert to jpeg" and "create thumbnails" options
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
e207c83242
commit
27ca260942
|
@ -18,7 +18,8 @@ class Config {
|
|||
title: "PhotoPrism",
|
||||
};
|
||||
|
||||
this.subscriptionId = Event.subscribe("config.updated", (ev, data) => this.setValues(data));
|
||||
Event.subscribe("config.updated", (ev, data) => this.setValues(data));
|
||||
Event.subscribe("count", (ev, data) => this.onCount(ev, data));
|
||||
|
||||
if(this.hasValue("settings")) {
|
||||
this.setTheme(this.getValue("settings").theme);
|
||||
|
@ -43,6 +44,32 @@ class Config {
|
|||
return this;
|
||||
}
|
||||
|
||||
onCount(ev, data) {
|
||||
const type = ev.split('.')[1];
|
||||
|
||||
switch (type) {
|
||||
case "favorites":
|
||||
this.values.count.favorites += data.count;
|
||||
break;
|
||||
case "albums":
|
||||
this.values.count.albums += data.count;
|
||||
break;
|
||||
case "photos":
|
||||
this.values.count.photos += data.count;
|
||||
break;
|
||||
case "countries":
|
||||
this.values.count.countries += data.count;
|
||||
break;
|
||||
case "labels":
|
||||
this.values.count.labels += data.count;
|
||||
break;
|
||||
default:
|
||||
console.warn("unknown count type", ev, data)
|
||||
}
|
||||
|
||||
this.values.count
|
||||
}
|
||||
|
||||
updateSettings(values, $vuetify) {
|
||||
this.setValue("settings", values);
|
||||
this.setTheme(values.theme);
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate>Photos</translate>
|
||||
<span v-if="config.count.photos > 0" class="p-navigation-count">{{ config.count.photos }}</span>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
@ -97,6 +98,7 @@
|
|||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate>Albums</translate>
|
||||
<span v-if="config.count.albums > 0" class="p-navigation-count">{{ config.count.albums }}</span>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
@ -119,18 +121,7 @@
|
|||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate>Favorites</translate>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile to="/places" @click="" class="p-navigation-places">
|
||||
<v-list-tile-action>
|
||||
<v-icon>place</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate>Places</translate>
|
||||
<span v-if="config.count.favorites > 0" class="p-navigation-count">{{ config.count.favorites }}</span>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
@ -143,6 +134,20 @@
|
|||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate>Labels</translate>
|
||||
<span v-if="config.count.labels > 0" class="p-navigation-count">{{ config.count.labels }}</span>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile to="/places" @click="" class="p-navigation-places">
|
||||
<v-list-tile-action>
|
||||
<v-icon>place</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>
|
||||
<translate>Places</translate>
|
||||
<span v-if="config.count.countries > 0" class="p-navigation-count">{{ config.count.countries }}</span>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
@ -223,7 +228,7 @@
|
|||
export default {
|
||||
name: "p-navigation",
|
||||
data() {
|
||||
let mini = (window.innerWidth < 1600);
|
||||
let mini = (window.innerWidth < 1400);
|
||||
|
||||
return {
|
||||
drawer: null,
|
||||
|
|
|
@ -39,6 +39,15 @@ main {
|
|||
z-index: 10;
|
||||
}
|
||||
|
||||
#p-navigation .p-navigation-count {
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
left: 80px;
|
||||
width: 40px;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#photoprism div.loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
@ -128,6 +137,10 @@ main {
|
|||
color: #F48FB1;
|
||||
}
|
||||
|
||||
#photoprism .p-log-warning {
|
||||
color: #FFD600;
|
||||
}
|
||||
|
||||
#photoprism .p-log-fatal {
|
||||
color: #FFECB3;
|
||||
}
|
||||
|
@ -137,5 +150,5 @@ main {
|
|||
}
|
||||
|
||||
#photoprism .p-log-debug {
|
||||
color: #EEEEEE;
|
||||
color: #DDDDDD;
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
slider-color="secondary-dark"
|
||||
height="64"
|
||||
>
|
||||
<v-tab id="tab-maintenance" ripple @click="changePath('/library')">
|
||||
<v-tab id="tab-originals" ripple @click="changePath('/library')">
|
||||
<translate>Originals</translate>
|
||||
</v-tab>
|
||||
<v-tab-item>
|
||||
<p-tab-maintenance></p-tab-maintenance>
|
||||
<p-tab-originals></p-tab-originals>
|
||||
</v-tab-item>
|
||||
|
||||
<v-tab id="tab-import" v-if="!readonly" ripple @click="changePath('/library/import')">
|
||||
|
@ -35,7 +35,7 @@
|
|||
<script>
|
||||
import uploadTab from "pages/library/upload.vue";
|
||||
import importTab from "pages/library/import.vue";
|
||||
import maintenanceTab from "pages/library/maintenance.vue";
|
||||
import originalsTab from "pages/library/originals.vue";
|
||||
|
||||
export default {
|
||||
name: 'p-page-library',
|
||||
|
@ -43,7 +43,7 @@
|
|||
tab: Number
|
||||
},
|
||||
components: {
|
||||
'p-tab-maintenance': maintenanceTab,
|
||||
'p-tab-originals': originalsTab,
|
||||
'p-tab-import': importTab,
|
||||
'p-tab-upload': uploadTab,
|
||||
},
|
||||
|
|
|
@ -3,21 +3,39 @@
|
|||
<v-form ref="form" class="p-photo-index" lazy-validation @submit.prevent="submit" dense>
|
||||
<v-container fluid>
|
||||
<p class="subheading">
|
||||
<span v-if="fileName">Indexing {{ fileName }}...</span>
|
||||
<span v-if="fileName">{{ action }} {{ fileName }}...</span>
|
||||
<span v-else-if="busy">Indexing photos and sidecar files...</span>
|
||||
<span v-else-if="completed">Done.</span>
|
||||
<span v-else>Press button to start indexing...</span>
|
||||
</p>
|
||||
|
||||
<p class="options">
|
||||
<v-progress-linear color="secondary-dark" :value="completed" :indeterminate="busy"></v-progress-linear>
|
||||
<v-progress-linear color="secondary-dark" :value="completed"
|
||||
:indeterminate="busy"></v-progress-linear>
|
||||
</p>
|
||||
|
||||
<v-checkbox
|
||||
v-model="options.skip"
|
||||
class="mb-0 mt-4 pa-0"
|
||||
v-model="options.skipUnchanged"
|
||||
color="secondary-dark"
|
||||
:disabled="busy"
|
||||
:label="labels.skip"
|
||||
:label="labels.skipUnchanged"
|
||||
></v-checkbox>
|
||||
<v-checkbox
|
||||
v-if="!readonly"
|
||||
class="ma-0 pa-0"
|
||||
v-model="options.convertRaw"
|
||||
color="secondary-dark"
|
||||
:disabled="busy"
|
||||
:label="labels.convertRaw"
|
||||
></v-checkbox>
|
||||
<v-checkbox
|
||||
v-if="!readonly"
|
||||
class="ma-0 pa-0"
|
||||
v-model="options.createThumbs"
|
||||
color="secondary-dark"
|
||||
:disabled="busy"
|
||||
:label="labels.createThumbs"
|
||||
></v-checkbox>
|
||||
|
||||
<v-btn
|
||||
|
@ -27,7 +45,7 @@
|
|||
depressed
|
||||
@click.stop="startIndexing()"
|
||||
>
|
||||
<translate>Start</translate>
|
||||
<translate>Index</translate>
|
||||
<v-icon right dark>update</v-icon>
|
||||
</v-btn>
|
||||
</v-container>
|
||||
|
@ -45,17 +63,23 @@
|
|||
name: 'p-tab-index',
|
||||
data() {
|
||||
return {
|
||||
readonly: this.$config.getValue("readonly"),
|
||||
started: false,
|
||||
busy: false,
|
||||
completed: 0,
|
||||
subscriptionId: '',
|
||||
fileName: '',
|
||||
subscriptionId: "",
|
||||
action: "",
|
||||
fileName: "",
|
||||
source: null,
|
||||
options: {
|
||||
skip: true
|
||||
skipUnchanged: true,
|
||||
createThumbs: false,
|
||||
convertRaw: false,
|
||||
},
|
||||
labels: {
|
||||
skip: this.$gettext("Skip unchanged photos and sidecar files"),
|
||||
skipUnchanged: this.$gettext("Skip unchanged files"),
|
||||
createThumbs: this.$gettext("Pre-render thumbnails"),
|
||||
convertRaw: this.$gettext("Convert RAW to JPEG"),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -73,7 +97,7 @@
|
|||
const ctx = this;
|
||||
Notify.blockUI();
|
||||
|
||||
Api.post('index', this.options, { cancelToken: this.source.token }).then(function () {
|
||||
Api.post('index', this.options, {cancelToken: this.source.token}).then(function () {
|
||||
Notify.unblockUI();
|
||||
ctx.busy = false;
|
||||
ctx.completed = 100;
|
||||
|
@ -94,7 +118,7 @@
|
|||
});
|
||||
},
|
||||
handleEvent(ev, data) {
|
||||
if(this.source) {
|
||||
if (this.source) {
|
||||
this.source.cancel('run in background');
|
||||
this.source = null;
|
||||
Notify.unblockUI();
|
||||
|
@ -103,12 +127,26 @@
|
|||
const type = ev.split('.')[1];
|
||||
|
||||
switch (type) {
|
||||
case 'file':
|
||||
case "indexing":
|
||||
this.action = "Indexing";
|
||||
this.busy = true;
|
||||
this.completed = 0;
|
||||
this.fileName = data.fileName;
|
||||
break;
|
||||
case "converting":
|
||||
this.action = "Converting";
|
||||
this.busy = true;
|
||||
this.completed = 0;
|
||||
this.fileName = data.fileName;
|
||||
break;
|
||||
case "thumbnails":
|
||||
this.action = "Creating thumbnails for";
|
||||
this.busy = true;
|
||||
this.completed = 0;
|
||||
this.fileName = data.fileName;
|
||||
break;
|
||||
case 'completed':
|
||||
this.action = "";
|
||||
this.busy = false;
|
||||
this.completed = 100;
|
||||
this.fileName = '';
|
|
@ -86,6 +86,10 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
event.Publish("count.albums", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
|
||||
event.Success(fmt.Sprintf("album \"%s\" created", m.AlbumName))
|
||||
|
||||
c.JSON(http.StatusOK, m)
|
||||
|
|
|
@ -46,6 +46,10 @@ func BatchPhotosDelete(router *gin.RouterGroup, conf *config.Config) {
|
|||
|
||||
elapsed := int(time.Since(start).Seconds())
|
||||
|
||||
event.Publish("count.photos", event.Data{
|
||||
"count": len(f.Photos) * -1,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("photos deleted in %d s", elapsed)})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -47,9 +47,20 @@ func Index(router *gin.RouterGroup, conf *config.Config) {
|
|||
|
||||
event.Info(fmt.Sprintf("indexing photos in \"%s\"", filepath.Base(path)))
|
||||
|
||||
if f.ConvertRaw {
|
||||
converter := photoprism.NewConverter(conf)
|
||||
converter.ConvertAll(conf.OriginalsPath())
|
||||
}
|
||||
|
||||
if f.CreateThumbs {
|
||||
if err := photoprism.CreateThumbnailsFromOriginals(conf.OriginalsPath(), conf.ThumbnailsPath(), false); err != nil {
|
||||
event.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
initIndexer(conf)
|
||||
|
||||
if f.SkipExisting {
|
||||
if f.SkipUnchanged {
|
||||
indexer.IndexOriginals(photoprism.IndexerOptionsNone())
|
||||
} else {
|
||||
indexer.IndexOriginals(photoprism.IndexerOptionsAll())
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -110,6 +111,10 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
|||
m.PhotoFavorite = true
|
||||
conf.Db().Save(&m)
|
||||
|
||||
event.Publish("count.favorites", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||
})
|
||||
}
|
||||
|
@ -136,6 +141,10 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
|||
m.PhotoFavorite = false
|
||||
conf.Db().Save(&m)
|
||||
|
||||
event.Publish("count.favorites", event.Data{
|
||||
"count": -1,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ func wsReader(ws *websocket.Conn) {
|
|||
|
||||
func wsWriter(ws *websocket.Conn, conf *config.Config) {
|
||||
pingTicker := time.NewTicker(10 * time.Second)
|
||||
s := event.Subscribe("log.*", "notify.*", "index.*", "upload.*", "import.*", "config.*")
|
||||
s := event.Subscribe("log.*", "notify.*", "index.*", "upload.*", "import.*", "config.*", "count.*")
|
||||
|
||||
defer func() {
|
||||
pingTicker.Stop()
|
||||
|
|
|
@ -525,11 +525,46 @@ func (c *Config) ClientConfig() ClientConfig {
|
|||
}
|
||||
|
||||
var countries []country
|
||||
var count = struct {
|
||||
Photos uint `json:"photos"`
|
||||
Favorites uint `json:"favorites"`
|
||||
Private uint `json:"private"`
|
||||
Stories uint `json:"stories"`
|
||||
Labels uint `json:"labels"`
|
||||
Albums uint `json:"albums"`
|
||||
Countries uint `json:"countries"`
|
||||
}{}
|
||||
|
||||
db.Model(&models.Location{}).Select("DISTINCT loc_country_code, loc_country").Scan(&countries)
|
||||
db.Table("photos").
|
||||
Select("COUNT(*) AS photos, SUM(photo_favorite) AS favorites, SUM(photo_private) AS private, SUM(photo_story) AS stories").
|
||||
Where("deleted_at IS NULL").
|
||||
Take(&count)
|
||||
|
||||
db.Where("deleted_at IS NULL").Limit(1000).Order("camera_model").Find(&cameras)
|
||||
db.Where("deleted_at IS NULL AND album_favorite = 1").Limit(20).Order("album_name").Find(&albums)
|
||||
db.Table("labels").
|
||||
Select("COUNT(*) AS labels").
|
||||
Where("deleted_at IS NULL").
|
||||
Take(&count)
|
||||
|
||||
db.Table("albums").
|
||||
Select("COUNT(*) AS albums").
|
||||
Where("deleted_at IS NULL").
|
||||
Take(&count)
|
||||
|
||||
db.Table("countries").
|
||||
Select("COUNT(*) AS countries").
|
||||
Take(&count)
|
||||
|
||||
db.Model(&models.Location{}).
|
||||
Select("DISTINCT loc_country_code, loc_country").
|
||||
Scan(&countries)
|
||||
|
||||
db.Where("deleted_at IS NULL").
|
||||
Limit(1000).Order("camera_model").
|
||||
Find(&cameras)
|
||||
|
||||
db.Where("deleted_at IS NULL AND album_favorite = 1").
|
||||
Limit(20).Order("album_name").
|
||||
Find(&albums)
|
||||
|
||||
jsHash := util.Hash(c.HttpStaticBuildPath() + "/app.js")
|
||||
cssHash := util.Hash(c.HttpStaticBuildPath() + "/app.css")
|
||||
|
@ -548,6 +583,7 @@ func (c *Config) ClientConfig() ClientConfig {
|
|||
"jsHash": jsHash,
|
||||
"cssHash": cssHash,
|
||||
"settings": c.Settings(),
|
||||
"count": count,
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package form
|
||||
|
||||
type IndexerOptions struct {
|
||||
SkipExisting bool `json:"skip"`
|
||||
SkipUnchanged bool `json:"skipUnchanged"`
|
||||
CreateThumbs bool `json:"createThumbs"`
|
||||
ConvertRaw bool `json:"convertRaw"`
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ type Country struct {
|
|||
CountryNotes string `gorm:"type:text;"`
|
||||
CountryPhoto *Photo
|
||||
CountryPhotoID uint
|
||||
New bool `gorm:"-"`
|
||||
}
|
||||
|
||||
// Create a new country
|
||||
|
@ -41,3 +42,7 @@ func (m *Country) FirstOrCreate(db *gorm.DB) *Country {
|
|||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Country) AfterCreate(scope *gorm.Scope) error {
|
||||
return scope.SetColumn("New", true)
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ type Label struct {
|
|||
LabelDescription string `gorm:"type:text;"`
|
||||
LabelNotes string `gorm:"type:text;"`
|
||||
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id"`
|
||||
New bool `gorm:"-"`
|
||||
}
|
||||
|
||||
func NewLabel(labelName string, labelPriority int) *Label {
|
||||
|
@ -42,3 +43,7 @@ func (m *Label) FirstOrCreate(db *gorm.DB) *Label {
|
|||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Label) AfterCreate(scope *gorm.Scope) error {
|
||||
return scope.SetColumn("New", true)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
)
|
||||
|
||||
// Converter wraps a darktable cli binary.
|
||||
|
@ -98,12 +99,21 @@ func (c *Converter) ConvertToJpeg(image *MediaFile) (*MediaFile, error) {
|
|||
|
||||
log.Infof("converting \"%s\" to \"%s\"", image.filename, jpegFilename)
|
||||
|
||||
fileName := image.RelativeFilename(c.conf.OriginalsPath())
|
||||
|
||||
xmpFilename := baseFilename + ".xmp"
|
||||
|
||||
if _, err := os.Stat(xmpFilename); err != nil {
|
||||
xmpFilename = ""
|
||||
}
|
||||
|
||||
event.Publish("index.converting", event.Data{
|
||||
"fileType": image.Type(),
|
||||
"fileName": fileName,
|
||||
"baseName": filepath.Base(fileName),
|
||||
"xmpName": filepath.Base(xmpFilename),
|
||||
})
|
||||
|
||||
if convertCommand, err := c.ConvertCommand(image, jpegFilename, xmpFilename); err != nil {
|
||||
return nil, err
|
||||
} else if err := convertCommand.Run(); err != nil {
|
||||
|
|
|
@ -51,7 +51,7 @@ func (i *Indexer) IndexRelated(mediaFile *MediaFile, o IndexerOptions) map[strin
|
|||
mainIndexResult := i.indexMediaFile(mainFile, o)
|
||||
indexed[mainFile.Filename()] = true
|
||||
|
||||
log.Infof("%s main %s file \"%s\"", mainIndexResult, mainFile.Type(), mainFile.RelativeFilename(i.originalsPath()))
|
||||
log.Infof("index: %s main %s file \"%s\"", mainIndexResult, mainFile.Type(), mainFile.RelativeFilename(i.originalsPath()))
|
||||
|
||||
for _, relatedMediaFile := range relatedFiles {
|
||||
if indexed[relatedMediaFile.Filename()] {
|
||||
|
@ -61,7 +61,7 @@ func (i *Indexer) IndexRelated(mediaFile *MediaFile, o IndexerOptions) map[strin
|
|||
indexResult := i.indexMediaFile(relatedMediaFile, o)
|
||||
indexed[relatedMediaFile.Filename()] = true
|
||||
|
||||
log.Infof("%s related %s file \"%s\"", indexResult, relatedMediaFile.Type(), relatedMediaFile.RelativeFilename(i.originalsPath()))
|
||||
log.Infof("index: %s related %s file \"%s\"", indexResult, relatedMediaFile.Type(), relatedMediaFile.RelativeFilename(i.originalsPath()))
|
||||
}
|
||||
|
||||
return indexed
|
||||
|
|
|
@ -21,6 +21,200 @@ const (
|
|||
|
||||
type IndexResult string
|
||||
|
||||
func (i *Indexer) indexMediaFile(mediaFile *MediaFile, o IndexerOptions) IndexResult {
|
||||
var photo models.Photo
|
||||
var file, primaryFile models.File
|
||||
var isPrimary = false
|
||||
var exifData *Exif
|
||||
var photoQuery, fileQuery *gorm.DB
|
||||
var keywords []string
|
||||
|
||||
labels := Labels{}
|
||||
fileBase := mediaFile.Basename()
|
||||
filePath := mediaFile.RelativePath(i.originalsPath())
|
||||
fileName := mediaFile.RelativeFilename(i.originalsPath())
|
||||
fileHash := mediaFile.Hash()
|
||||
fileChanged := true
|
||||
fileExists := false
|
||||
photoExists := false
|
||||
|
||||
event.Publish("index.indexing", event.Data{
|
||||
"fileHash": fileHash,
|
||||
"fileName": fileName,
|
||||
"baseName": filepath.Base(fileName),
|
||||
})
|
||||
|
||||
fileQuery = i.db.Unscoped().First(&file, "file_hash = ? OR file_name = ?", fileHash, fileName)
|
||||
fileExists = fileQuery.Error == nil
|
||||
|
||||
if !fileExists {
|
||||
photoQuery = i.db.Unscoped().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fileBase)
|
||||
|
||||
if photoQuery.Error != nil && mediaFile.HasTimeAndPlace() {
|
||||
exifData, _ = mediaFile.Exif()
|
||||
photoQuery = i.db.Unscoped().First(&photo, "photo_lat = ? AND photo_long = ? AND taken_at = ?", exifData.Lat, exifData.Long, exifData.TakenAt)
|
||||
}
|
||||
} else {
|
||||
photoQuery = i.db.Unscoped().First(&photo, "id = ?", file.PhotoID)
|
||||
fileChanged = file.FileHash != fileHash
|
||||
isPrimary = file.FilePrimary
|
||||
}
|
||||
|
||||
photoExists = photoQuery.Error == nil
|
||||
|
||||
if !fileChanged && photoExists && !photo.TakenAt.IsZero() && o.SkipUnchanged() {
|
||||
return indexResultSkipped
|
||||
}
|
||||
|
||||
photo.PhotoPath = filePath
|
||||
photo.PhotoName = fileBase
|
||||
|
||||
if isPrimary || !photoExists || photo.TakenAt.IsZero() {
|
||||
if jpeg, err := mediaFile.Jpeg(); err == nil {
|
||||
if fileChanged || o.UpdateLabels || o.UpdateTitle {
|
||||
// Image classification labels
|
||||
labels = i.classifyImage(jpeg)
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateExif {
|
||||
// Read UpdateExif data
|
||||
if exifData, err := jpeg.Exif(); err == nil {
|
||||
photo.PhotoLat = exifData.Lat
|
||||
photo.PhotoLong = exifData.Long
|
||||
photo.TakenAt = exifData.TakenAt
|
||||
photo.TakenAtLocal = exifData.TakenAtLocal
|
||||
photo.TimeZone = exifData.TimeZone
|
||||
photo.PhotoAltitude = exifData.Altitude
|
||||
photo.PhotoArtist = exifData.Artist
|
||||
|
||||
if exifData.UUID != "" {
|
||||
log.Debugf("index: photo uuid \"%s\"", exifData.UUID)
|
||||
photo.PhotoUUID = exifData.UUID
|
||||
} else {
|
||||
log.Debug("index: no photo uuid in exif data")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateCamera {
|
||||
// Set UpdateCamera, Lens, Focal Length and F Number
|
||||
photo.Camera = models.NewCamera(mediaFile.CameraModel(), mediaFile.CameraMake()).FirstOrCreate(i.db)
|
||||
photo.Lens = models.NewLens(mediaFile.LensModel(), mediaFile.LensMake()).FirstOrCreate(i.db)
|
||||
photo.PhotoFocalLength = mediaFile.FocalLength()
|
||||
photo.PhotoFNumber = mediaFile.FNumber()
|
||||
photo.PhotoIso = mediaFile.Iso()
|
||||
photo.PhotoExposure = mediaFile.Exposure()
|
||||
}
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateLocation || o.UpdateTitle {
|
||||
keywords, labels = i.indexLocation(mediaFile, &photo, keywords, labels, fileChanged, o)
|
||||
}
|
||||
|
||||
if (fileChanged || o.UpdateTitle) && photo.PhotoTitle == "" {
|
||||
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", util.Title(labels[0].Name), mediaFile.DateCreated().Format("2006"))
|
||||
} else if !photo.TakenAtLocal.IsZero() {
|
||||
var daytimeString string
|
||||
hour := photo.TakenAtLocal.Hour()
|
||||
|
||||
switch {
|
||||
case hour < 17:
|
||||
daytimeString = "Unknown"
|
||||
case hour < 20:
|
||||
daytimeString = "Sunset"
|
||||
default:
|
||||
daytimeString = "Unknown"
|
||||
}
|
||||
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", daytimeString, photo.TakenAtLocal.Format("2006"))
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("index: changed empty photo title to \"%s\"", photo.PhotoTitle)
|
||||
}
|
||||
|
||||
// This should never happen
|
||||
if photo.TakenAt.IsZero() || photo.TakenAtLocal.IsZero() {
|
||||
photo.TakenAt = mediaFile.DateCreated()
|
||||
photo.TakenAtLocal = photo.TakenAt
|
||||
|
||||
log.Warnf("index: %s has invalid date, set to \"%s\"", filepath.Base(mediaFile.Filename()), photo.TakenAt.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
|
||||
if photoExists {
|
||||
// Estimate location
|
||||
if o.UpdateLocation && photo.LocationID == 0 {
|
||||
i.estimateLocation(&photo)
|
||||
}
|
||||
|
||||
i.db.Unscoped().Save(&photo)
|
||||
} else {
|
||||
event.Publish("count.photos", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
|
||||
photo.PhotoFavorite = false
|
||||
|
||||
i.db.Create(&photo)
|
||||
}
|
||||
|
||||
if len(labels) > 0 {
|
||||
log.Infof("index: adding labels %+v", labels)
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateLabels {
|
||||
i.addLabels(photo.ID, labels)
|
||||
}
|
||||
|
||||
if result := i.db.Where("file_type = 'jpg' AND file_primary = 1 AND photo_id = ?", photo.ID).First(&primaryFile); result.Error != nil {
|
||||
isPrimary = mediaFile.IsJpeg()
|
||||
} else {
|
||||
isPrimary = mediaFile.IsJpeg() && (fileName == primaryFile.FileName || fileHash == primaryFile.FileHash)
|
||||
}
|
||||
|
||||
if (fileChanged || o.UpdateKeywords || o.UpdateTitle) && isPrimary {
|
||||
photo.IndexKeywords(keywords, i.db)
|
||||
}
|
||||
|
||||
file.PhotoID = photo.ID
|
||||
file.PhotoUUID = photo.PhotoUUID
|
||||
file.FilePrimary = isPrimary
|
||||
file.FileMissing = false
|
||||
file.FileName = fileName
|
||||
file.FileHash = fileHash
|
||||
file.FileType = mediaFile.Type()
|
||||
file.FileMime = mediaFile.MimeType()
|
||||
file.FileOrientation = mediaFile.Orientation()
|
||||
|
||||
if fileChanged || o.UpdateColors {
|
||||
// Color information
|
||||
if p, err := mediaFile.Colors(i.thumbnailsPath()); err == nil {
|
||||
file.FileMainColor = p.MainColor.Name()
|
||||
file.FileColors = p.Colors.Hex()
|
||||
file.FileLuminance = p.Luminance.Hex()
|
||||
file.FileChroma = p.Chroma.Uint()
|
||||
}
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateSize {
|
||||
if mediaFile.Width() > 0 && mediaFile.Height() > 0 {
|
||||
file.FileWidth = mediaFile.Width()
|
||||
file.FileHeight = mediaFile.Height()
|
||||
file.FileAspectRatio = mediaFile.AspectRatio()
|
||||
file.FilePortrait = mediaFile.Width() < mediaFile.Height()
|
||||
}
|
||||
}
|
||||
|
||||
if fileQuery.Error == nil {
|
||||
i.db.Unscoped().Save(&file)
|
||||
return indexResultUpdated
|
||||
}
|
||||
|
||||
i.db.Create(&file)
|
||||
return indexResultAdded
|
||||
}
|
||||
|
||||
// classifyImage returns all matching labels for a media file.
|
||||
func (i *Indexer) classifyImage(jpeg *MediaFile) (results Labels) {
|
||||
start := time.Now()
|
||||
|
@ -70,289 +264,126 @@ func (i *Indexer) classifyImage(jpeg *MediaFile) (results Labels) {
|
|||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
log.Debugf("finding %+v labels for %s took %s", results, jpeg.Filename(), elapsed)
|
||||
log.Debugf("index: image classification took %s", elapsed)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (i *Indexer) indexMediaFile(mediaFile *MediaFile, o IndexerOptions) IndexResult {
|
||||
var photo models.Photo
|
||||
var file, primaryFile models.File
|
||||
var isPrimary = false
|
||||
var exifData *Exif
|
||||
var photoQuery, fileQuery *gorm.DB
|
||||
var keywords []string
|
||||
func (i *Indexer) addLabels(photoId uint, labels Labels) {
|
||||
for _, label := range labels {
|
||||
lm := models.NewLabel(label.Name, label.Priority).FirstOrCreate(i.db)
|
||||
|
||||
labels := Labels{}
|
||||
fileBase := mediaFile.Basename()
|
||||
filePath := mediaFile.RelativePath(i.originalsPath())
|
||||
fileName := mediaFile.RelativeFilename(i.originalsPath())
|
||||
fileHash := mediaFile.Hash()
|
||||
fileChanged := true
|
||||
fileExists := false
|
||||
photoExists := false
|
||||
|
||||
event.Publish("index.file", event.Data{
|
||||
"fileHash": fileHash,
|
||||
"fileName": fileName,
|
||||
"baseName": filepath.Base(fileName),
|
||||
})
|
||||
|
||||
exifData, err := mediaFile.Exif()
|
||||
|
||||
if err != nil {
|
||||
log.Debug(err)
|
||||
}
|
||||
|
||||
fileQuery = i.db.Unscoped().First(&file, "file_hash = ? OR file_name = ?", fileHash, fileName)
|
||||
fileExists = fileQuery.Error == nil
|
||||
|
||||
if !fileExists {
|
||||
photoQuery = i.db.Unscoped().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fileBase)
|
||||
|
||||
if photoQuery.Error != nil && mediaFile.HasTimeAndPlace() {
|
||||
photoQuery = i.db.Unscoped().First(&photo, "photo_lat = ? AND photo_long = ? AND taken_at = ?", exifData.Lat, exifData.Long, exifData.TakenAt)
|
||||
}
|
||||
} else {
|
||||
photoQuery = i.db.Unscoped().First(&photo, "id = ?", file.PhotoID)
|
||||
fileChanged = file.FileHash != fileHash
|
||||
}
|
||||
|
||||
photoExists = photoQuery.Error == nil
|
||||
|
||||
if !fileChanged && photoExists && o.SkipUnchanged() {
|
||||
return indexResultSkipped
|
||||
}
|
||||
|
||||
photo.PhotoPath = filePath
|
||||
photo.PhotoName = fileBase
|
||||
|
||||
if jpeg, err := mediaFile.Jpeg(); err == nil {
|
||||
|
||||
if fileChanged || o.UpdateLabels {
|
||||
// Image classification labels
|
||||
labels = i.classifyImage(jpeg)
|
||||
if lm.New {
|
||||
event.Publish("count.labels", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateExif {
|
||||
// Read UpdateExif data
|
||||
if exifData, err := jpeg.Exif(); err == nil {
|
||||
photo.PhotoLat = exifData.Lat
|
||||
photo.PhotoLong = exifData.Long
|
||||
photo.TakenAt = exifData.TakenAt
|
||||
photo.TakenAtLocal = exifData.TakenAtLocal
|
||||
photo.TimeZone = exifData.TimeZone
|
||||
photo.PhotoAltitude = exifData.Altitude
|
||||
photo.PhotoArtist = exifData.Artist
|
||||
|
||||
if exifData.UUID != "" {
|
||||
log.Debugf("photo uuid: %s", exifData.UUID)
|
||||
photo.PhotoUUID = exifData.UUID
|
||||
} else {
|
||||
log.Debug("no photo uuid")
|
||||
}
|
||||
}
|
||||
if lm.LabelPriority != label.Priority {
|
||||
lm.LabelPriority = label.Priority
|
||||
i.db.Save(&lm)
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateCamera {
|
||||
// Set UpdateCamera, Lens, Focal Length and F Number
|
||||
photo.Camera = models.NewCamera(mediaFile.CameraModel(), mediaFile.CameraMake()).FirstOrCreate(i.db)
|
||||
photo.Lens = models.NewLens(mediaFile.LensModel(), mediaFile.LensMake()).FirstOrCreate(i.db)
|
||||
photo.PhotoFocalLength = mediaFile.FocalLength()
|
||||
photo.PhotoFNumber = mediaFile.FNumber()
|
||||
photo.PhotoIso = mediaFile.Iso()
|
||||
photo.PhotoExposure = mediaFile.Exposure()
|
||||
}
|
||||
}
|
||||
plm := models.NewPhotoLabel(photoId, lm.ID, label.Uncertainty, label.Source).FirstOrCreate(i.db)
|
||||
|
||||
if fileChanged || o.UpdateDate {
|
||||
if photo.TakenAt.IsZero() || photo.TakenAtLocal.IsZero() {
|
||||
photo.TakenAt = mediaFile.DateCreated()
|
||||
photo.TakenAtLocal = photo.TakenAt
|
||||
}
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateLocation {
|
||||
if location, err := mediaFile.Location(); err == nil {
|
||||
i.db.FirstOrCreate(location, "id = ?", location.ID)
|
||||
photo.Location = location
|
||||
photo.LocationEstimated = false
|
||||
|
||||
photo.Country = models.NewCountry(location.LocCountryCode, location.LocCountry).FirstOrCreate(i.db)
|
||||
|
||||
keywords = append(keywords, util.Keywords(location.LocDisplayName)...)
|
||||
|
||||
// Append labels from OpenStreetMap
|
||||
if location.LocCity != "" {
|
||||
labels = append(labels, NewLocationLabel(location.LocCity, 0, -2))
|
||||
}
|
||||
|
||||
if location.LocCountry != "" {
|
||||
labels = append(labels, NewLocationLabel(location.LocCountry, 0, -2))
|
||||
}
|
||||
|
||||
if location.LocCategory != "" {
|
||||
labels = append(labels, NewLocationLabel(location.LocCategory, 0, -2))
|
||||
}
|
||||
|
||||
if location.LocType != "" {
|
||||
labels = append(labels, NewLocationLabel(location.LocType, 0, -1))
|
||||
}
|
||||
|
||||
// Sort by priority and uncertainty
|
||||
sort.Sort(labels)
|
||||
|
||||
|
||||
if (fileChanged || o.UpdateTitle) && photo.PhotoTitleChanged == false {
|
||||
log.Infof("setting title based on the following labels: %#v", labels)
|
||||
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 60 && labels[0].Name != "" { // TODO: User defined title format
|
||||
log.Infof("label for title: %#v", labels[0])
|
||||
if location.LocCity == "" || len(location.LocCity) > 16 || strings.Contains(labels[0].Name, location.LocCity) {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(labels[0].Name), location.LocCountry, photo.TakenAt.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(labels[0].Name), location.LocCity, photo.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if location.LocName != "" && location.LocCity != "" {
|
||||
if len(location.LocName) > 45 {
|
||||
photo.PhotoTitle = util.Title(location.LocName)
|
||||
} else if len(location.LocName) > 20 || len(location.LocCity) > 16 || strings.Contains(location.LocName, location.LocCity) {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", util.Title(location.LocName), photo.TakenAt.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(location.LocName), location.LocCity, photo.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if location.LocCity != "" && location.LocCountry != "" {
|
||||
if len(location.LocCity) > 20 {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.LocCity, photo.TakenAt.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCity, location.LocCountry, photo.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if location.LocCounty != "" && location.LocCountry != "" {
|
||||
if len(location.LocCounty) > 20 {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.LocCounty, photo.TakenAt.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCounty, location.LocCountry, photo.TakenAt.Format("2006"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Debugf("location cannot be determined precisely: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if (fileChanged || o.UpdateTitle) && photo.PhotoTitleChanged == false && photo.PhotoTitle == "" {
|
||||
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", util.Title(labels[0].Name), mediaFile.DateCreated().Format("2006"))
|
||||
} else if !photo.TakenAtLocal.IsZero() {
|
||||
var daytimeString string
|
||||
hour := photo.TakenAtLocal.Hour()
|
||||
|
||||
switch {
|
||||
case hour < 17:
|
||||
daytimeString = "Unknown"
|
||||
case hour < 20:
|
||||
daytimeString = "Sunset"
|
||||
default:
|
||||
daytimeString = "Unknown"
|
||||
}
|
||||
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", daytimeString, photo.TakenAtLocal.Format("2006"))
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("title: \"%s\"", photo.PhotoTitle)
|
||||
|
||||
if photoExists {
|
||||
// Estimate location
|
||||
if o.UpdateLocation && photo.LocationID == 0 {
|
||||
var recentPhoto models.Photo
|
||||
|
||||
if result := i.db.Unscoped().Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", photo.TakenAt)).Preload("Country").First(&recentPhoto); result.Error == nil {
|
||||
if recentPhoto.Country != nil {
|
||||
photo.Country = recentPhoto.Country
|
||||
photo.LocationEstimated = true
|
||||
log.Debugf("approximate location: %s", recentPhoto.Country.CountryName)
|
||||
}
|
||||
}
|
||||
// Add categories
|
||||
for _, category := range label.Categories {
|
||||
sn := models.NewLabel(category, -1).FirstOrCreate(i.db)
|
||||
i.db.Model(&lm).Association("LabelCategories").Append(sn)
|
||||
}
|
||||
|
||||
i.db.Unscoped().Save(&photo)
|
||||
} else {
|
||||
photo.PhotoFavorite = false
|
||||
|
||||
i.db.Create(&photo)
|
||||
}
|
||||
|
||||
log.Infof("adding labels: %+v", labels)
|
||||
|
||||
if fileChanged || o.UpdateLabels {
|
||||
for _, label := range labels {
|
||||
lm := models.NewLabel(label.Name, label.Priority).FirstOrCreate(i.db)
|
||||
|
||||
if lm.LabelPriority != label.Priority {
|
||||
lm.LabelPriority = label.Priority
|
||||
i.db.Save(&lm)
|
||||
}
|
||||
|
||||
plm := models.NewPhotoLabel(photo.ID, lm.ID, label.Uncertainty, label.Source).FirstOrCreate(i.db)
|
||||
|
||||
// Add categories
|
||||
for _, category := range label.Categories {
|
||||
sn := models.NewLabel(category, -1).FirstOrCreate(i.db)
|
||||
i.db.Model(&lm).Association("LabelCategories").Append(sn)
|
||||
}
|
||||
|
||||
if plm.LabelUncertainty > label.Uncertainty {
|
||||
plm.LabelUncertainty = label.Uncertainty
|
||||
plm.LabelSource = label.Source
|
||||
i.db.Save(&plm)
|
||||
}
|
||||
if plm.LabelUncertainty > label.Uncertainty {
|
||||
plm.LabelUncertainty = label.Uncertainty
|
||||
plm.LabelSource = label.Source
|
||||
i.db.Save(&plm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Indexer) indexLocation (mediaFile *MediaFile, photo *models.Photo, keywords []string, labels Labels, fileChanged bool, o IndexerOptions) ([]string, Labels){
|
||||
if location, err := mediaFile.Location(); err == nil {
|
||||
i.db.FirstOrCreate(location, "id = ?", location.ID)
|
||||
photo.Location = location
|
||||
photo.LocationEstimated = false
|
||||
|
||||
photo.Country = models.NewCountry(location.LocCountryCode, location.LocCountry).FirstOrCreate(i.db)
|
||||
|
||||
if photo.Country.New {
|
||||
event.Publish("count.countries", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
}
|
||||
|
||||
keywords = append(keywords, util.Keywords(location.LocDisplayName)...)
|
||||
|
||||
// Append labels from OpenStreetMap
|
||||
if location.LocCity != "" {
|
||||
labels = append(labels, NewLocationLabel(location.LocCity, 0, -2))
|
||||
}
|
||||
|
||||
if location.LocCountry != "" {
|
||||
labels = append(labels, NewLocationLabel(location.LocCountry, 0, -2))
|
||||
}
|
||||
|
||||
if location.LocCategory != "" {
|
||||
labels = append(labels, NewLocationLabel(location.LocCategory, 0, -2))
|
||||
}
|
||||
|
||||
if location.LocType != "" {
|
||||
labels = append(labels, NewLocationLabel(location.LocType, 0, -1))
|
||||
}
|
||||
|
||||
// Sort by priority and uncertainty
|
||||
sort.Sort(labels)
|
||||
|
||||
|
||||
if (fileChanged || o.UpdateTitle) && photo.PhotoTitleChanged == false {
|
||||
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 60 && labels[0].Name != "" { // TODO: User defined title format
|
||||
log.Infof("index: using label %s to create photo title (%d%% uncertainty)", labels[0].Name, labels[0].Uncertainty)
|
||||
if location.LocCity == "" || len(location.LocCity) > 16 || strings.Contains(labels[0].Name, location.LocCity) {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(labels[0].Name), location.LocCountry, photo.TakenAt.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(labels[0].Name), location.LocCity, photo.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if location.LocName != "" && location.LocCity != "" {
|
||||
if len(location.LocName) > 45 {
|
||||
photo.PhotoTitle = util.Title(location.LocName)
|
||||
} else if len(location.LocName) > 20 || len(location.LocCity) > 16 || strings.Contains(location.LocName, location.LocCity) {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", util.Title(location.LocName), photo.TakenAt.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(location.LocName), location.LocCity, photo.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if location.LocCity != "" && location.LocCountry != "" {
|
||||
if len(location.LocCity) > 20 {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.LocCity, photo.TakenAt.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCity, location.LocCountry, photo.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if location.LocCounty != "" && location.LocCountry != "" {
|
||||
if len(location.LocCounty) > 20 {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", location.LocCounty, photo.TakenAt.Format("2006"))
|
||||
} else {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCounty, location.LocCountry, photo.TakenAt.Format("2006"))
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("index: new photo title is \"%s\"", photo.PhotoTitle)
|
||||
}
|
||||
} else {
|
||||
log.Debugf("index: location cannot be determined precisely (%s)", err.Error())
|
||||
}
|
||||
|
||||
return keywords, labels
|
||||
}
|
||||
|
||||
func (i *Indexer) estimateLocation(photo *models.Photo) {
|
||||
var recentPhoto models.Photo
|
||||
|
||||
if result := i.db.Unscoped().Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", photo.TakenAt)).Preload("Country").First(&recentPhoto); result.Error == nil {
|
||||
if recentPhoto.Country != nil {
|
||||
photo.Country = recentPhoto.Country
|
||||
photo.LocationEstimated = true
|
||||
log.Debugf("index: approximate location is \"%s\"", recentPhoto.Country.CountryName)
|
||||
}
|
||||
}
|
||||
|
||||
if result := i.db.Where("file_type = 'jpg' AND file_primary = 1 AND photo_id = ?", photo.ID).First(&primaryFile); result.Error != nil {
|
||||
isPrimary = mediaFile.IsJpeg()
|
||||
} else {
|
||||
isPrimary = mediaFile.IsJpeg() && (fileName == primaryFile.FileName || fileHash == primaryFile.FileHash)
|
||||
}
|
||||
|
||||
if (fileChanged || o.UpdateKeywords) && isPrimary {
|
||||
photo.IndexKeywords(keywords, i.db)
|
||||
}
|
||||
|
||||
file.PhotoID = photo.ID
|
||||
file.PhotoUUID = photo.PhotoUUID
|
||||
file.FilePrimary = isPrimary
|
||||
file.FileMissing = false
|
||||
file.FileName = fileName
|
||||
file.FileHash = fileHash
|
||||
file.FileType = mediaFile.Type()
|
||||
file.FileMime = mediaFile.MimeType()
|
||||
file.FileOrientation = mediaFile.Orientation()
|
||||
|
||||
if fileChanged || o.UpdateColors {
|
||||
// Color information
|
||||
if p, err := mediaFile.Colors(i.thumbnailsPath()); err == nil {
|
||||
file.FileMainColor = p.MainColor.Name()
|
||||
file.FileColors = p.Colors.Hex()
|
||||
file.FileLuminance = p.Luminance.Hex()
|
||||
file.FileChroma = p.Chroma.Uint()
|
||||
}
|
||||
}
|
||||
|
||||
if fileChanged || o.UpdateSize {
|
||||
if mediaFile.Width() > 0 && mediaFile.Height() > 0 {
|
||||
file.FileWidth = mediaFile.Width()
|
||||
file.FileHeight = mediaFile.Height()
|
||||
file.FileAspectRatio = mediaFile.AspectRatio()
|
||||
file.FilePortrait = mediaFile.Width() < mediaFile.Height()
|
||||
}
|
||||
}
|
||||
|
||||
if fileQuery.Error == nil {
|
||||
i.db.Unscoped().Save(&file)
|
||||
return indexResultUpdated
|
||||
}
|
||||
|
||||
i.db.Create(&file)
|
||||
return indexResultAdded
|
||||
}
|
||||
|
|
|
@ -323,10 +323,8 @@ func (m *MediaFile) RelativePath(directory string) string {
|
|||
if i := strings.Index(pathname, directory); i == 0 {
|
||||
if i := strings.LastIndex(directory, string(os.PathSeparator)); i == len(directory)-1 {
|
||||
pathname = pathname[len(directory):]
|
||||
log.Info(pathname)
|
||||
} else if i := strings.LastIndex(directory, string(os.PathSeparator)); i != len(directory) {
|
||||
pathname = pathname[len(directory)+1:]
|
||||
log.Info(pathname)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
|
@ -51,11 +52,12 @@ func (t *TensorFlow) loadLabelRules() (err error) {
|
|||
|
||||
fileName := t.conf.ConfigPath() + "/labels.yml"
|
||||
|
||||
log.Debugf("loading label rules from \"%s\"", fileName)
|
||||
log.Debugf("tensorflow: loading label rules from \"%s\"", filepath.Base(fileName))
|
||||
|
||||
if !util.Exists(fileName) {
|
||||
log.Errorf("label rules file not found: \"%s\"", fileName)
|
||||
return fmt.Errorf("label rules file not found: \"%s\"", fileName)
|
||||
e := fmt.Errorf("tensorflow: label rules file not found in \"%s\"", filepath.Base(fileName))
|
||||
log.Error(e.Error())
|
||||
return e
|
||||
}
|
||||
|
||||
yamlConfig, err := ioutil.ReadFile(fileName)
|
||||
|
@ -115,7 +117,9 @@ func (t *TensorFlow) Labels(img []byte) (result Labels, err error) {
|
|||
// Return best labels
|
||||
result = t.bestLabels(output[0].Value().([][]float32)[0])
|
||||
|
||||
log.Debugf("labels: %v", result)
|
||||
if len(result) > 0 {
|
||||
log.Debugf("tensorflow: image classified as %+v", result)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
@ -123,7 +127,7 @@ func (t *TensorFlow) Labels(img []byte) (result Labels, err error) {
|
|||
func (t *TensorFlow) loadLabels(path string) error {
|
||||
modelLabels := path + "/labels.txt"
|
||||
|
||||
log.Infof("loading classification labels from \"%s\"", modelLabels)
|
||||
log.Infof("tensorflow: loading classification labels from labels.txt")
|
||||
|
||||
// Load labels
|
||||
f, err := os.Open(modelLabels)
|
||||
|
@ -156,7 +160,7 @@ func (t *TensorFlow) loadModel() error {
|
|||
|
||||
path := t.conf.TensorFlowModelPath()
|
||||
|
||||
log.Infof("loading image classification model from \"%s\"", path)
|
||||
log.Infof("tensorflow: loading image classification model from \"%s\"", filepath.Base(path))
|
||||
|
||||
// Load model
|
||||
model, err := tf.LoadSavedModel(path, []string{"photoprism"}, nil)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/photoprism/photoprism/internal/util"
|
||||
|
@ -93,6 +94,14 @@ func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string,
|
|||
return nil
|
||||
}
|
||||
|
||||
fileName := mediaFile.RelativeFilename(originalsPath)
|
||||
|
||||
event.Publish("index.thumbnails", event.Data{
|
||||
"fileName": fileName,
|
||||
"baseName": filepath.Base(fileName),
|
||||
"force": force,
|
||||
})
|
||||
|
||||
if err := mediaFile.CreateDefaultThumbnails(thumbnailsPath, force); err != nil {
|
||||
log.Errorf("could not create default thumbnails: %s", err)
|
||||
return err
|
||||
|
|
Loading…
Reference in a new issue