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",
|
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")) {
|
if(this.hasValue("settings")) {
|
||||||
this.setTheme(this.getValue("settings").theme);
|
this.setTheme(this.getValue("settings").theme);
|
||||||
|
@ -43,6 +44,32 @@ class Config {
|
||||||
return this;
|
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) {
|
updateSettings(values, $vuetify) {
|
||||||
this.setValue("settings", values);
|
this.setValue("settings", values);
|
||||||
this.setTheme(values.theme);
|
this.setTheme(values.theme);
|
||||||
|
|
|
@ -59,6 +59,7 @@
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title>
|
<v-list-tile-title>
|
||||||
<translate>Photos</translate>
|
<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-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
@ -97,6 +98,7 @@
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title>
|
<v-list-tile-title>
|
||||||
<translate>Albums</translate>
|
<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-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
@ -119,18 +121,7 @@
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title>
|
<v-list-tile-title>
|
||||||
<translate>Favorites</translate>
|
<translate>Favorites</translate>
|
||||||
</v-list-tile-title>
|
<span v-if="config.count.favorites > 0" class="p-navigation-count">{{ config.count.favorites }}</span>
|
||||||
</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>
|
|
||||||
</v-list-tile-title>
|
</v-list-tile-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
@ -143,6 +134,20 @@
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title>
|
<v-list-tile-title>
|
||||||
<translate>Labels</translate>
|
<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-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
@ -223,7 +228,7 @@
|
||||||
export default {
|
export default {
|
||||||
name: "p-navigation",
|
name: "p-navigation",
|
||||||
data() {
|
data() {
|
||||||
let mini = (window.innerWidth < 1600);
|
let mini = (window.innerWidth < 1400);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
drawer: null,
|
drawer: null,
|
||||||
|
|
|
@ -39,6 +39,15 @@ main {
|
||||||
z-index: 10;
|
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 {
|
#photoprism div.loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -128,6 +137,10 @@ main {
|
||||||
color: #F48FB1;
|
color: #F48FB1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#photoprism .p-log-warning {
|
||||||
|
color: #FFD600;
|
||||||
|
}
|
||||||
|
|
||||||
#photoprism .p-log-fatal {
|
#photoprism .p-log-fatal {
|
||||||
color: #FFECB3;
|
color: #FFECB3;
|
||||||
}
|
}
|
||||||
|
@ -137,5 +150,5 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
#photoprism .p-log-debug {
|
#photoprism .p-log-debug {
|
||||||
color: #EEEEEE;
|
color: #DDDDDD;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,11 @@
|
||||||
slider-color="secondary-dark"
|
slider-color="secondary-dark"
|
||||||
height="64"
|
height="64"
|
||||||
>
|
>
|
||||||
<v-tab id="tab-maintenance" ripple @click="changePath('/library')">
|
<v-tab id="tab-originals" ripple @click="changePath('/library')">
|
||||||
<translate>Originals</translate>
|
<translate>Originals</translate>
|
||||||
</v-tab>
|
</v-tab>
|
||||||
<v-tab-item>
|
<v-tab-item>
|
||||||
<p-tab-maintenance></p-tab-maintenance>
|
<p-tab-originals></p-tab-originals>
|
||||||
</v-tab-item>
|
</v-tab-item>
|
||||||
|
|
||||||
<v-tab id="tab-import" v-if="!readonly" ripple @click="changePath('/library/import')">
|
<v-tab id="tab-import" v-if="!readonly" ripple @click="changePath('/library/import')">
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
<script>
|
<script>
|
||||||
import uploadTab from "pages/library/upload.vue";
|
import uploadTab from "pages/library/upload.vue";
|
||||||
import importTab from "pages/library/import.vue";
|
import importTab from "pages/library/import.vue";
|
||||||
import maintenanceTab from "pages/library/maintenance.vue";
|
import originalsTab from "pages/library/originals.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'p-page-library',
|
name: 'p-page-library',
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
tab: Number
|
tab: Number
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
'p-tab-maintenance': maintenanceTab,
|
'p-tab-originals': originalsTab,
|
||||||
'p-tab-import': importTab,
|
'p-tab-import': importTab,
|
||||||
'p-tab-upload': uploadTab,
|
'p-tab-upload': uploadTab,
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,21 +3,39 @@
|
||||||
<v-form ref="form" class="p-photo-index" lazy-validation @submit.prevent="submit" dense>
|
<v-form ref="form" class="p-photo-index" lazy-validation @submit.prevent="submit" dense>
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
<p class="subheading">
|
<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="busy">Indexing photos and sidecar files...</span>
|
||||||
<span v-else-if="completed">Done.</span>
|
<span v-else-if="completed">Done.</span>
|
||||||
<span v-else>Press button to start indexing...</span>
|
<span v-else>Press button to start indexing...</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="options">
|
<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>
|
</p>
|
||||||
|
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="options.skip"
|
class="mb-0 mt-4 pa-0"
|
||||||
|
v-model="options.skipUnchanged"
|
||||||
color="secondary-dark"
|
color="secondary-dark"
|
||||||
:disabled="busy"
|
: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-checkbox>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
|
@ -27,7 +45,7 @@
|
||||||
depressed
|
depressed
|
||||||
@click.stop="startIndexing()"
|
@click.stop="startIndexing()"
|
||||||
>
|
>
|
||||||
<translate>Start</translate>
|
<translate>Index</translate>
|
||||||
<v-icon right dark>update</v-icon>
|
<v-icon right dark>update</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
@ -45,17 +63,23 @@
|
||||||
name: 'p-tab-index',
|
name: 'p-tab-index',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
readonly: this.$config.getValue("readonly"),
|
||||||
started: false,
|
started: false,
|
||||||
busy: false,
|
busy: false,
|
||||||
completed: 0,
|
completed: 0,
|
||||||
subscriptionId: '',
|
subscriptionId: "",
|
||||||
fileName: '',
|
action: "",
|
||||||
|
fileName: "",
|
||||||
source: null,
|
source: null,
|
||||||
options: {
|
options: {
|
||||||
skip: true
|
skipUnchanged: true,
|
||||||
|
createThumbs: false,
|
||||||
|
convertRaw: false,
|
||||||
},
|
},
|
||||||
labels: {
|
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;
|
const ctx = this;
|
||||||
Notify.blockUI();
|
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();
|
Notify.unblockUI();
|
||||||
ctx.busy = false;
|
ctx.busy = false;
|
||||||
ctx.completed = 100;
|
ctx.completed = 100;
|
||||||
|
@ -94,7 +118,7 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleEvent(ev, data) {
|
handleEvent(ev, data) {
|
||||||
if(this.source) {
|
if (this.source) {
|
||||||
this.source.cancel('run in background');
|
this.source.cancel('run in background');
|
||||||
this.source = null;
|
this.source = null;
|
||||||
Notify.unblockUI();
|
Notify.unblockUI();
|
||||||
|
@ -103,12 +127,26 @@
|
||||||
const type = ev.split('.')[1];
|
const type = ev.split('.')[1];
|
||||||
|
|
||||||
switch (type) {
|
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.busy = true;
|
||||||
this.completed = 0;
|
this.completed = 0;
|
||||||
this.fileName = data.fileName;
|
this.fileName = data.fileName;
|
||||||
break;
|
break;
|
||||||
case 'completed':
|
case 'completed':
|
||||||
|
this.action = "";
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
this.completed = 100;
|
this.completed = 100;
|
||||||
this.fileName = '';
|
this.fileName = '';
|
|
@ -86,6 +86,10 @@ func CreateAlbum(router *gin.RouterGroup, conf *config.Config) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.Publish("count.albums", event.Data{
|
||||||
|
"count": 1,
|
||||||
|
})
|
||||||
|
|
||||||
event.Success(fmt.Sprintf("album \"%s\" created", m.AlbumName))
|
event.Success(fmt.Sprintf("album \"%s\" created", m.AlbumName))
|
||||||
|
|
||||||
c.JSON(http.StatusOK, m)
|
c.JSON(http.StatusOK, m)
|
||||||
|
|
|
@ -46,6 +46,10 @@ func BatchPhotosDelete(router *gin.RouterGroup, conf *config.Config) {
|
||||||
|
|
||||||
elapsed := int(time.Since(start).Seconds())
|
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)})
|
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)))
|
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)
|
initIndexer(conf)
|
||||||
|
|
||||||
if f.SkipExisting {
|
if f.SkipUnchanged {
|
||||||
indexer.IndexOriginals(photoprism.IndexerOptionsNone())
|
indexer.IndexOriginals(photoprism.IndexerOptionsNone())
|
||||||
} else {
|
} else {
|
||||||
indexer.IndexOriginals(photoprism.IndexerOptionsAll())
|
indexer.IndexOriginals(photoprism.IndexerOptionsAll())
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
@ -110,6 +111,10 @@ func LikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||||
m.PhotoFavorite = true
|
m.PhotoFavorite = true
|
||||||
conf.Db().Save(&m)
|
conf.Db().Save(&m)
|
||||||
|
|
||||||
|
event.Publish("count.favorites", event.Data{
|
||||||
|
"count": 1,
|
||||||
|
})
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
c.JSON(http.StatusOK, gin.H{"photo": m})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -136,6 +141,10 @@ func DislikePhoto(router *gin.RouterGroup, conf *config.Config) {
|
||||||
m.PhotoFavorite = false
|
m.PhotoFavorite = false
|
||||||
conf.Db().Save(&m)
|
conf.Db().Save(&m)
|
||||||
|
|
||||||
|
event.Publish("count.favorites", event.Data{
|
||||||
|
"count": -1,
|
||||||
|
})
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"photo": m})
|
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) {
|
func wsWriter(ws *websocket.Conn, conf *config.Config) {
|
||||||
pingTicker := time.NewTicker(10 * time.Second)
|
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() {
|
defer func() {
|
||||||
pingTicker.Stop()
|
pingTicker.Stop()
|
||||||
|
|
|
@ -525,11 +525,46 @@ func (c *Config) ClientConfig() ClientConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
var countries []country
|
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.Table("labels").
|
||||||
db.Where("deleted_at IS NULL AND album_favorite = 1").Limit(20).Order("album_name").Find(&albums)
|
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")
|
jsHash := util.Hash(c.HttpStaticBuildPath() + "/app.js")
|
||||||
cssHash := util.Hash(c.HttpStaticBuildPath() + "/app.css")
|
cssHash := util.Hash(c.HttpStaticBuildPath() + "/app.css")
|
||||||
|
@ -548,6 +583,7 @@ func (c *Config) ClientConfig() ClientConfig {
|
||||||
"jsHash": jsHash,
|
"jsHash": jsHash,
|
||||||
"cssHash": cssHash,
|
"cssHash": cssHash,
|
||||||
"settings": c.Settings(),
|
"settings": c.Settings(),
|
||||||
|
"count": count,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package form
|
package form
|
||||||
|
|
||||||
type IndexerOptions struct {
|
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;"`
|
CountryNotes string `gorm:"type:text;"`
|
||||||
CountryPhoto *Photo
|
CountryPhoto *Photo
|
||||||
CountryPhotoID uint
|
CountryPhotoID uint
|
||||||
|
New bool `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new country
|
// Create a new country
|
||||||
|
@ -41,3 +42,7 @@ func (m *Country) FirstOrCreate(db *gorm.DB) *Country {
|
||||||
|
|
||||||
return m
|
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;"`
|
LabelDescription string `gorm:"type:text;"`
|
||||||
LabelNotes string `gorm:"type:text;"`
|
LabelNotes string `gorm:"type:text;"`
|
||||||
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id"`
|
LabelCategories []*Label `gorm:"many2many:categories;association_jointable_foreignkey:category_id"`
|
||||||
|
New bool `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLabel(labelName string, labelPriority int) *Label {
|
func NewLabel(labelName string, labelPriority int) *Label {
|
||||||
|
@ -42,3 +43,7 @@ func (m *Label) FirstOrCreate(db *gorm.DB) *Label {
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Label) AfterCreate(scope *gorm.Scope) error {
|
||||||
|
return scope.SetColumn("New", true)
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Converter wraps a darktable cli binary.
|
// 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)
|
log.Infof("converting \"%s\" to \"%s\"", image.filename, jpegFilename)
|
||||||
|
|
||||||
|
fileName := image.RelativeFilename(c.conf.OriginalsPath())
|
||||||
|
|
||||||
xmpFilename := baseFilename + ".xmp"
|
xmpFilename := baseFilename + ".xmp"
|
||||||
|
|
||||||
if _, err := os.Stat(xmpFilename); err != nil {
|
if _, err := os.Stat(xmpFilename); err != nil {
|
||||||
xmpFilename = ""
|
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 {
|
if convertCommand, err := c.ConvertCommand(image, jpegFilename, xmpFilename); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if err := convertCommand.Run(); err != nil {
|
} 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)
|
mainIndexResult := i.indexMediaFile(mainFile, o)
|
||||||
indexed[mainFile.Filename()] = true
|
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 {
|
for _, relatedMediaFile := range relatedFiles {
|
||||||
if indexed[relatedMediaFile.Filename()] {
|
if indexed[relatedMediaFile.Filename()] {
|
||||||
|
@ -61,7 +61,7 @@ func (i *Indexer) IndexRelated(mediaFile *MediaFile, o IndexerOptions) map[strin
|
||||||
indexResult := i.indexMediaFile(relatedMediaFile, o)
|
indexResult := i.indexMediaFile(relatedMediaFile, o)
|
||||||
indexed[relatedMediaFile.Filename()] = true
|
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
|
return indexed
|
||||||
|
|
|
@ -21,6 +21,200 @@ const (
|
||||||
|
|
||||||
type IndexResult string
|
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.
|
// classifyImage returns all matching labels for a media file.
|
||||||
func (i *Indexer) classifyImage(jpeg *MediaFile) (results Labels) {
|
func (i *Indexer) classifyImage(jpeg *MediaFile) (results Labels) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
@ -70,289 +264,126 @@ func (i *Indexer) classifyImage(jpeg *MediaFile) (results Labels) {
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
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
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Indexer) indexMediaFile(mediaFile *MediaFile, o IndexerOptions) IndexResult {
|
func (i *Indexer) addLabels(photoId uint, labels Labels) {
|
||||||
var photo models.Photo
|
for _, label := range labels {
|
||||||
var file, primaryFile models.File
|
lm := models.NewLabel(label.Name, label.Priority).FirstOrCreate(i.db)
|
||||||
var isPrimary = false
|
|
||||||
var exifData *Exif
|
|
||||||
var photoQuery, fileQuery *gorm.DB
|
|
||||||
var keywords []string
|
|
||||||
|
|
||||||
labels := Labels{}
|
if lm.New {
|
||||||
fileBase := mediaFile.Basename()
|
event.Publish("count.labels", event.Data{
|
||||||
filePath := mediaFile.RelativePath(i.originalsPath())
|
"count": 1,
|
||||||
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 fileChanged || o.UpdateExif {
|
if lm.LabelPriority != label.Priority {
|
||||||
// Read UpdateExif data
|
lm.LabelPriority = label.Priority
|
||||||
if exifData, err := jpeg.Exif(); err == nil {
|
i.db.Save(&lm)
|
||||||
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 fileChanged || o.UpdateCamera {
|
plm := models.NewPhotoLabel(photoId, lm.ID, label.Uncertainty, label.Source).FirstOrCreate(i.db)
|
||||||
// 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.UpdateDate {
|
// Add categories
|
||||||
if photo.TakenAt.IsZero() || photo.TakenAtLocal.IsZero() {
|
for _, category := range label.Categories {
|
||||||
photo.TakenAt = mediaFile.DateCreated()
|
sn := models.NewLabel(category, -1).FirstOrCreate(i.db)
|
||||||
photo.TakenAtLocal = photo.TakenAt
|
i.db.Model(&lm).Association("LabelCategories").Append(sn)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
i.db.Unscoped().Save(&photo)
|
if plm.LabelUncertainty > label.Uncertainty {
|
||||||
} else {
|
plm.LabelUncertainty = label.Uncertainty
|
||||||
photo.PhotoFavorite = false
|
plm.LabelSource = label.Source
|
||||||
|
i.db.Save(&plm)
|
||||||
i.db.Create(&photo)
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
log.Infof("adding labels: %+v", labels)
|
|
||||||
|
func (i *Indexer) indexLocation (mediaFile *MediaFile, photo *models.Photo, keywords []string, labels Labels, fileChanged bool, o IndexerOptions) ([]string, Labels){
|
||||||
if fileChanged || o.UpdateLabels {
|
if location, err := mediaFile.Location(); err == nil {
|
||||||
for _, label := range labels {
|
i.db.FirstOrCreate(location, "id = ?", location.ID)
|
||||||
lm := models.NewLabel(label.Name, label.Priority).FirstOrCreate(i.db)
|
photo.Location = location
|
||||||
|
photo.LocationEstimated = false
|
||||||
if lm.LabelPriority != label.Priority {
|
|
||||||
lm.LabelPriority = label.Priority
|
photo.Country = models.NewCountry(location.LocCountryCode, location.LocCountry).FirstOrCreate(i.db)
|
||||||
i.db.Save(&lm)
|
|
||||||
}
|
if photo.Country.New {
|
||||||
|
event.Publish("count.countries", event.Data{
|
||||||
plm := models.NewPhotoLabel(photo.ID, lm.ID, label.Uncertainty, label.Source).FirstOrCreate(i.db)
|
"count": 1,
|
||||||
|
})
|
||||||
// Add categories
|
}
|
||||||
for _, category := range label.Categories {
|
|
||||||
sn := models.NewLabel(category, -1).FirstOrCreate(i.db)
|
keywords = append(keywords, util.Keywords(location.LocDisplayName)...)
|
||||||
i.db.Model(&lm).Association("LabelCategories").Append(sn)
|
|
||||||
}
|
// Append labels from OpenStreetMap
|
||||||
|
if location.LocCity != "" {
|
||||||
if plm.LabelUncertainty > label.Uncertainty {
|
labels = append(labels, NewLocationLabel(location.LocCity, 0, -2))
|
||||||
plm.LabelUncertainty = label.Uncertainty
|
}
|
||||||
plm.LabelSource = label.Source
|
|
||||||
i.db.Save(&plm)
|
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.Index(pathname, directory); i == 0 {
|
||||||
if i := strings.LastIndex(directory, string(os.PathSeparator)); i == len(directory)-1 {
|
if i := strings.LastIndex(directory, string(os.PathSeparator)); i == len(directory)-1 {
|
||||||
pathname = pathname[len(directory):]
|
pathname = pathname[len(directory):]
|
||||||
log.Info(pathname)
|
|
||||||
} else if i := strings.LastIndex(directory, string(os.PathSeparator)); i != len(directory) {
|
} else if i := strings.LastIndex(directory, string(os.PathSeparator)); i != len(directory) {
|
||||||
pathname = pathname[len(directory)+1:]
|
pathname = pathname[len(directory)+1:]
|
||||||
log.Info(pathname)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -51,11 +52,12 @@ func (t *TensorFlow) loadLabelRules() (err error) {
|
||||||
|
|
||||||
fileName := t.conf.ConfigPath() + "/labels.yml"
|
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) {
|
if !util.Exists(fileName) {
|
||||||
log.Errorf("label rules file not found: \"%s\"", fileName)
|
e := fmt.Errorf("tensorflow: label rules file not found in \"%s\"", filepath.Base(fileName))
|
||||||
return fmt.Errorf("label rules file not found: \"%s\"", fileName)
|
log.Error(e.Error())
|
||||||
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
yamlConfig, err := ioutil.ReadFile(fileName)
|
yamlConfig, err := ioutil.ReadFile(fileName)
|
||||||
|
@ -115,7 +117,9 @@ func (t *TensorFlow) Labels(img []byte) (result Labels, err error) {
|
||||||
// Return best labels
|
// Return best labels
|
||||||
result = t.bestLabels(output[0].Value().([][]float32)[0])
|
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
|
return result, nil
|
||||||
}
|
}
|
||||||
|
@ -123,7 +127,7 @@ func (t *TensorFlow) Labels(img []byte) (result Labels, err error) {
|
||||||
func (t *TensorFlow) loadLabels(path string) error {
|
func (t *TensorFlow) loadLabels(path string) error {
|
||||||
modelLabels := path + "/labels.txt"
|
modelLabels := path + "/labels.txt"
|
||||||
|
|
||||||
log.Infof("loading classification labels from \"%s\"", modelLabels)
|
log.Infof("tensorflow: loading classification labels from labels.txt")
|
||||||
|
|
||||||
// Load labels
|
// Load labels
|
||||||
f, err := os.Open(modelLabels)
|
f, err := os.Open(modelLabels)
|
||||||
|
@ -156,7 +160,7 @@ func (t *TensorFlow) loadModel() error {
|
||||||
|
|
||||||
path := t.conf.TensorFlowModelPath()
|
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
|
// Load model
|
||||||
model, err := tf.LoadSavedModel(path, []string{"photoprism"}, nil)
|
model, err := tf.LoadSavedModel(path, []string{"photoprism"}, nil)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
"github.com/photoprism/photoprism/internal/util"
|
"github.com/photoprism/photoprism/internal/util"
|
||||||
|
@ -93,6 +94,14 @@ func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string,
|
||||||
return nil
|
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 {
|
if err := mediaFile.CreateDefaultThumbnails(thumbnailsPath, force); err != nil {
|
||||||
log.Errorf("could not create default thumbnails: %s", err)
|
log.Errorf("could not create default thumbnails: %s", err)
|
||||||
return err
|
return err
|
||||||
|
|
Loading…
Reference in a new issue