mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
People: Add overview page with search and context menu #22
This commit is contained in:
@@ -146,6 +146,11 @@ export default class Config {
|
|||||||
this.values.people = [];
|
this.values.people = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data || !data.entities) {
|
||||||
|
console.warn("empty event data", ev, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "created":
|
case "created":
|
||||||
this.values.people.unshift(...data.entities);
|
this.values.people.unshift(...data.entities);
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import PAlbumClipboard from "./album/clipboard.vue";
|
|||||||
import PAlbumToolbar from "./album/toolbar.vue";
|
import PAlbumToolbar from "./album/toolbar.vue";
|
||||||
import PLabelClipboard from "./label/clipboard.vue";
|
import PLabelClipboard from "./label/clipboard.vue";
|
||||||
import PFileClipboard from "./file/clipboard.vue";
|
import PFileClipboard from "./file/clipboard.vue";
|
||||||
|
import PSubjectClipboard from "./subject/clipboard.vue";
|
||||||
import PAboutFooter from "./footer.vue";
|
import PAboutFooter from "./footer.vue";
|
||||||
|
|
||||||
const components = {};
|
const components = {};
|
||||||
@@ -63,6 +64,7 @@ components.install = (Vue) => {
|
|||||||
Vue.component("PAlbumToolbar", PAlbumToolbar);
|
Vue.component("PAlbumToolbar", PAlbumToolbar);
|
||||||
Vue.component("PLabelClipboard", PLabelClipboard);
|
Vue.component("PLabelClipboard", PLabelClipboard);
|
||||||
Vue.component("PFileClipboard", PFileClipboard);
|
Vue.component("PFileClipboard", PFileClipboard);
|
||||||
|
Vue.component("PSubjectClipboard", PSubjectClipboard);
|
||||||
Vue.component("PAboutFooter", PAboutFooter);
|
Vue.component("PAboutFooter", PAboutFooter);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,20 @@
|
|||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
|
<v-list-tile v-show="$config.feature('people')" :to="{ name: 'people' }" class="nav-people" @click.stop="">
|
||||||
|
<v-list-tile-action :title="$gettext('People')">
|
||||||
|
<v-icon>person</v-icon>
|
||||||
|
</v-list-tile-action>
|
||||||
|
|
||||||
|
<v-list-tile-content>
|
||||||
|
<v-list-tile-title>
|
||||||
|
<translate key="People">People</translate>
|
||||||
|
<span v-show="config.count.people > 0"
|
||||||
|
:class="`nav-count ${rtl ? '--rtl' : ''}`">{{ config.count.people }}</span>
|
||||||
|
</v-list-tile-title>
|
||||||
|
</v-list-tile-content>
|
||||||
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-tile v-if="isMini" v-show="$config.feature('places')" :to="{ name: 'places' }" class="nav-places"
|
<v-list-tile v-if="isMini" v-show="$config.feature('places')" :to="{ name: 'places' }" class="nav-places"
|
||||||
@click.stop="">
|
@click.stop="">
|
||||||
<v-list-tile-action :title="$gettext('Places')">
|
<v-list-tile-action :title="$gettext('Places')">
|
||||||
|
|||||||
121
frontend/src/component/subject/clipboard.vue
Normal file
121
frontend/src/component/subject/clipboard.vue
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-container v-if="selection.length > 0" fluid class="pa-0">
|
||||||
|
<v-speed-dial
|
||||||
|
id="t-clipboard" v-model="expanded"
|
||||||
|
fixed
|
||||||
|
bottom
|
||||||
|
direction="top"
|
||||||
|
transition="slide-y-reverse-transition"
|
||||||
|
:right="!rtl"
|
||||||
|
:left="rtl"
|
||||||
|
:class="`p-clipboard ${!rtl ? '--ltr' : '--rtl'} p-subject-clipboard`"
|
||||||
|
>
|
||||||
|
<template #activator>
|
||||||
|
<v-btn
|
||||||
|
fab
|
||||||
|
dark
|
||||||
|
color="accent darken-2"
|
||||||
|
class="action-menu"
|
||||||
|
>
|
||||||
|
<v-icon v-if="selection.length === 0">menu</v-icon>
|
||||||
|
<span v-else class="count-clipboard">{{ selection.length }}</span>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
v-if="features.download"
|
||||||
|
fab dark small
|
||||||
|
:title="$gettext('Download')"
|
||||||
|
color="download"
|
||||||
|
class="action-download"
|
||||||
|
:disabled="selection.length !== 1"
|
||||||
|
@click.stop="download()"
|
||||||
|
>
|
||||||
|
<v-icon>get_app</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
v-if="features.albums"
|
||||||
|
fab dark small
|
||||||
|
:title="$gettext('Add to album')"
|
||||||
|
color="album"
|
||||||
|
:disabled="selection.length === 0"
|
||||||
|
class="action-album"
|
||||||
|
@click.stop="dialog.album = true"
|
||||||
|
>
|
||||||
|
<v-icon>bookmark</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
fab dark small
|
||||||
|
color="accent"
|
||||||
|
class="action-clear"
|
||||||
|
@click.stop="clearClipboard()"
|
||||||
|
>
|
||||||
|
<v-icon>clear</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-speed-dial>
|
||||||
|
</v-container>
|
||||||
|
<p-photo-album-dialog :show="dialog.album" @cancel="dialog.album = false"
|
||||||
|
@confirm="addToAlbum"></p-photo-album-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Api from "common/api";
|
||||||
|
import Notify from "common/notify";
|
||||||
|
import download from "common/download";
|
||||||
|
import Photo from "../../model/photo";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PSubjectClipboard',
|
||||||
|
props: {
|
||||||
|
selection: Array,
|
||||||
|
refresh: Function,
|
||||||
|
clearSelection: Function,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
features: this.$config.settings().features,
|
||||||
|
expanded: false,
|
||||||
|
dialog: {
|
||||||
|
delete: false,
|
||||||
|
album: false,
|
||||||
|
edit: false,
|
||||||
|
},
|
||||||
|
rtl: this.$rtl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clearClipboard() {
|
||||||
|
this.clearSelection();
|
||||||
|
this.expanded = false;
|
||||||
|
},
|
||||||
|
addToAlbum(ppid) {
|
||||||
|
this.dialog.album = false;
|
||||||
|
|
||||||
|
Api.post(`albums/${ppid}/photos`, {"subjects": this.selection}).then(() => this.onAdded());
|
||||||
|
},
|
||||||
|
onAdded() {
|
||||||
|
this.clearClipboard();
|
||||||
|
},
|
||||||
|
download() {
|
||||||
|
if (this.selection.length !== 1) {
|
||||||
|
Notify.error(this.$gettext("You can only download one album"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Notify.success(this.$gettext("Downloading…"));
|
||||||
|
|
||||||
|
Api.post("zip", {"subjects": this.selection}).then(r => {
|
||||||
|
this.onDownload(`${this.$config.apiUri}/zip/${r.data.filename}?t=${this.$config.downloadToken()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.expanded = false;
|
||||||
|
},
|
||||||
|
onDownload(path) {
|
||||||
|
download(path, "photos.zip");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
:transition="false"
|
:transition="false"
|
||||||
aspect-ratio="1"
|
aspect-ratio="1"
|
||||||
class="accent lighten-2">
|
class="accent lighten-2">
|
||||||
<v-btn v-if="!marker.SubjectUID && !marker.Invalid" :ripple="false" :depressed="false" class="input-reject"
|
<v-btn v-if="!marker.SubjUID && !marker.Invalid" :ripple="false" :depressed="false" class="input-reject"
|
||||||
icon flat small absolute :title="$gettext('Remove')"
|
icon flat small absolute :title="$gettext('Remove')"
|
||||||
@click.stop.prevent="onReject(marker)">
|
@click.stop.prevent="onReject(marker)">
|
||||||
<v-icon color="white" class="action-reject">clear</v-icon>
|
<v-icon color="white" class="action-reject">clear</v-icon>
|
||||||
@@ -43,11 +43,11 @@
|
|||||||
large depressed block :round="false"
|
large depressed block :round="false"
|
||||||
class="action-approve text-xs-center"
|
class="action-approve text-xs-center"
|
||||||
:title="$gettext('Approve')" @click.stop="onApprove(marker)">
|
:title="$gettext('Approve')" @click.stop="onApprove(marker)">
|
||||||
<v-icon dark>check</v-icon>
|
<v-icon dark>undo</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
<v-layout v-else-if="marker.SubjectUID" row wrap align-center>
|
<v-layout v-else-if="marker.SubjUID" row wrap align-center>
|
||||||
<v-flex xs12 class="text-xs-left pa-0">
|
<v-flex xs12 class="text-xs-left pa-0">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="marker.Name"
|
v-model="marker.Name"
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ export class Marker extends RestModel {
|
|||||||
H: 0.0,
|
H: 0.0,
|
||||||
CropID: "",
|
CropID: "",
|
||||||
FaceID: "",
|
FaceID: "",
|
||||||
SubjectSrc: "",
|
SubjSrc: "",
|
||||||
SubjectUID: "",
|
SubjUID: "",
|
||||||
Score: 0,
|
Score: 0,
|
||||||
Size: 0,
|
Size: 0,
|
||||||
};
|
};
|
||||||
@@ -118,9 +118,9 @@ export class Marker extends RestModel {
|
|||||||
return Promise.resolve(this);
|
return Promise.resolve(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.SubjectSrc = src.Manual;
|
this.SubjSrc = src.Manual;
|
||||||
|
|
||||||
const payload = { SubjectSrc: this.SubjectSrc, Name: this.Name };
|
const payload = { SubjSrc: this.SubjSrc, Name: this.Name };
|
||||||
|
|
||||||
return Api.put(this.getEntityResource(), payload).then((resp) =>
|
return Api.put(this.getEntityResource(), payload).then((resp) =>
|
||||||
Promise.resolve(this.setValues(resp.data))
|
Promise.resolve(this.setValues(resp.data))
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<v-toolbar flat color="secondary" :dense="$vuetify.breakpoint.smAndDown">
|
|
||||||
<v-toolbar-title>
|
|
||||||
<translate>Not implemented yet</translate>
|
|
||||||
</v-toolbar-title>
|
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-toolbar>
|
|
||||||
|
|
||||||
<v-container>
|
|
||||||
<p>
|
|
||||||
Issues labeled <a href="https://github.com/photoprism/photoprism/labels/help%20wanted">help wanted</a> /
|
|
||||||
<a href="https://github.com/photoprism/photoprism/labels/easy">easy</a> can be good (first)
|
|
||||||
contributions.
|
|
||||||
Our <a href="https://github.com/photoprism/photoprism/wiki">Developer Guide</a> contains all information
|
|
||||||
necessary to get you started.
|
|
||||||
</p>
|
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'People',
|
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
methods: {}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -163,7 +163,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
viewType() {
|
viewType() {
|
||||||
let queryParam = this.$route.query['view'];
|
let queryParam = this.$route.query["view"];
|
||||||
let storedType = window.localStorage.getItem("photo_view");
|
let storedType = window.localStorage.getItem("photo_view");
|
||||||
|
|
||||||
if (queryParam) {
|
if (queryParam) {
|
||||||
@@ -178,7 +178,7 @@ export default {
|
|||||||
return 'cards';
|
return 'cards';
|
||||||
},
|
},
|
||||||
sortOrder() {
|
sortOrder() {
|
||||||
let queryParam = this.$route.query['order'];
|
let queryParam = this.$route.query["order"];
|
||||||
let storedType = window.localStorage.getItem("photo_order");
|
let storedType = window.localStorage.getItem("photo_order");
|
||||||
|
|
||||||
if (queryParam) {
|
if (queryParam) {
|
||||||
@@ -188,7 +188,7 @@ export default {
|
|||||||
return storedType;
|
return storedType;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'newest';
|
return "newest";
|
||||||
},
|
},
|
||||||
openLocation(index) {
|
openLocation(index) {
|
||||||
const photo = this.results[index];
|
const photo = this.results[index];
|
||||||
|
|||||||
555
frontend/src/pages/subjects.vue
Normal file
555
frontend/src/pages/subjects.vue
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
<template>
|
||||||
|
<div v-infinite-scroll="loadMore" class="p-page p-page-subjects" style="user-select: none"
|
||||||
|
:infinite-scroll-disabled="scrollDisabled" :infinite-scroll-distance="1200"
|
||||||
|
:infinite-scroll-listen-for-event="'scrollRefresh'">
|
||||||
|
|
||||||
|
<v-form ref="form" class="p-people-search" lazy-validation dense @submit.prevent="updateQuery">
|
||||||
|
<v-toolbar flat color="secondary" :dense="$vuetify.breakpoint.smAndDown">
|
||||||
|
<v-text-field id="search"
|
||||||
|
v-model="filter.q"
|
||||||
|
class="pt-3 pr-3 input-search"
|
||||||
|
single-line
|
||||||
|
:label="$gettext('Search')"
|
||||||
|
prepend-inner-icon="search"
|
||||||
|
browser-autocomplete="off"
|
||||||
|
clearable
|
||||||
|
color="secondary-dark"
|
||||||
|
@click:clear="clearQuery"
|
||||||
|
@keyup.enter.native="updateQuery"
|
||||||
|
></v-text-field>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-btn icon class="action-reload" :title="$gettext('Reload')" @click.stop="refresh">
|
||||||
|
<v-icon>refresh</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
</v-form>
|
||||||
|
|
||||||
|
<v-container v-if="loading" fluid class="pa-4">
|
||||||
|
<v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear>
|
||||||
|
</v-container>
|
||||||
|
<v-container v-else fluid class="pa-0">
|
||||||
|
<p-subject-clipboard :refresh="refresh" :selection="selection"
|
||||||
|
:clear-selection="clearSelection"></p-subject-clipboard>
|
||||||
|
|
||||||
|
<p-scroll-top></p-scroll-top>
|
||||||
|
|
||||||
|
<v-container grid-list-xs fluid class="pa-2">
|
||||||
|
<v-card v-if="results.length === 0" class="no-results secondary-light lighten-1 ma-1" flat>
|
||||||
|
<v-card-title primary-title>
|
||||||
|
<div>
|
||||||
|
<h3 class="title ma-0 pa-0">
|
||||||
|
<translate>Couldn't find anything</translate>
|
||||||
|
</h3>
|
||||||
|
<p class="mt-4 mb-0 pa-0">
|
||||||
|
<translate>Try again using other filters or keywords.</translate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
</v-card>
|
||||||
|
<v-layout row wrap class="search-results subject-results cards-view" :class="{'select-results': selection.length > 0}">
|
||||||
|
<v-flex
|
||||||
|
v-for="(model, index) in results"
|
||||||
|
:key="index"
|
||||||
|
xs6 sm4 md3 lg2 xxl1 d-flex
|
||||||
|
>
|
||||||
|
<v-card tile
|
||||||
|
:data-uid="model.UID"
|
||||||
|
style="user-select: none"
|
||||||
|
class="result accent lighten-3"
|
||||||
|
:class="model.classes(selection.includes(model.UID))"
|
||||||
|
:to="model.route(view)"
|
||||||
|
@contextmenu.stop="onContextMenu($event, index)"
|
||||||
|
>
|
||||||
|
<div class="card-background accent lighten-3"></div>
|
||||||
|
<v-img
|
||||||
|
:src="model.thumbnailUrl('tile_500')"
|
||||||
|
:alt="model.Name"
|
||||||
|
:transition="false"
|
||||||
|
aspect-ratio="1"
|
||||||
|
style="user-select: none"
|
||||||
|
class="accent lighten-2 clickable"
|
||||||
|
@touchstart="input.touchStart($event, index)"
|
||||||
|
@touchend.prevent="onClick($event, index)"
|
||||||
|
@mousedown="input.mouseDown($event, index)"
|
||||||
|
@click.stop.prevent="onClick($event, index)"
|
||||||
|
>
|
||||||
|
<v-btn :ripple="false"
|
||||||
|
icon flat absolute
|
||||||
|
class="input-select"
|
||||||
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
|
@touchend.stop.prevent="onSelect($event, index)"
|
||||||
|
@touchmove.stop.prevent
|
||||||
|
@click.stop.prevent="onSelect($event, index)">
|
||||||
|
<v-icon color="white" class="select-on">check_circle</v-icon>
|
||||||
|
<v-icon color="white" class="select-off">radio_button_off</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn :ripple="false"
|
||||||
|
icon flat absolute
|
||||||
|
class="input-favorite"
|
||||||
|
@touchstart.stop.prevent="input.touchStart($event, index)"
|
||||||
|
@touchend.stop.prevent="toggleLike($event, index)"
|
||||||
|
@touchmove.stop.prevent
|
||||||
|
@click.stop.prevent="toggleLike($event, index)">
|
||||||
|
<v-icon color="#FFD600" class="select-on">star</v-icon>
|
||||||
|
<v-icon color="white" class="select-off">star_border</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-img>
|
||||||
|
|
||||||
|
<v-card-title primary-title class="pa-3 card-details" style="user-select: none;" @click.stop.prevent="">
|
||||||
|
<v-edit-dialog
|
||||||
|
:return-value.sync="model.Name"
|
||||||
|
lazy
|
||||||
|
class="inline-edit"
|
||||||
|
@save="onSave(model)"
|
||||||
|
>
|
||||||
|
<span v-if="model.Name" class="body-2 ma-0">
|
||||||
|
{{ model.Name }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<v-icon>edit</v-icon>
|
||||||
|
</span>
|
||||||
|
<template #input>
|
||||||
|
<v-text-field
|
||||||
|
v-model="model.Name"
|
||||||
|
:rules="[titleRule]"
|
||||||
|
:label="$gettext('Name')"
|
||||||
|
color="secondary-dark"
|
||||||
|
single-line
|
||||||
|
autofocus
|
||||||
|
></v-text-field>
|
||||||
|
</template>
|
||||||
|
</v-edit-dialog>
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text primary-title class="pb-2 pt-0 card-details" style="user-select: none;"
|
||||||
|
@click.stop.prevent="">
|
||||||
|
<div v-if="model.Bio" class="caption mb-2" :title="$gettext('Bio')">
|
||||||
|
{{ model.Bio | truncate(100) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="caption mb-2">
|
||||||
|
<button v-if="model.Files === 1">
|
||||||
|
<translate>Contains one entry.</translate>
|
||||||
|
</button>
|
||||||
|
<button v-else-if="model.Files > 0">
|
||||||
|
<translate :translate-params="{n: model.Files}">Contains %{n} entries.</translate>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
</v-container>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Subject from "model/subject";
|
||||||
|
import Event from "pubsub-js";
|
||||||
|
import RestModel from "model/rest";
|
||||||
|
import {MaxItems} from "common/clipboard";
|
||||||
|
import Notify from "common/notify";
|
||||||
|
import {Input, InputInvalid, ClickShort, ClickLong} from "common/input";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PPageSubjects',
|
||||||
|
props: {
|
||||||
|
staticFilter: Object
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const query = this.$route.query;
|
||||||
|
const routeName = this.$route.name;
|
||||||
|
const q = query['q'] ? query['q'] : '';
|
||||||
|
const all = query['all'] ? query['all'] : '';
|
||||||
|
const order = this.sortOrder();
|
||||||
|
const filter = {q: q, all: all, order: order};
|
||||||
|
const settings = {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
view: 'all',
|
||||||
|
config: this.$config.values,
|
||||||
|
subscriptions: [],
|
||||||
|
listen: false,
|
||||||
|
dirty: false,
|
||||||
|
results: [],
|
||||||
|
scrollDisabled: true,
|
||||||
|
loading: true,
|
||||||
|
batchSize: Subject.batchSize(),
|
||||||
|
offset: 0,
|
||||||
|
page: 0,
|
||||||
|
selection: [],
|
||||||
|
settings: settings,
|
||||||
|
filter: filter,
|
||||||
|
lastFilter: {},
|
||||||
|
routeName: routeName,
|
||||||
|
titleRule: v => v.length <= this.$config.get("clip") || this.$gettext("Name too long"),
|
||||||
|
input: new Input(),
|
||||||
|
lastId: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$route'() {
|
||||||
|
const query = this.$route.query;
|
||||||
|
|
||||||
|
this.filter.q = query["q"] ? query["q"] : "";
|
||||||
|
this.filter.all = query["all"] ? query["all"] : "";
|
||||||
|
this.filter.order = this.sortOrder();
|
||||||
|
this.lastFilter = {};
|
||||||
|
this.routeName = this.$route.name;
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.search();
|
||||||
|
|
||||||
|
this.subscriptions.push(Event.subscribe("subjects", (ev, data) => this.onUpdate(ev, data)));
|
||||||
|
|
||||||
|
this.subscriptions.push(Event.subscribe("touchmove.top", () => this.refresh()));
|
||||||
|
this.subscriptions.push(Event.subscribe("touchmove.bottom", () => this.loadMore()));
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
for (let i = 0; i < this.subscriptions.length; i++) {
|
||||||
|
Event.unsubscribe(this.subscriptions[i]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
searchCount() {
|
||||||
|
const offset = parseInt(window.localStorage.getItem("subjects_offset"));
|
||||||
|
|
||||||
|
if(this.offset > 0 || !offset) {
|
||||||
|
return this.batchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset + this.batchSize;
|
||||||
|
},
|
||||||
|
sortOrder() {
|
||||||
|
return "relevance";
|
||||||
|
},
|
||||||
|
setOffset(offset) {
|
||||||
|
this.offset = offset;
|
||||||
|
window.localStorage.setItem("subjects_offset", offset);
|
||||||
|
},
|
||||||
|
toggleLike(ev, index) {
|
||||||
|
const inputType = this.input.eval(ev, index);
|
||||||
|
|
||||||
|
if (inputType !== ClickShort) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = this.results[index];
|
||||||
|
|
||||||
|
if (!m) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m.toggleLike();
|
||||||
|
},
|
||||||
|
selectRange(rangeEnd, models) {
|
||||||
|
if (!models || !models[rangeEnd] || !(models[rangeEnd] instanceof RestModel)) {
|
||||||
|
console.warn("selectRange() - invalid arguments:", rangeEnd, models);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rangeStart = models.findIndex((m) => m.getId() === this.lastId);
|
||||||
|
|
||||||
|
if (rangeStart === -1) {
|
||||||
|
this.toggleSelection(models[rangeEnd].getId());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeStart > rangeEnd) {
|
||||||
|
const newEnd = rangeStart;
|
||||||
|
rangeStart = rangeEnd;
|
||||||
|
rangeEnd = newEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||||||
|
this.addSelection(models[i].getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (rangeEnd - rangeStart) + 1;
|
||||||
|
},
|
||||||
|
onSelect(ev, index) {
|
||||||
|
const inputType = this.input.eval(ev, index);
|
||||||
|
|
||||||
|
if (inputType !== ClickShort) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.shiftKey) {
|
||||||
|
this.selectRange(index, this.results);
|
||||||
|
} else {
|
||||||
|
this.toggleSelection(this.results[index].getId());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick(ev, index) {
|
||||||
|
const inputType = this.input.eval(ev, index);
|
||||||
|
const longClick = inputType === ClickLong;
|
||||||
|
|
||||||
|
if (inputType === InputInvalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (longClick || this.selection.length > 0) {
|
||||||
|
if (longClick || ev.shiftKey) {
|
||||||
|
this.selectRange(index, this.results);
|
||||||
|
} else {
|
||||||
|
this.toggleSelection(this.results[index].getId());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$router.push(this.results[index].route(this.view));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onContextMenu(ev, index) {
|
||||||
|
if (this.$isMobile) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
if (this.results[index]) {
|
||||||
|
this.selectRange(index, this.results);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSave(m) {
|
||||||
|
m.update();
|
||||||
|
},
|
||||||
|
showAll() {
|
||||||
|
this.filter.all = "true";
|
||||||
|
this.updateQuery();
|
||||||
|
},
|
||||||
|
showImportant() {
|
||||||
|
this.filter.all = "";
|
||||||
|
this.updateQuery();
|
||||||
|
},
|
||||||
|
clearQuery() {
|
||||||
|
this.filter.q = '';
|
||||||
|
this.updateQuery();
|
||||||
|
},
|
||||||
|
addSelection(uid) {
|
||||||
|
const pos = this.selection.indexOf(uid);
|
||||||
|
|
||||||
|
if (pos === -1) {
|
||||||
|
if (this.selection.length >= MaxItems) {
|
||||||
|
Notify.warn(this.$gettext("Can't select more items"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selection.push(uid);
|
||||||
|
this.lastId = uid;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleSelection(uid) {
|
||||||
|
const pos = this.selection.indexOf(uid);
|
||||||
|
|
||||||
|
if (pos !== -1) {
|
||||||
|
this.selection.splice(pos, 1);
|
||||||
|
this.lastId = "";
|
||||||
|
} else {
|
||||||
|
if (this.selection.length >= MaxItems) {
|
||||||
|
Notify.warn(this.$gettext("Can't select more items"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selection.push(uid);
|
||||||
|
this.lastId = uid;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeSelection(uid) {
|
||||||
|
const pos = this.selection.indexOf(uid);
|
||||||
|
|
||||||
|
if (pos !== -1) {
|
||||||
|
this.selection.splice(pos, 1);
|
||||||
|
this.lastId = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearSelection() {
|
||||||
|
this.selection.splice(0, this.selection.length);
|
||||||
|
this.lastId = "";
|
||||||
|
},
|
||||||
|
loadMore() {
|
||||||
|
if (this.scrollDisabled) return;
|
||||||
|
|
||||||
|
this.scrollDisabled = true;
|
||||||
|
this.listen = false;
|
||||||
|
|
||||||
|
const count = this.dirty ? (this.page + 2) * this.batchSize : this.batchSize;
|
||||||
|
const offset = this.dirty ? 0 : this.offset;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
count: count,
|
||||||
|
offset: offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(params, this.lastFilter);
|
||||||
|
|
||||||
|
if (this.staticFilter) {
|
||||||
|
Object.assign(params, this.staticFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Subject.search(params).then(resp => {
|
||||||
|
this.results = this.dirty ? resp.models : this.results.concat(resp.models);
|
||||||
|
|
||||||
|
this.scrollDisabled = (resp.count < resp.limit);
|
||||||
|
|
||||||
|
if (this.scrollDisabled) {
|
||||||
|
this.setOffset(resp.offset);
|
||||||
|
if (this.results.length > 1) {
|
||||||
|
this.$notify.info(this.$gettextInterpolate(this.$gettext("All %{n} people loaded"), {n: this.results.length}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setOffset(resp.offset + resp.limit);
|
||||||
|
this.page++;
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$root.$el.clientHeight <= window.document.documentElement.clientHeight + 300) {
|
||||||
|
this.$emit("scrollRefresh");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.scrollDisabled = false;
|
||||||
|
}).finally(() => {
|
||||||
|
this.dirty = false;
|
||||||
|
this.loading = false;
|
||||||
|
this.listen = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateQuery() {
|
||||||
|
this.filter.q = this.filter.q.trim();
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
view: this.settings.view
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(query, this.filter);
|
||||||
|
|
||||||
|
for (let key in query) {
|
||||||
|
if (query[key] === undefined || !query[key]) {
|
||||||
|
delete query[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JSON.stringify(this.$route.query) === JSON.stringify(query)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$router.replace({query: query});
|
||||||
|
},
|
||||||
|
searchParams() {
|
||||||
|
const params = {
|
||||||
|
count: this.searchCount(),
|
||||||
|
offset: this.offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(params, this.filter);
|
||||||
|
|
||||||
|
if (this.staticFilter) {
|
||||||
|
Object.assign(params, this.staticFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
refresh() {
|
||||||
|
if (this.loading) return;
|
||||||
|
this.loading = true;
|
||||||
|
this.page = 0;
|
||||||
|
this.dirty = true;
|
||||||
|
this.scrollDisabled = false;
|
||||||
|
this.loadMore();
|
||||||
|
},
|
||||||
|
search() {
|
||||||
|
this.scrollDisabled = true;
|
||||||
|
|
||||||
|
// Don't query the same data more than once
|
||||||
|
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) {
|
||||||
|
this.$nextTick(() => this.$emit("scrollRefresh"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this.lastFilter, this.filter);
|
||||||
|
|
||||||
|
this.offset = 0;
|
||||||
|
this.page = 0;
|
||||||
|
this.loading = true;
|
||||||
|
this.listen = false;
|
||||||
|
|
||||||
|
const params = this.searchParams();
|
||||||
|
|
||||||
|
Subject.search(params).then(resp => {
|
||||||
|
this.offset = resp.limit;
|
||||||
|
this.results = resp.models;
|
||||||
|
|
||||||
|
this.scrollDisabled = (resp.count < resp.limit);
|
||||||
|
|
||||||
|
if (this.scrollDisabled) {
|
||||||
|
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} people found"), {n: this.results.length}));
|
||||||
|
} else {
|
||||||
|
this.$notify.info(this.$gettext('More than 20 people found'));
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$root.$el.clientHeight <= window.document.documentElement.clientHeight + 300) {
|
||||||
|
this.$emit("scrollRefresh");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.dirty = false;
|
||||||
|
this.loading = false;
|
||||||
|
this.listen = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onUpdate(ev, data) {
|
||||||
|
if (!this.listen) return;
|
||||||
|
|
||||||
|
if (!data || !data.entities) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("onUpdate", ev, data);
|
||||||
|
|
||||||
|
const type = ev.split('.')[1];
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'updated':
|
||||||
|
for (let i = 0; i < data.entities.length; i++) {
|
||||||
|
const values = data.entities[i];
|
||||||
|
const model = this.results.find((m) => m.UID === values.UID);
|
||||||
|
|
||||||
|
if (model) {
|
||||||
|
for (let key in values) {
|
||||||
|
if (values.hasOwnProperty(key) && values[key] != null && typeof values[key] !== "object") {
|
||||||
|
model[key] = values[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'deleted':
|
||||||
|
this.dirty = true;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.entities.length; i++) {
|
||||||
|
const uid = data.entities[i];
|
||||||
|
const index = this.results.findIndex((m) => m.UID === uid);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.results.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.removeSelection(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'created':
|
||||||
|
this.dirty = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn("unexpected event type", ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -35,7 +35,7 @@ import Places from "pages/places.vue";
|
|||||||
import Files from "pages/library/files.vue";
|
import Files from "pages/library/files.vue";
|
||||||
import Errors from "pages/library/errors.vue";
|
import Errors from "pages/library/errors.vue";
|
||||||
import Labels from "pages/labels.vue";
|
import Labels from "pages/labels.vue";
|
||||||
import People from "pages/people.vue";
|
import Subjects from "pages/subjects.vue";
|
||||||
import Library from "pages/library.vue";
|
import Library from "pages/library.vue";
|
||||||
import Settings from "pages/settings.vue";
|
import Settings from "pages/settings.vue";
|
||||||
import Login from "pages/login.vue";
|
import Login from "pages/login.vue";
|
||||||
@@ -261,7 +261,7 @@ export default [
|
|||||||
{
|
{
|
||||||
name: "people",
|
name: "people",
|
||||||
path: "/people",
|
path: "/people",
|
||||||
component: People,
|
component: Subjects,
|
||||||
meta: { title: $gettext("People"), auth: true },
|
meta: { title: $gettext("People"), auth: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,3 +53,16 @@ func PublishLabelEvent(e EntityEvent, uid string, c *gin.Context) {
|
|||||||
|
|
||||||
event.PublishEntities("labels", string(e), result)
|
event.PublishEntities("labels", string(e), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PublishSubjectEvent(e EntityEvent, uid string, c *gin.Context) {
|
||||||
|
f := form.SubjectSearch{ID: uid}
|
||||||
|
result, err := query.SubjectSearch(f)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
AbortUnexpected(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.PublishEntities("subjects", string(e), result)
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import (
|
|||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetLabels finds and returns labels as JSON.
|
||||||
|
//
|
||||||
// GET /api/v1/labels
|
// GET /api/v1/labels
|
||||||
func GetLabels(router *gin.RouterGroup) {
|
func GetLabels(router *gin.RouterGroup) {
|
||||||
router.GET("/labels", func(c *gin.Context) {
|
router.GET("/labels", func(c *gin.Context) {
|
||||||
@@ -49,6 +51,8 @@ func GetLabels(router *gin.RouterGroup) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateLabel updates label properties.
|
||||||
|
//
|
||||||
// PUT /api/v1/labels/:uid
|
// PUT /api/v1/labels/:uid
|
||||||
func UpdateLabel(router *gin.RouterGroup) {
|
func UpdateLabel(router *gin.RouterGroup) {
|
||||||
router.PUT("/labels/:uid", func(c *gin.Context) {
|
router.PUT("/labels/:uid", func(c *gin.Context) {
|
||||||
@@ -85,6 +89,8 @@ func UpdateLabel(router *gin.RouterGroup) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LikeLabel flags a label as favorite.
|
||||||
|
//
|
||||||
// POST /api/v1/labels/:uid/like
|
// POST /api/v1/labels/:uid/like
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
@@ -123,6 +129,8 @@ func LikeLabel(router *gin.RouterGroup) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DislikeLabel removes the favorite flag from a label.
|
||||||
|
//
|
||||||
// DELETE /api/v1/labels/:uid/like
|
// DELETE /api/v1/labels/:uid/like
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func UpdateMarker(router *gin.RouterGroup) {
|
|||||||
log.Errorf("photo: %s (save marker form)", err)
|
log.Errorf("photo: %s (save marker form)", err)
|
||||||
AbortSaveFailed(c)
|
AbortSaveFailed(c)
|
||||||
return
|
return
|
||||||
} else if marker.SubjectUID != "" && marker.SubjectSrc == entity.SrcManual && marker.FaceID != "" {
|
} else if marker.SubjUID != "" && marker.SubjSrc == entity.SrcManual && marker.FaceID != "" {
|
||||||
if res, err := service.Faces().Optimize(); err != nil {
|
if res, err := service.Faces().Optimize(); err != nil {
|
||||||
log.Errorf("faces: %s (optimize)", err)
|
log.Errorf("faces: %s (optimize)", err)
|
||||||
} else if res.Merged > 0 {
|
} else if res.Merged > 0 {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func TestUpdateMarker(t *testing.T) {
|
|||||||
u := fmt.Sprintf("/api/v1/markers/%s", markerUID)
|
u := fmt.Sprintf("/api/v1/markers/%s", markerUID)
|
||||||
|
|
||||||
var m = form.Marker{
|
var m = form.Marker{
|
||||||
SubjectSrc: "manual",
|
SubjSrc: "manual",
|
||||||
MarkerInvalid: true,
|
MarkerInvalid: true,
|
||||||
MarkerName: "Foo",
|
MarkerName: "Foo",
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ func TestUpdateMarker(t *testing.T) {
|
|||||||
UpdateMarker(router)
|
UpdateMarker(router)
|
||||||
|
|
||||||
var m = form.Marker{
|
var m = form.Marker{
|
||||||
SubjectSrc: "manual",
|
SubjSrc: "manual",
|
||||||
MarkerInvalid: false,
|
MarkerInvalid: false,
|
||||||
MarkerName: "Actress A",
|
MarkerName: "Actress A",
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ func TestUpdateMarker(t *testing.T) {
|
|||||||
UpdateMarker(router)
|
UpdateMarker(router)
|
||||||
|
|
||||||
var m = form.Marker{
|
var m = form.Marker{
|
||||||
SubjectSrc: "manual",
|
SubjSrc: "manual",
|
||||||
MarkerInvalid: false,
|
MarkerInvalid: false,
|
||||||
MarkerName: "Actress A",
|
MarkerName: "Actress A",
|
||||||
}
|
}
|
||||||
@@ -150,20 +150,20 @@ func TestUpdateMarker(t *testing.T) {
|
|||||||
UpdateMarker(router)
|
UpdateMarker(router)
|
||||||
|
|
||||||
var m = struct {
|
var m = struct {
|
||||||
ID int
|
ID int
|
||||||
Type string
|
Type string
|
||||||
Src int
|
Src int
|
||||||
Name int
|
Name int
|
||||||
SubjectUID string
|
SubjUID string
|
||||||
SubjectSrc string
|
SubjSrc string
|
||||||
FaceID string
|
FaceID string
|
||||||
}{ID: 8,
|
}{ID: 8,
|
||||||
Type: "face",
|
Type: "face",
|
||||||
Src: 123,
|
Src: 123,
|
||||||
Name: 456,
|
Name: 456,
|
||||||
SubjectUID: "jqy1y111h1njaaac",
|
SubjUID: "jqy1y111h1njaaac",
|
||||||
SubjectSrc: "manual",
|
SubjSrc: "manual",
|
||||||
FaceID: "GMH5NISEEULNJL6RATITOA3TMZXMTMCI"}
|
FaceID: "GMH5NISEEULNJL6RATITOA3TMZXMTMCI"}
|
||||||
if b, err := json.Marshal(m); err != nil {
|
if b, err := json.Marshal(m); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
"github.com/photoprism/photoprism/internal/acl"
|
"github.com/photoprism/photoprism/internal/acl"
|
||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
|
"github.com/photoprism/photoprism/internal/event"
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
"github.com/photoprism/photoprism/internal/i18n"
|
"github.com/photoprism/photoprism/internal/i18n"
|
||||||
"github.com/photoprism/photoprism/internal/query"
|
"github.com/photoprism/photoprism/internal/query"
|
||||||
@@ -68,6 +69,111 @@ func GetSubject(router *gin.RouterGroup) {
|
|||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusOK, subj)
|
c.JSON(http.StatusOK, subj)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSubject updates subject properties.
|
||||||
|
//
|
||||||
|
// PUT /api/v1/subjects/:uid
|
||||||
|
func UpdateSubject(router *gin.RouterGroup) {
|
||||||
|
router.PUT("/subjects/:uid", func(c *gin.Context) {
|
||||||
|
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionUpdate)
|
||||||
|
|
||||||
|
if s.Invalid() {
|
||||||
|
AbortUnauthorized(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var f form.Subject
|
||||||
|
|
||||||
|
if err := c.BindJSON(&f); err != nil {
|
||||||
|
AbortBadRequest(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.Param("uid")
|
||||||
|
m := entity.FindSubject(uid)
|
||||||
|
|
||||||
|
if m == nil {
|
||||||
|
Abort(c, http.StatusNotFound, i18n.ErrSubjectNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := m.UpdateName(f.SubjName); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.SuccessMsg(i18n.MsgSubjectSaved)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, m)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LikeSubject flags a subject as favorite.
|
||||||
|
//
|
||||||
|
// POST /api/v1/subjects/:uid/like
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// uid: string Subject UID
|
||||||
|
func LikeSubject(router *gin.RouterGroup) {
|
||||||
|
router.POST("/subjects/:uid/like", func(c *gin.Context) {
|
||||||
|
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionUpdate)
|
||||||
|
|
||||||
|
if s.Invalid() {
|
||||||
|
AbortUnauthorized(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.Param("uid")
|
||||||
|
subj := entity.FindSubject(uid)
|
||||||
|
|
||||||
|
if subj == nil {
|
||||||
|
Abort(c, http.StatusNotFound, i18n.ErrSubjectNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := subj.Update("SubjFavorite", true); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishSubjectEvent(EntityUpdated, uid, c)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, http.Response{})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DislikeSubject removes the favorite flag from a subject.
|
||||||
|
//
|
||||||
|
// DELETE /api/v1/subjects/:uid/like
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// uid: string Subject UID
|
||||||
|
func DislikeSubject(router *gin.RouterGroup) {
|
||||||
|
router.DELETE("/subjects/:uid/like", func(c *gin.Context) {
|
||||||
|
s := Auth(SessionID(c), acl.ResourceSubjects, acl.ActionUpdate)
|
||||||
|
|
||||||
|
if s.Invalid() {
|
||||||
|
AbortUnauthorized(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.Param("uid")
|
||||||
|
subj := entity.FindSubject(uid)
|
||||||
|
|
||||||
|
if subj == nil {
|
||||||
|
Abort(c, http.StatusNotFound, i18n.ErrSubjectNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := subj.Update("SubjFavorite", false); err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishSubjectEvent(EntityUpdated, uid, c)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, http.Response{})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ var DeprecatedTables = Deprecated{
|
|||||||
"subjects_dev5",
|
"subjects_dev5",
|
||||||
"subjects_dev6",
|
"subjects_dev6",
|
||||||
"subjects_dev7",
|
"subjects_dev7",
|
||||||
|
"subjects_dev8",
|
||||||
"markers_dev1",
|
"markers_dev1",
|
||||||
"markers_dev2",
|
"markers_dev2",
|
||||||
"markers_dev3",
|
"markers_dev3",
|
||||||
@@ -28,6 +29,7 @@ var DeprecatedTables = Deprecated{
|
|||||||
"markers_dev5",
|
"markers_dev5",
|
||||||
"markers_dev6",
|
"markers_dev6",
|
||||||
"markers_dev7",
|
"markers_dev7",
|
||||||
|
"markers_dev8",
|
||||||
"faces_dev1",
|
"faces_dev1",
|
||||||
"faces_dev2",
|
"faces_dev2",
|
||||||
"faces_dev3",
|
"faces_dev3",
|
||||||
@@ -35,4 +37,5 @@ var DeprecatedTables = Deprecated{
|
|||||||
"faces_dev5",
|
"faces_dev5",
|
||||||
"faces_dev6",
|
"faces_dev6",
|
||||||
"faces_dev7",
|
"faces_dev7",
|
||||||
|
"faces_dev8",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ var faceMutex = sync.Mutex{}
|
|||||||
type Face struct {
|
type Face struct {
|
||||||
ID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
|
ID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
|
||||||
FaceSrc string `gorm:"type:VARBINARY(8);" json:"Src" yaml:"Src,omitempty"`
|
FaceSrc string `gorm:"type:VARBINARY(8);" json:"Src" yaml:"Src,omitempty"`
|
||||||
SubjectUID string `gorm:"type:VARBINARY(42);index;" json:"SubjectUID" yaml:"SubjectUID,omitempty"`
|
SubjUID string `gorm:"type:VARBINARY(42);index;" json:"SubjUID" yaml:"SubjUID,omitempty"`
|
||||||
Samples int `json:"Samples" yaml:"Samples,omitempty"`
|
Samples int `json:"Samples" yaml:"Samples,omitempty"`
|
||||||
SampleRadius float64 `json:"SampleRadius" yaml:"SampleRadius,omitempty"`
|
SampleRadius float64 `json:"SampleRadius" yaml:"SampleRadius,omitempty"`
|
||||||
Collisions int `json:"Collisions" yaml:"Collisions,omitempty"`
|
Collisions int `json:"Collisions" yaml:"Collisions,omitempty"`
|
||||||
@@ -38,14 +38,14 @@ var Faceless = []string{""}
|
|||||||
|
|
||||||
// TableName returns the entity database table name.
|
// TableName returns the entity database table name.
|
||||||
func (Face) TableName() string {
|
func (Face) TableName() string {
|
||||||
return "faces_dev8"
|
return "faces_dev9"
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFace returns a new face.
|
// NewFace returns a new face.
|
||||||
func NewFace(subjectUID, faceSrc string, embeddings Embeddings) *Face {
|
func NewFace(subjUID, faceSrc string, embeddings Embeddings) *Face {
|
||||||
result := &Face{
|
result := &Face{
|
||||||
SubjectUID: subjectUID,
|
SubjUID: subjUID,
|
||||||
FaceSrc: faceSrc,
|
FaceSrc: faceSrc,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := result.SetEmbeddings(embeddings); err != nil {
|
if err := result.SetEmbeddings(embeddings); err != nil {
|
||||||
@@ -145,7 +145,7 @@ func (m *Face) Match(embeddings Embeddings) (match bool, dist float64) {
|
|||||||
|
|
||||||
// ResolveCollision resolves a collision with a different subject's face.
|
// ResolveCollision resolves a collision with a different subject's face.
|
||||||
func (m *Face) ResolveCollision(embeddings Embeddings) (resolved bool, err error) {
|
func (m *Face) ResolveCollision(embeddings Embeddings) (resolved bool, err error) {
|
||||||
if m.SubjectUID == "" {
|
if m.SubjUID == "" {
|
||||||
// Ignore reports for anonymous faces.
|
// Ignore reports for anonymous faces.
|
||||||
return false, nil
|
return false, nil
|
||||||
} else if m.ID == "" {
|
} else if m.ID == "" {
|
||||||
@@ -165,9 +165,9 @@ func (m *Face) ResolveCollision(embeddings Embeddings) (resolved bool, err error
|
|||||||
log.Infof("faces: %s collision at dist %f reported, same person?", m.ID, dist)
|
log.Infof("faces: %s collision at dist %f reported, same person?", m.ID, dist)
|
||||||
|
|
||||||
// Reset subject UID just in case.
|
// Reset subject UID just in case.
|
||||||
m.SubjectUID = ""
|
m.SubjUID = ""
|
||||||
|
|
||||||
return false, m.Updates(Values{"SubjectUID": m.SubjectUID})
|
return false, m.Updates(Values{"SubjUID": m.SubjUID})
|
||||||
} else {
|
} else {
|
||||||
m.MatchedAt = nil
|
m.MatchedAt = nil
|
||||||
m.Collisions++
|
m.Collisions++
|
||||||
@@ -242,20 +242,20 @@ func (m *Face) MatchMarkers(faceIds []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetSubjectUID updates the face's subject uid and related markers.
|
// SetSubjectUID updates the face's subject uid and related markers.
|
||||||
func (m *Face) SetSubjectUID(uid string) (err error) {
|
func (m *Face) SetSubjectUID(subjUID string) (err error) {
|
||||||
// Update face.
|
// Update face.
|
||||||
if err = m.Update("SubjectUID", uid); err != nil {
|
if err = m.Update("SubjUID", subjUID); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
m.SubjectUID = uid
|
m.SubjUID = subjUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update related markers.
|
// Update related markers.
|
||||||
if err = Db().Model(&Marker{}).
|
if err = Db().Model(&Marker{}).
|
||||||
Where("face_id = ?", m.ID).
|
Where("face_id = ?", m.ID).
|
||||||
Where("subject_src = ?", SrcAuto).
|
Where("subj_src = ?", SrcAuto).
|
||||||
Where("subject_uid <> ?", m.SubjectUID).
|
Where("subj_uid <> ?", m.SubjUID).
|
||||||
Updates(Values{"SubjectUID": m.SubjectUID, "Review": false}).Error; err != nil {
|
Updates(Values{"SubjUID": m.SubjUID, "MarkerReview": false}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,12 +309,12 @@ func FirstOrCreateFace(m *Face) *Face {
|
|||||||
result := Face{}
|
result := Face{}
|
||||||
|
|
||||||
if err := UnscopedDb().Where("id = ?", m.ID).First(&result).Error; err == nil {
|
if err := UnscopedDb().Where("id = ?", m.ID).First(&result).Error; err == nil {
|
||||||
log.Warnf("faces: %s has ambiguous subject %s", m.ID, m.SubjectUID)
|
log.Warnf("faces: %s has ambiguous subject %s", m.ID, m.SubjUID)
|
||||||
return &result
|
return &result
|
||||||
} else if createErr := m.Create(); createErr == nil {
|
} else if createErr := m.Create(); createErr == nil {
|
||||||
return m
|
return m
|
||||||
} else if err := UnscopedDb().Where("id = ?", m.ID).First(&result).Error; err == nil {
|
} else if err := UnscopedDb().Where("id = ?", m.ID).First(&result).Error; err == nil {
|
||||||
log.Warnf("faces: %s has ambiguous subject %s", m.ID, m.SubjectUID)
|
log.Warnf("faces: %s has ambiguous subject %s", m.ID, m.SubjUID)
|
||||||
return &result
|
return &result
|
||||||
} else {
|
} else {
|
||||||
log.Errorf("faces: %s when trying to create %s", createErr, m.ID)
|
log.Errorf("faces: %s when trying to create %s", createErr, m.ID)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -9,7 +9,7 @@ import (
|
|||||||
func TestFaceMap_Get(t *testing.T) {
|
func TestFaceMap_Get(t *testing.T) {
|
||||||
t.Run("get existing face", func(t *testing.T) {
|
t.Run("get existing face", func(t *testing.T) {
|
||||||
r := FaceFixtures.Get("jane-doe")
|
r := FaceFixtures.Get("jane-doe")
|
||||||
assert.Equal(t, "jqy1y111h1njaaab", r.SubjectUID)
|
assert.Equal(t, "jqy1y111h1njaaab", r.SubjUID)
|
||||||
assert.Equal(t, "VF7ANLDET2BKZNT4VQWJMMC6HBEFDOG7", r.ID)
|
assert.Equal(t, "VF7ANLDET2BKZNT4VQWJMMC6HBEFDOG7", r.ID)
|
||||||
assert.IsType(t, Face{}, r)
|
assert.IsType(t, Face{}, r)
|
||||||
})
|
})
|
||||||
@@ -23,7 +23,7 @@ func TestFaceMap_Get(t *testing.T) {
|
|||||||
func TestFaceMap_Pointer(t *testing.T) {
|
func TestFaceMap_Pointer(t *testing.T) {
|
||||||
t.Run("get existing face", func(t *testing.T) {
|
t.Run("get existing face", func(t *testing.T) {
|
||||||
r := FaceFixtures.Pointer("jane-doe")
|
r := FaceFixtures.Pointer("jane-doe")
|
||||||
assert.Equal(t, "jqy1y111h1njaaab", r.SubjectUID)
|
assert.Equal(t, "jqy1y111h1njaaab", r.SubjUID)
|
||||||
assert.Equal(t, "VF7ANLDET2BKZNT4VQWJMMC6HBEFDOG7", r.ID)
|
assert.Equal(t, "VF7ANLDET2BKZNT4VQWJMMC6HBEFDOG7", r.ID)
|
||||||
assert.IsType(t, &Face{}, r)
|
assert.IsType(t, &Face{}, r)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ func TestNewFace(t *testing.T) {
|
|||||||
|
|
||||||
r := NewFace("123", SrcAuto, e)
|
r := NewFace("123", SrcAuto, e)
|
||||||
assert.Equal(t, "", r.FaceSrc)
|
assert.Equal(t, "", r.FaceSrc)
|
||||||
assert.Equal(t, "123", r.SubjectUID)
|
assert.Equal(t, "123", r.SubjUID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ func TestFace_Save(t *testing.T) {
|
|||||||
assert.Nil(t, FindFace(m.ID))
|
assert.Nil(t, FindFace(m.ID))
|
||||||
m.Save()
|
m.Save()
|
||||||
assert.NotNil(t, FindFace(m.ID))
|
assert.NotNil(t, FindFace(m.ID))
|
||||||
assert.Equal(t, "12345fde", FindFace(m.ID).SubjectUID)
|
assert.Equal(t, "12345fde", FindFace(m.ID).SubjUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFace_Update(t *testing.T) {
|
func TestFace_Update(t *testing.T) {
|
||||||
@@ -193,11 +193,11 @@ func TestFace_Update(t *testing.T) {
|
|||||||
assert.Nil(t, FindFace(m.ID))
|
assert.Nil(t, FindFace(m.ID))
|
||||||
m.Save()
|
m.Save()
|
||||||
assert.NotNil(t, FindFace(m.ID))
|
assert.NotNil(t, FindFace(m.ID))
|
||||||
assert.Equal(t, "12345fdef", FindFace(m.ID).SubjectUID)
|
assert.Equal(t, "12345fdef", FindFace(m.ID).SubjUID)
|
||||||
|
|
||||||
m2 := FindFace(m.ID)
|
m2 := FindFace(m.ID)
|
||||||
m2.Update("SubjectUID", "new")
|
m2.Update("SubjUID", "new")
|
||||||
assert.Equal(t, "new", FindFace(m.ID).SubjectUID)
|
assert.Equal(t, "new", FindFace(m.ID).SubjUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFace_RefreshPhotos(t *testing.T) {
|
func TestFace_RefreshPhotos(t *testing.T) {
|
||||||
@@ -212,12 +212,12 @@ func TestFirstOrCreateFace(t *testing.T) {
|
|||||||
t.Run("create new face", func(t *testing.T) {
|
t.Run("create new face", func(t *testing.T) {
|
||||||
m := NewFace("12345unique", SrcAuto, Embeddings{Embedding{99}, Embedding{2}})
|
m := NewFace("12345unique", SrcAuto, Embeddings{Embedding{99}, Embedding{2}})
|
||||||
r := FirstOrCreateFace(m)
|
r := FirstOrCreateFace(m)
|
||||||
assert.Equal(t, "12345unique", r.SubjectUID)
|
assert.Equal(t, "12345unique", r.SubjUID)
|
||||||
})
|
})
|
||||||
t.Run("return existing entity", func(t *testing.T) {
|
t.Run("return existing entity", func(t *testing.T) {
|
||||||
m := FaceFixtures.Pointer("joe-biden")
|
m := FaceFixtures.Pointer("joe-biden")
|
||||||
r := FirstOrCreateFace(m)
|
r := FirstOrCreateFace(m)
|
||||||
assert.Equal(t, "jqy3y652h8njw0sx", r.SubjectUID)
|
assert.Equal(t, "jqy3y652h8njw0sx", r.SubjUID)
|
||||||
assert.Equal(t, 33, r.Samples)
|
assert.Equal(t, 33, r.Samples)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -420,8 +420,8 @@ func (m *File) AddFaces(faces face.Faces) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddFace adds a face marker to the file.
|
// AddFace adds a face marker to the file.
|
||||||
func (m *File) AddFace(f face.Face, subjectUID string) {
|
func (m *File) AddFace(f face.Face, subjUID string) {
|
||||||
marker := *NewFaceMarker(f, *m, subjectUID)
|
marker := *NewFaceMarker(f, *m, subjUID)
|
||||||
|
|
||||||
if markers := m.Markers(); !markers.Contains(marker) {
|
if markers := m.Markers(); !markers.Contains(marker) {
|
||||||
markers.Append(marker)
|
markers.Append(marker)
|
||||||
|
|||||||
@@ -34,9 +34,10 @@ type Marker struct {
|
|||||||
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
|
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
|
||||||
MarkerName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name,omitempty"`
|
MarkerName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name,omitempty"`
|
||||||
MarkerInvalid bool `json:"Invalid" yaml:"Invalid,omitempty"`
|
MarkerInvalid bool `json:"Invalid" yaml:"Invalid,omitempty"`
|
||||||
SubjectUID string `gorm:"type:VARBINARY(42);index:idx_markers_subject_uid_src;" json:"SubjectUID" yaml:"SubjectUID,omitempty"`
|
MarkerReview bool `json:"Review" yaml:"Review,omitempty"`
|
||||||
SubjectSrc string `gorm:"type:VARBINARY(8);index:idx_markers_subject_uid_src;default:'';" json:"SubjectSrc" yaml:"SubjectSrc,omitempty"`
|
SubjUID string `gorm:"type:VARBINARY(42);index:idx_markers_subj_uid_src;" json:"SubjUID" yaml:"SubjUID,omitempty"`
|
||||||
subject *Subject `gorm:"foreignkey:SubjectUID;association_foreignkey:SubjectUID;association_autoupdate:false;association_autocreate:false;association_save_reference:false"`
|
SubjSrc string `gorm:"type:VARBINARY(8);index:idx_markers_subj_uid_src;default:'';" json:"SubjSrc" yaml:"SubjSrc,omitempty"`
|
||||||
|
subject *Subject `gorm:"foreignkey:SubjUID;association_foreignkey:SubjUID;association_autoupdate:false;association_autocreate:false;association_save_reference:false"`
|
||||||
FaceID string `gorm:"type:VARBINARY(42);index;" json:"FaceID" yaml:"FaceID,omitempty"`
|
FaceID string `gorm:"type:VARBINARY(42);index;" json:"FaceID" yaml:"FaceID,omitempty"`
|
||||||
FaceDist float64 `gorm:"default:-1" json:"FaceDist" yaml:"FaceDist,omitempty"`
|
FaceDist float64 `gorm:"default:-1" json:"FaceDist" yaml:"FaceDist,omitempty"`
|
||||||
face *Face `gorm:"foreignkey:FaceID;association_foreignkey:ID;association_autoupdate:false;association_autocreate:false;association_save_reference:false"`
|
face *Face `gorm:"foreignkey:FaceID;association_foreignkey:ID;association_autoupdate:false;association_autocreate:false;association_save_reference:false"`
|
||||||
@@ -49,7 +50,6 @@ type Marker struct {
|
|||||||
H float32 `gorm:"type:FLOAT;" json:"H" yaml:"H,omitempty"`
|
H float32 `gorm:"type:FLOAT;" json:"H" yaml:"H,omitempty"`
|
||||||
Size int `gorm:"default:-1" json:"Size" yaml:"Size,omitempty"`
|
Size int `gorm:"default:-1" json:"Size" yaml:"Size,omitempty"`
|
||||||
Score int `gorm:"type:SMALLINT" json:"Score" yaml:"Score,omitempty"`
|
Score int `gorm:"type:SMALLINT" json:"Score" yaml:"Score,omitempty"`
|
||||||
Review bool `json:"Review" yaml:"Review,omitempty"`
|
|
||||||
MatchedAt *time.Time `sql:"index" json:"MatchedAt" yaml:"MatchedAt,omitempty"`
|
MatchedAt *time.Time `sql:"index" json:"MatchedAt" yaml:"MatchedAt,omitempty"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
@@ -57,7 +57,7 @@ type Marker struct {
|
|||||||
|
|
||||||
// TableName returns the entity database table name.
|
// TableName returns the entity database table name.
|
||||||
func (Marker) TableName() string {
|
func (Marker) TableName() string {
|
||||||
return "markers_dev8"
|
return "markers_dev9"
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||||
@@ -70,14 +70,14 @@ func (m *Marker) BeforeCreate(scope *gorm.Scope) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewMarker creates a new entity.
|
// NewMarker creates a new entity.
|
||||||
func NewMarker(file File, area crop.Area, subjectUID, markerSrc, markerType string) *Marker {
|
func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string) *Marker {
|
||||||
m := &Marker{
|
m := &Marker{
|
||||||
FileUID: file.FileUID,
|
FileUID: file.FileUID,
|
||||||
FileHash: file.FileHash,
|
FileHash: file.FileHash,
|
||||||
CropArea: area.String(),
|
CropArea: area.String(),
|
||||||
MarkerSrc: markerSrc,
|
MarkerSrc: markerSrc,
|
||||||
MarkerType: markerType,
|
MarkerType: markerType,
|
||||||
SubjectUID: subjectUID,
|
SubjUID: subjUID,
|
||||||
X: area.X,
|
X: area.X,
|
||||||
Y: area.Y,
|
Y: area.Y,
|
||||||
W: area.W,
|
W: area.W,
|
||||||
@@ -89,12 +89,12 @@ func NewMarker(file File, area crop.Area, subjectUID, markerSrc, markerType stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewFaceMarker creates a new entity.
|
// NewFaceMarker creates a new entity.
|
||||||
func NewFaceMarker(f face.Face, file File, subjectUID string) *Marker {
|
func NewFaceMarker(f face.Face, file File, subjUID string) *Marker {
|
||||||
m := NewMarker(file, f.CropArea(), subjectUID, SrcImage, MarkerFace)
|
m := NewMarker(file, f.CropArea(), subjUID, SrcImage, MarkerFace)
|
||||||
|
|
||||||
m.Size = f.Size()
|
m.Size = f.Size()
|
||||||
m.Score = f.Score
|
m.Score = f.Score
|
||||||
m.Review = f.Score < 30
|
m.MarkerReview = f.Score < 30
|
||||||
m.FaceDist = -1
|
m.FaceDist = -1
|
||||||
m.EmbeddingsJSON = f.EmbeddingsJSON()
|
m.EmbeddingsJSON = f.EmbeddingsJSON()
|
||||||
m.LandmarksJSON = f.RelativeLandmarksJSON()
|
m.LandmarksJSON = f.RelativeLandmarksJSON()
|
||||||
@@ -121,13 +121,13 @@ func (m *Marker) SaveForm(f form.Marker) error {
|
|||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.Review != f.Review {
|
if m.MarkerReview != f.MarkerReview {
|
||||||
m.Review = f.Review
|
m.MarkerReview = f.MarkerReview
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.SubjectSrc == SrcManual && strings.TrimSpace(f.MarkerName) != "" {
|
if f.SubjSrc == SrcManual && strings.TrimSpace(f.MarkerName) != "" {
|
||||||
m.SubjectSrc = SrcManual
|
m.SubjSrc = SrcManual
|
||||||
m.MarkerName = txt.Title(txt.Clip(f.MarkerName, txt.ClipDefault))
|
m.MarkerName = txt.Title(txt.Clip(f.MarkerName, txt.ClipDefault))
|
||||||
|
|
||||||
if err := m.SyncSubject(true); err != nil {
|
if err := m.SyncSubject(true); err != nil {
|
||||||
@@ -172,21 +172,21 @@ func (m *Marker) SetFace(f *Face, dist float64) (updated bool, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Any reason we don't want to set a new face for this marker?
|
// Any reason we don't want to set a new face for this marker?
|
||||||
if m.SubjectSrc == SrcAuto || f.SubjectUID == "" || m.SubjectUID == "" || f.SubjectUID == m.SubjectUID {
|
if m.SubjSrc == SrcAuto || f.SubjUID == "" || m.SubjUID == "" || f.SubjUID == m.SubjUID {
|
||||||
// Don't skip if subject wasn't set manually, or subjects match.
|
// Don't skip if subject wasn't set manually, or subjects match.
|
||||||
} else if reported, err := f.ResolveCollision(m.Embeddings()); err != nil {
|
} else if reported, err := f.ResolveCollision(m.Embeddings()); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if reported {
|
} else if reported {
|
||||||
log.Infof("faces: collision of marker %s, subject %s, face %s, subject %s, source %s", m.MarkerUID, m.SubjectUID, f.ID, f.SubjectUID, m.SubjectSrc)
|
log.Infof("faces: collision of marker %s, subject %s, face %s, subject %s, source %s", m.MarkerUID, m.SubjUID, f.ID, f.SubjUID, m.SubjSrc)
|
||||||
return false, nil
|
return false, nil
|
||||||
} else {
|
} else {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update face with known subject from marker?
|
// Update face with known subject from marker?
|
||||||
if m.SubjectSrc == SrcAuto || m.SubjectUID == "" || f.SubjectUID != "" {
|
if m.SubjSrc == SrcAuto || m.SubjUID == "" || f.SubjUID != "" {
|
||||||
// Don't update if face has a known subject, or marker subject is unknown.
|
// Don't update if face has a known subject, or marker subject is unknown.
|
||||||
} else if err = f.SetSubjectUID(m.SubjectUID); err != nil {
|
} else if err = f.SetSubjectUID(m.SubjUID); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ func (m *Marker) SetFace(f *Face, dist float64) (updated bool, err error) {
|
|||||||
m.face = f
|
m.face = f
|
||||||
|
|
||||||
// Skip update if the same face is already set.
|
// Skip update if the same face is already set.
|
||||||
if m.SubjectUID == f.SubjectUID && m.FaceID == f.ID {
|
if m.SubjUID == f.SubjUID && m.FaceID == f.ID {
|
||||||
// Update matching timestamp.
|
// Update matching timestamp.
|
||||||
m.MatchedAt = TimePointer()
|
m.MatchedAt = TimePointer()
|
||||||
return false, m.Updates(Values{"MatchedAt": m.MatchedAt})
|
return false, m.Updates(Values{"MatchedAt": m.MatchedAt})
|
||||||
@@ -202,8 +202,8 @@ func (m *Marker) SetFace(f *Face, dist float64) (updated bool, err error) {
|
|||||||
|
|
||||||
// Remember current values for comparison.
|
// Remember current values for comparison.
|
||||||
faceID := m.FaceID
|
faceID := m.FaceID
|
||||||
subjectUID := m.SubjectUID
|
subjUID := m.SubjUID
|
||||||
SubjectSrc := m.SubjectSrc
|
subjSrc := m.SubjSrc
|
||||||
|
|
||||||
m.FaceID = f.ID
|
m.FaceID = f.ID
|
||||||
m.FaceDist = dist
|
m.FaceDist = dist
|
||||||
@@ -211,7 +211,7 @@ func (m *Marker) SetFace(f *Face, dist float64) (updated bool, err error) {
|
|||||||
if m.FaceDist < 0 {
|
if m.FaceDist < 0 {
|
||||||
faceEmbedding := f.Embedding()
|
faceEmbedding := f.Embedding()
|
||||||
|
|
||||||
// Calculate smallest distance to embeddings.
|
// Calculate the smallest distance to embeddings.
|
||||||
for _, e := range m.Embeddings() {
|
for _, e := range m.Embeddings() {
|
||||||
if len(e) != len(faceEmbedding) {
|
if len(e) != len(faceEmbedding) {
|
||||||
continue
|
continue
|
||||||
@@ -223,8 +223,8 @@ func (m *Marker) SetFace(f *Face, dist float64) (updated bool, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.SubjectUID != "" {
|
if f.SubjUID != "" {
|
||||||
m.SubjectUID = f.SubjectUID
|
m.SubjUID = f.SubjUID
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = m.SyncSubject(false); err != nil {
|
if err = m.SyncSubject(false); err != nil {
|
||||||
@@ -232,18 +232,18 @@ func (m *Marker) SetFace(f *Face, dist float64) (updated bool, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update face subject?
|
// Update face subject?
|
||||||
if m.SubjectSrc == SrcAuto || m.SubjectUID == "" || f.SubjectUID == m.SubjectUID {
|
if m.SubjSrc == SrcAuto || m.SubjUID == "" || f.SubjUID == m.SubjUID {
|
||||||
// Not needed.
|
// Not needed.
|
||||||
} else if err = f.SetSubjectUID(m.SubjectUID); err != nil {
|
} else if err = f.SetSubjectUID(m.SubjUID); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
updated = m.FaceID != faceID || m.SubjectUID != subjectUID || m.SubjectSrc != SubjectSrc
|
updated = m.FaceID != faceID || m.SubjUID != subjUID || m.SubjSrc != subjSrc
|
||||||
|
|
||||||
// Update matching timestamp.
|
// Update matching timestamp.
|
||||||
m.MatchedAt = TimePointer()
|
m.MatchedAt = TimePointer()
|
||||||
|
|
||||||
if err := m.Updates(Values{"FaceID": m.FaceID, "FaceDist": m.FaceDist, "SubjectUID": m.SubjectUID, "SubjectSrc": m.SubjectSrc, "Review": false, "MatchedAt": m.MatchedAt}); err != nil {
|
if err := m.Updates(Values{"FaceID": m.FaceID, "FaceDist": m.FaceDist, "SubjUID": m.SubjUID, "SubjSrc": m.SubjSrc, "MarkerReview": false, "MatchedAt": m.MatchedAt}); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if !updated {
|
} else if !updated {
|
||||||
return false, nil
|
return false, nil
|
||||||
@@ -261,20 +261,20 @@ func (m *Marker) SyncSubject(updateRelated bool) (err error) {
|
|||||||
|
|
||||||
subj := m.Subject()
|
subj := m.Subject()
|
||||||
|
|
||||||
if subj == nil || m.SubjectSrc == SrcAuto {
|
if subj == nil || m.SubjSrc == SrcAuto {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update subject with marker name?
|
// Update subject with marker name?
|
||||||
if m.MarkerName == "" || subj.SubjectName == m.MarkerName {
|
if m.MarkerName == "" || subj.SubjName == m.MarkerName {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
} else if subj, err = subj.UpdateName(m.MarkerName); err != nil {
|
} else if subj, err = subj.UpdateName(m.MarkerName); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if subj != nil {
|
} else if subj != nil {
|
||||||
// Update subject fields in case it was merged.
|
// Update subject fields in case it was merged.
|
||||||
m.subject = subj
|
m.subject = subj
|
||||||
m.SubjectUID = subj.SubjectUID
|
m.SubjUID = subj.SubjUID
|
||||||
m.MarkerName = subj.SubjectName
|
m.MarkerName = subj.SubjName
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create known face for subject?
|
// Create known face for subject?
|
||||||
@@ -285,21 +285,21 @@ func (m *Marker) SyncSubject(updateRelated bool) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update related markers?
|
// Update related markers?
|
||||||
if m.FaceID == "" || m.SubjectUID == "" {
|
if m.FaceID == "" || m.SubjUID == "" {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
} else if res := Db().Model(&Face{}).Where("id = ? AND subject_uid = ''", m.FaceID).Update("SubjectUID", m.SubjectUID); res.Error != nil {
|
} else if res := Db().Model(&Face{}).Where("id = ? AND subj_uid = ''", m.FaceID).Update("SubjUID", m.SubjUID); res.Error != nil {
|
||||||
return fmt.Errorf("%s (update known face)", err)
|
return fmt.Errorf("%s (update known face)", err)
|
||||||
} else if !updateRelated {
|
} else if !updateRelated {
|
||||||
return nil
|
return nil
|
||||||
} else if err := Db().Model(&Marker{}).
|
} else if err := Db().Model(&Marker{}).
|
||||||
Where("marker_uid <> ?", m.MarkerUID).
|
Where("marker_uid <> ?", m.MarkerUID).
|
||||||
Where("face_id = ?", m.FaceID).
|
Where("face_id = ?", m.FaceID).
|
||||||
Where("subject_src = ?", SrcAuto).
|
Where("subj_src = ?", SrcAuto).
|
||||||
Where("subject_uid <> ?", m.SubjectUID).
|
Where("subj_uid <> ?", m.SubjUID).
|
||||||
Updates(Values{"SubjectUID": m.SubjectUID, "SubjectSrc": SrcAuto, "Review": false}).Error; err != nil {
|
Updates(Values{"SubjUID": m.SubjUID, "SubjSrc": SrcAuto, "MarkerReview": false}).Error; err != nil {
|
||||||
return fmt.Errorf("%s (update related markers)", err)
|
return fmt.Errorf("%s (update related markers)", err)
|
||||||
} else if res.RowsAffected > 0 && m.face != nil {
|
} else if res.RowsAffected > 0 && m.face != nil {
|
||||||
log.Debugf("marker: matched %s with %s", subj.SubjectName, m.FaceID)
|
log.Debugf("marker: matched %s with %s", subj.SubjName, m.FaceID)
|
||||||
return m.face.RefreshPhotos()
|
return m.face.RefreshPhotos()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +342,7 @@ func (m *Marker) SubjectName() string {
|
|||||||
if m.MarkerName != "" {
|
if m.MarkerName != "" {
|
||||||
return m.MarkerName
|
return m.MarkerName
|
||||||
} else if s := m.Subject(); s != nil {
|
} else if s := m.Subject(); s != nil {
|
||||||
return s.SubjectName
|
return s.SubjName
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
@@ -351,27 +351,27 @@ func (m *Marker) SubjectName() string {
|
|||||||
// Subject returns the matching subject or nil.
|
// Subject returns the matching subject or nil.
|
||||||
func (m *Marker) Subject() (subj *Subject) {
|
func (m *Marker) Subject() (subj *Subject) {
|
||||||
if m.subject != nil {
|
if m.subject != nil {
|
||||||
if m.SubjectUID == m.subject.SubjectUID {
|
if m.SubjUID == m.subject.SubjUID {
|
||||||
return m.subject
|
return m.subject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create subject?
|
// Create subject?
|
||||||
if m.SubjectSrc != SrcAuto && m.MarkerName != "" && m.SubjectUID == "" {
|
if m.SubjSrc != SrcAuto && m.MarkerName != "" && m.SubjUID == "" {
|
||||||
if subj = NewSubject(m.MarkerName, SubjectPerson, m.SubjectSrc); subj == nil {
|
if subj = NewSubject(m.MarkerName, SubjPerson, m.SubjSrc); subj == nil {
|
||||||
return nil
|
return nil
|
||||||
} else if subj = FirstOrCreateSubject(subj); subj == nil {
|
} else if subj = FirstOrCreateSubject(subj); subj == nil {
|
||||||
log.Debugf("marker: invalid subject %s", txt.Quote(m.MarkerName))
|
log.Debugf("marker: invalid subject %s", txt.Quote(m.MarkerName))
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
m.subject = subj
|
m.subject = subj
|
||||||
m.SubjectUID = subj.SubjectUID
|
m.SubjUID = subj.SubjUID
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.subject
|
return m.subject
|
||||||
}
|
}
|
||||||
|
|
||||||
m.subject = FindSubject(m.SubjectUID)
|
m.subject = FindSubject(m.SubjUID)
|
||||||
|
|
||||||
return m.subject
|
return m.subject
|
||||||
}
|
}
|
||||||
@@ -384,7 +384,7 @@ func (m *Marker) ClearSubject(src string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update index & resolve collisions.
|
// Update index & resolve collisions.
|
||||||
if err := m.Updates(Values{"MarkerName": "", "FaceID": "", "FaceDist": -1.0, "SubjectUID": "", "SubjectSrc": src}); err != nil {
|
if err := m.Updates(Values{"MarkerName": "", "FaceID": "", "FaceDist": -1.0, "SubjUID": "", "SubjSrc": src}); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if m.face == nil {
|
} else if m.face == nil {
|
||||||
m.subject = nil
|
m.subject = nil
|
||||||
@@ -411,14 +411,14 @@ func (m *Marker) Face() (f *Face) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add face if size
|
// Add face if size
|
||||||
if m.SubjectSrc != SrcAuto && m.FaceID == "" {
|
if m.SubjSrc != SrcAuto && m.FaceID == "" {
|
||||||
if m.Size < face.ClusterMinSize || m.Score < face.ClusterMinScore {
|
if m.Size < face.ClusterMinSize || m.Score < face.ClusterMinScore {
|
||||||
log.Debugf("faces: skipped adding face for low-quality marker %s, size %d, score %d", m.MarkerUID, m.Size, m.Score)
|
log.Debugf("faces: skipped adding face for low-quality marker %s, size %d, score %d", m.MarkerUID, m.Size, m.Score)
|
||||||
return nil
|
return nil
|
||||||
} else if emb := m.Embeddings(); len(emb) == 0 {
|
} else if emb := m.Embeddings(); len(emb) == 0 {
|
||||||
log.Warnf("marker: %s has no embeddings", m.MarkerUID)
|
log.Warnf("marker: %s has no embeddings", m.MarkerUID)
|
||||||
return nil
|
return nil
|
||||||
} else if f = NewFace(m.SubjectUID, m.SubjectSrc, emb); f == nil {
|
} else if f = NewFace(m.SubjUID, m.SubjSrc, emb); f == nil {
|
||||||
log.Warnf("marker: failed adding face for id %s", m.MarkerUID)
|
log.Warnf("marker: failed adding face for id %s", m.MarkerUID)
|
||||||
return nil
|
return nil
|
||||||
} else if f = FirstOrCreateFace(f); f == nil {
|
} else if f = FirstOrCreateFace(f); f == nil {
|
||||||
@@ -452,9 +452,9 @@ func (m *Marker) ClearFace() (updated bool, err error) {
|
|||||||
m.MatchedAt = TimePointer()
|
m.MatchedAt = TimePointer()
|
||||||
|
|
||||||
// Remove subject if set automatically.
|
// Remove subject if set automatically.
|
||||||
if m.SubjectSrc == SrcAuto {
|
if m.SubjSrc == SrcAuto {
|
||||||
m.SubjectUID = ""
|
m.SubjUID = ""
|
||||||
err = m.Updates(Values{"FaceID": "", "FaceDist": -1.0, "SubjectUID": "", "MatchedAt": m.MatchedAt})
|
err = m.Updates(Values{"FaceID": "", "FaceDist": -1.0, "SubjUID": "", "MatchedAt": m.MatchedAt})
|
||||||
} else {
|
} else {
|
||||||
err = m.Updates(Values{"FaceID": "", "FaceDist": -1.0, "MatchedAt": m.MatchedAt})
|
err = m.Updates(Values{"FaceID": "", "FaceDist": -1.0, "MatchedAt": m.MatchedAt})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ var MarkerFixtures = MarkerMap{
|
|||||||
"1000003-1": Marker{ //Photo04
|
"1000003-1": Marker{ //Photo04
|
||||||
MarkerUID: "mqu0xs11qekk9jx8",
|
MarkerUID: "mqu0xs11qekk9jx8",
|
||||||
FileUID: "ft2es39w45bnlqdw",
|
FileUID: "ft2es39w45bnlqdw",
|
||||||
SubjectUID: "jqu0xs11qekk9jx8",
|
SubjUID: "jqu0xs11qekk9jx8",
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerLabel,
|
MarkerType: MarkerLabel,
|
||||||
X: 0.308333,
|
X: 0.308333,
|
||||||
@@ -40,7 +40,7 @@ var MarkerFixtures = MarkerMap{
|
|||||||
"1000003-2": Marker{ //Photo04
|
"1000003-2": Marker{ //Photo04
|
||||||
MarkerUID: "mt9k3pw1wowuy3c3",
|
MarkerUID: "mt9k3pw1wowuy3c3",
|
||||||
FileUID: "ft2es39w45bnlqdw",
|
FileUID: "ft2es39w45bnlqdw",
|
||||||
SubjectUID: "lt9k3pw1wowuy3c3",
|
SubjUID: "lt9k3pw1wowuy3c3",
|
||||||
FaceID: "LRG2HJBDZE66LYG7Q5SRFXO2MDTOES52",
|
FaceID: "LRG2HJBDZE66LYG7Q5SRFXO2MDTOES52",
|
||||||
MarkerName: "Unknown",
|
MarkerName: "Unknown",
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
@@ -55,7 +55,7 @@ var MarkerFixtures = MarkerMap{
|
|||||||
"1000003-3": Marker{ //Photo04
|
"1000003-3": Marker{ //Photo04
|
||||||
MarkerUID: "mt9k3pw1wowuy111",
|
MarkerUID: "mt9k3pw1wowuy111",
|
||||||
FileUID: "ft2es39w45bnlqdw",
|
FileUID: "ft2es39w45bnlqdw",
|
||||||
SubjectUID: "",
|
SubjUID: "",
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerLabel,
|
MarkerType: MarkerLabel,
|
||||||
MarkerName: "Center",
|
MarkerName: "Center",
|
||||||
@@ -69,7 +69,7 @@ var MarkerFixtures = MarkerMap{
|
|||||||
"1000003-4": Marker{ //Photo04
|
"1000003-4": Marker{ //Photo04
|
||||||
MarkerUID: "mt9k3pw1wowuy222",
|
MarkerUID: "mt9k3pw1wowuy222",
|
||||||
FileUID: "ft2es39w45bnlqdw",
|
FileUID: "ft2es39w45bnlqdw",
|
||||||
SubjectUID: "",
|
SubjUID: "",
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "Jens Mander",
|
MarkerName: "Jens Mander",
|
||||||
@@ -86,8 +86,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
MarkerUID: "mt9k3pw1wowuy333",
|
MarkerUID: "mt9k3pw1wowuy333",
|
||||||
FileUID: "ft2es39w45bnlqdw",
|
FileUID: "ft2es39w45bnlqdw",
|
||||||
FaceID: FaceFixtures.Get("unknown").ID,
|
FaceID: FaceFixtures.Get("unknown").ID,
|
||||||
SubjectUID: "",
|
SubjUID: "",
|
||||||
SubjectSrc: SrcAuto,
|
SubjSrc: SrcAuto,
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "Corn McCornface",
|
MarkerName: "Corn McCornface",
|
||||||
@@ -105,8 +105,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FileUID: "ft2es39w45bnlqdw",
|
FileUID: "ft2es39w45bnlqdw",
|
||||||
FaceID: FaceFixtures.Get("john-doe").ID,
|
FaceID: FaceFixtures.Get("john-doe").ID,
|
||||||
FaceDist: 0.2,
|
FaceDist: 0.2,
|
||||||
SubjectSrc: SrcAuto,
|
SubjSrc: SrcAuto,
|
||||||
SubjectUID: "",
|
SubjUID: "",
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "",
|
MarkerName: "",
|
||||||
@@ -124,8 +124,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FileUID: "ft2es49qhhinlple",
|
FileUID: "ft2es49qhhinlple",
|
||||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||||
FaceDist: 0.5,
|
FaceDist: 0.5,
|
||||||
SubjectSrc: "",
|
SubjSrc: "",
|
||||||
SubjectUID: "",
|
SubjUID: "",
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "",
|
MarkerName: "",
|
||||||
@@ -143,8 +143,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FileUID: "ft2es49qhhinlple",
|
FileUID: "ft2es49qhhinlple",
|
||||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||||
FaceDist: 0.6,
|
FaceDist: 0.6,
|
||||||
SubjectSrc: SrcAuto,
|
SubjSrc: SrcAuto,
|
||||||
SubjectUID: "",
|
SubjUID: "",
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "",
|
MarkerName: "",
|
||||||
@@ -162,8 +162,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FileUID: "ft2es49w15bnlqdw",
|
FileUID: "ft2es49w15bnlqdw",
|
||||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||||
FaceDist: 0.6,
|
FaceDist: 0.6,
|
||||||
SubjectSrc: SrcAuto,
|
SubjSrc: SrcAuto,
|
||||||
SubjectUID: "",
|
SubjUID: "",
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "",
|
MarkerName: "",
|
||||||
@@ -181,8 +181,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FileUID: "ft8es39w45bnlqdw",
|
FileUID: "ft8es39w45bnlqdw",
|
||||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||||
FaceDist: 0.6,
|
FaceDist: 0.6,
|
||||||
SubjectSrc: SrcAuto,
|
SubjSrc: SrcAuto,
|
||||||
SubjectUID: "",
|
SubjUID: "",
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "",
|
MarkerName: "",
|
||||||
@@ -201,8 +201,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||||
CropArea: "045038063041",
|
CropArea: "045038063041",
|
||||||
FaceDist: 0.26852392873736236,
|
FaceDist: 0.26852392873736236,
|
||||||
SubjectSrc: SrcManual,
|
SubjSrc: SrcManual,
|
||||||
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "Actress A",
|
MarkerName: "Actress A",
|
||||||
@@ -221,8 +221,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||||
CropArea: "046045043065",
|
CropArea: "046045043065",
|
||||||
FaceDist: 0.4507357278575355,
|
FaceDist: 0.4507357278575355,
|
||||||
SubjectSrc: "",
|
SubjSrc: "",
|
||||||
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "",
|
MarkerName: "",
|
||||||
@@ -241,8 +241,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FaceID: FaceFixtures.Get("actress-1").ID,
|
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||||
CropArea: "05403304060446",
|
CropArea: "05403304060446",
|
||||||
FaceDist: 0.5099754448545762,
|
FaceDist: 0.5099754448545762,
|
||||||
SubjectSrc: "",
|
SubjSrc: "",
|
||||||
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "",
|
MarkerName: "",
|
||||||
@@ -260,8 +260,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FileUID: "ft3es39w45bnlqdw",
|
FileUID: "ft3es39w45bnlqdw",
|
||||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||||
FaceDist: 0.5223304453393212,
|
FaceDist: 0.5223304453393212,
|
||||||
SubjectSrc: "",
|
SubjSrc: "",
|
||||||
SubjectUID: SubjectFixtures.Get("actor-1").SubjectUID,
|
SubjUID: SubjectFixtures.Get("actor-1").SubjUID,
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "",
|
MarkerName: "",
|
||||||
@@ -279,8 +279,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FileUID: "ft2es39q45bnlqd0",
|
FileUID: "ft2es39q45bnlqd0",
|
||||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||||
FaceDist: 0.5088545446490167,
|
FaceDist: 0.5088545446490167,
|
||||||
SubjectSrc: "",
|
SubjSrc: "",
|
||||||
SubjectUID: SubjectFixtures.Get("actor-1").SubjectUID,
|
SubjUID: SubjectFixtures.Get("actor-1").SubjUID,
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "",
|
MarkerName: "",
|
||||||
@@ -298,8 +298,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FileUID: "fikjs39w45bnlqdw",
|
FileUID: "fikjs39w45bnlqdw",
|
||||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||||
FaceDist: 0.3139983399779298,
|
FaceDist: 0.3139983399779298,
|
||||||
SubjectSrc: "",
|
SubjSrc: "",
|
||||||
SubjectUID: SubjectFixtures.Get("actor-1").SubjectUID,
|
SubjUID: SubjectFixtures.Get("actor-1").SubjUID,
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "",
|
MarkerName: "",
|
||||||
@@ -317,8 +317,8 @@ var MarkerFixtures = MarkerMap{
|
|||||||
FileUID: "ft8es39w45bnlqdw",
|
FileUID: "ft8es39w45bnlqdw",
|
||||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||||
FaceDist: 0.3139983399779298,
|
FaceDist: 0.3139983399779298,
|
||||||
SubjectSrc: "",
|
SubjSrc: "",
|
||||||
SubjectUID: SubjectFixtures.Get("actor-1").SubjectUID,
|
SubjUID: SubjectFixtures.Get("actor-1").SubjUID,
|
||||||
MarkerSrc: SrcImage,
|
MarkerSrc: SrcImage,
|
||||||
MarkerType: MarkerFace,
|
MarkerType: MarkerFace,
|
||||||
MarkerName: "",
|
MarkerName: "",
|
||||||
|
|||||||
@@ -13,48 +13,48 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
|
|||||||
if subj = m.Subject(); subj == nil {
|
if subj = m.Subject(); subj == nil {
|
||||||
name = m.MarkerName
|
name = m.MarkerName
|
||||||
} else {
|
} else {
|
||||||
name = subj.SubjectName
|
name = subj.SubjName
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.Marshal(&struct {
|
return json.Marshal(&struct {
|
||||||
UID string
|
UID string
|
||||||
FileUID string
|
FileUID string
|
||||||
FileHash string
|
FileHash string
|
||||||
CropArea string
|
CropArea string
|
||||||
Type string
|
Type string
|
||||||
Src string
|
Src string
|
||||||
Name string
|
Name string
|
||||||
Invalid bool
|
Invalid bool
|
||||||
Review bool
|
Review bool
|
||||||
FaceID string
|
FaceID string
|
||||||
SubjectUID string
|
SubjUID string
|
||||||
SubjectSrc string
|
SubjSrc string
|
||||||
X float32
|
X float32
|
||||||
Y float32
|
Y float32
|
||||||
W float32 `json:",omitempty"`
|
W float32 `json:",omitempty"`
|
||||||
H float32 `json:",omitempty"`
|
H float32 `json:",omitempty"`
|
||||||
Size int `json:",omitempty"`
|
Size int `json:",omitempty"`
|
||||||
Score int `json:",omitempty"`
|
Score int `json:",omitempty"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
}{
|
}{
|
||||||
UID: m.MarkerUID,
|
UID: m.MarkerUID,
|
||||||
FileUID: m.FileUID,
|
FileUID: m.FileUID,
|
||||||
FileHash: m.FileHash,
|
FileHash: m.FileHash,
|
||||||
CropArea: m.CropArea,
|
CropArea: m.CropArea,
|
||||||
Type: m.MarkerType,
|
Type: m.MarkerType,
|
||||||
Src: m.MarkerSrc,
|
Src: m.MarkerSrc,
|
||||||
Name: name,
|
Name: name,
|
||||||
Invalid: m.MarkerInvalid,
|
Invalid: m.MarkerInvalid,
|
||||||
Review: m.Review,
|
Review: m.MarkerReview,
|
||||||
FaceID: m.FaceID,
|
FaceID: m.FaceID,
|
||||||
SubjectUID: m.SubjectUID,
|
SubjUID: m.SubjUID,
|
||||||
SubjectSrc: m.SubjectSrc,
|
SubjSrc: m.SubjSrc,
|
||||||
X: m.X,
|
X: m.X,
|
||||||
Y: m.Y,
|
Y: m.Y,
|
||||||
W: m.W,
|
W: m.W,
|
||||||
H: m.H,
|
H: m.H,
|
||||||
Size: m.Size,
|
Size: m.Size,
|
||||||
Score: m.Score,
|
Score: m.Score,
|
||||||
CreatedAt: m.CreatedAt,
|
CreatedAt: m.CreatedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestNewMarker(t *testing.T) {
|
|||||||
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
|
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
|
||||||
assert.Equal(t, "2cad9168fa6acc5c5c2965ddf6ec465ca42fd818", m.FileHash)
|
assert.Equal(t, "2cad9168fa6acc5c5c2965ddf6ec465ca42fd818", m.FileHash)
|
||||||
assert.Equal(t, "1340ce163163", m.CropArea)
|
assert.Equal(t, "1340ce163163", m.CropArea)
|
||||||
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjectUID)
|
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjUID)
|
||||||
assert.Equal(t, SrcImage, m.MarkerSrc)
|
assert.Equal(t, SrcImage, m.MarkerSrc)
|
||||||
assert.Equal(t, MarkerLabel, m.MarkerType)
|
assert.Equal(t, MarkerLabel, m.MarkerType)
|
||||||
}
|
}
|
||||||
@@ -38,16 +38,16 @@ func TestMarker_SaveForm(t *testing.T) {
|
|||||||
m2 := MarkerFixtures.Get("fa-gr-2")
|
m2 := MarkerFixtures.Get("fa-gr-2")
|
||||||
m3 := MarkerFixtures.Get("fa-gr-3")
|
m3 := MarkerFixtures.Get("fa-gr-3")
|
||||||
|
|
||||||
assert.Empty(t, m.SubjectUID)
|
assert.Empty(t, m.SubjUID)
|
||||||
assert.Empty(t, m2.SubjectUID)
|
assert.Empty(t, m2.SubjUID)
|
||||||
assert.Empty(t, m3.SubjectUID)
|
assert.Empty(t, m3.SubjUID)
|
||||||
|
|
||||||
m.MarkerInvalid = true
|
m.MarkerInvalid = true
|
||||||
m.Score = 50
|
m.Score = 50
|
||||||
|
|
||||||
//set new name
|
//set new name
|
||||||
|
|
||||||
f := form.Marker{SubjectSrc: SrcManual, MarkerName: "Jane Doe", MarkerInvalid: false}
|
f := form.Marker{SubjSrc: SrcManual, MarkerName: "Jane Doe", MarkerInvalid: false}
|
||||||
|
|
||||||
err := m.SaveForm(f)
|
err := m.SaveForm(f)
|
||||||
|
|
||||||
@@ -55,20 +55,20 @@ func TestMarker_SaveForm(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.NotEmpty(t, m.SubjectUID)
|
assert.NotEmpty(t, m.SubjUID)
|
||||||
|
|
||||||
if s := m.Subject(); s != nil {
|
if s := m.Subject(); s != nil {
|
||||||
assert.Equal(t, "Jane Doe", s.SubjectName)
|
assert.Equal(t, "Jane Doe", s.SubjName)
|
||||||
}
|
}
|
||||||
if m := FindMarker("mt9k3pw1wowuy777"); m != nil {
|
if m := FindMarker("mt9k3pw1wowuy777"); m != nil {
|
||||||
assert.Equal(t, "Jane Doe", m.Subject().SubjectName)
|
assert.Equal(t, "Jane Doe", m.Subject().SubjName)
|
||||||
}
|
}
|
||||||
if m := FindMarker("mt9k3pw1wowuy888"); m != nil {
|
if m := FindMarker("mt9k3pw1wowuy888"); m != nil {
|
||||||
assert.Equal(t, "Jane Doe", m.Subject().SubjectName)
|
assert.Equal(t, "Jane Doe", m.Subject().SubjName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename subject.
|
// Rename subject.
|
||||||
f3 := form.Marker{SubjectSrc: SrcManual, MarkerName: "Franzilein", MarkerInvalid: false}
|
f3 := form.Marker{SubjSrc: SrcManual, MarkerName: "Franzilein", MarkerInvalid: false}
|
||||||
|
|
||||||
if m := FindMarker("mt9k3pw1wowuy777"); m == nil {
|
if m := FindMarker("mt9k3pw1wowuy777"); m == nil {
|
||||||
t.Fatal("result is nil")
|
t.Fatal("result is nil")
|
||||||
@@ -77,13 +77,13 @@ func TestMarker_SaveForm(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m := FindMarker("mt9k3pw1wowuy666"); m != nil {
|
if m := FindMarker("mt9k3pw1wowuy666"); m != nil {
|
||||||
assert.Equal(t, "Franzilein", m.Subject().SubjectName)
|
assert.Equal(t, "Franzilein", m.Subject().SubjName)
|
||||||
}
|
}
|
||||||
if m := FindMarker("mt9k3pw1wowuy777"); m != nil {
|
if m := FindMarker("mt9k3pw1wowuy777"); m != nil {
|
||||||
assert.Equal(t, "Franzilein", m.Subject().SubjectName)
|
assert.Equal(t, "Franzilein", m.Subject().SubjName)
|
||||||
}
|
}
|
||||||
if m := FindMarker("mt9k3pw1wowuy888"); m != nil {
|
if m := FindMarker("mt9k3pw1wowuy888"); m != nil {
|
||||||
assert.Equal(t, "Franzilein", m.Subject().SubjectName)
|
assert.Equal(t, "Franzilein", m.Subject().SubjName)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ func TestUpdateOrCreateMarker(t *testing.T) {
|
|||||||
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel)
|
m := NewMarker(FileFixtures.Get("exampleFileName.jpg"), testArea, "lt9k3pw1wowuy3c3", SrcImage, MarkerLabel)
|
||||||
assert.IsType(t, &Marker{}, m)
|
assert.IsType(t, &Marker{}, m)
|
||||||
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
|
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
|
||||||
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjectUID)
|
assert.Equal(t, "lt9k3pw1wowuy3c3", m.SubjUID)
|
||||||
assert.Equal(t, SrcImage, m.MarkerSrc)
|
assert.Equal(t, SrcImage, m.MarkerSrc)
|
||||||
assert.Equal(t, MarkerLabel, m.MarkerType)
|
assert.Equal(t, MarkerLabel, m.MarkerType)
|
||||||
|
|
||||||
@@ -227,10 +227,10 @@ func TestMarker_ClearSubject(t *testing.T) {
|
|||||||
m3 := MarkerFixtures.Get("actor-a-2") // id 16
|
m3 := MarkerFixtures.Get("actor-a-2") // id 16
|
||||||
m4 := MarkerFixtures.Get("actor-a-1") // id 15
|
m4 := MarkerFixtures.Get("actor-a-1") // id 15
|
||||||
|
|
||||||
assert.Equal(t, "jqy1y111h1njaaad", m.SubjectUID)
|
assert.Equal(t, "jqy1y111h1njaaad", m.SubjUID)
|
||||||
assert.Equal(t, "jqy1y111h1njaaad", m2.SubjectUID)
|
assert.Equal(t, "jqy1y111h1njaaad", m2.SubjUID)
|
||||||
assert.Equal(t, "jqy1y111h1njaaad", m3.SubjectUID)
|
assert.Equal(t, "jqy1y111h1njaaad", m3.SubjUID)
|
||||||
assert.Equal(t, "jqy1y111h1njaaad", m4.SubjectUID)
|
assert.Equal(t, "jqy1y111h1njaaad", m4.SubjUID)
|
||||||
assert.NotNil(t, m.Face())
|
assert.NotNil(t, m.Face())
|
||||||
assert.NotNil(t, m2.Face())
|
assert.NotNil(t, m2.Face())
|
||||||
assert.NotNil(t, m3.Face())
|
assert.NotNil(t, m3.Face())
|
||||||
@@ -260,10 +260,10 @@ func TestMarker_ClearSubject(t *testing.T) {
|
|||||||
assert.NotNil(t, FindMarker("mt9k3pw1wowu1002"))
|
assert.NotNil(t, FindMarker("mt9k3pw1wowu1002"))
|
||||||
assert.NotNil(t, FindFace("PI6A2XGOTUXEFI7CBF4KCI5I2I3JEJHS"))
|
assert.NotNil(t, FindFace("PI6A2XGOTUXEFI7CBF4KCI5I2I3JEJHS"))
|
||||||
|
|
||||||
assert.Empty(t, m.SubjectUID)
|
assert.Empty(t, m.SubjUID)
|
||||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1004").SubjectUID)
|
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1004").SubjUID)
|
||||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1003").SubjectUID)
|
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1003").SubjUID)
|
||||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1002").SubjectUID)
|
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1002").SubjUID)
|
||||||
assert.Empty(t, m.FaceID)
|
assert.Empty(t, m.FaceID)
|
||||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1004").FaceID)
|
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1004").FaceID)
|
||||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1003").FaceID)
|
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1003").FaceID)
|
||||||
@@ -385,43 +385,43 @@ func TestMarker_HasFace(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMarker_Subject(t *testing.T) {
|
func TestMarker_Subject(t *testing.T) {
|
||||||
t.Run("EmptySubjectUID", func(t *testing.T) {
|
t.Run("EmptySubjUID", func(t *testing.T) {
|
||||||
m := Marker{SubjectUID: "", subject: &Subject{SubjectUID: "", SubjectName: "Test Subject"}}
|
m := Marker{SubjUID: "", subject: &Subject{SubjUID: "", SubjName: "Test Subject"}}
|
||||||
|
|
||||||
if s := m.Subject(); s == nil {
|
if s := m.Subject(); s == nil {
|
||||||
t.Fatal("return value must not be nil")
|
t.Fatal("return value must not be nil")
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, "Test Subject", s.SubjectName)
|
assert.Equal(t, "Test Subject", s.SubjName)
|
||||||
assert.Equal(t, "", m.SubjectUID)
|
assert.Equal(t, "", m.SubjUID)
|
||||||
assert.Equal(t, "", s.SubjectUID)
|
assert.Equal(t, "", s.SubjUID)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("ConflictingSubjectUID", func(t *testing.T) {
|
t.Run("ConflictingSubjUID", func(t *testing.T) {
|
||||||
m := Marker{SubjectUID: "", subject: &Subject{SubjectUID: "xyz", SubjectName: "Test Subject"}}
|
m := Marker{SubjUID: "", subject: &Subject{SubjUID: "xyz", SubjName: "Test Subject"}}
|
||||||
|
|
||||||
if s := m.Subject(); s != nil {
|
if s := m.Subject(); s != nil {
|
||||||
t.Fatal("return value must be nil")
|
t.Fatal("return value must be nil")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("SubjectSrcAuto", func(t *testing.T) {
|
t.Run("SubjSrcAuto", func(t *testing.T) {
|
||||||
m := Marker{SubjectSrc: SrcAuto, SubjectUID: "", MarkerName: "Hans Mayer"}
|
m := Marker{SubjSrc: SrcAuto, SubjUID: "", MarkerName: "Hans Mayer"}
|
||||||
|
|
||||||
if s := m.Subject(); s != nil {
|
if s := m.Subject(); s != nil {
|
||||||
t.Fatal("return value must be nil")
|
t.Fatal("return value must be nil")
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, "Hans Mayer", m.MarkerName)
|
assert.Equal(t, "Hans Mayer", m.MarkerName)
|
||||||
assert.Empty(t, m.SubjectUID)
|
assert.Empty(t, m.SubjUID)
|
||||||
assert.Equal(t, SrcAuto, m.SubjectSrc)
|
assert.Equal(t, SrcAuto, m.SubjSrc)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("SubjectSrcManual", func(t *testing.T) {
|
t.Run("SubjSrcManual", func(t *testing.T) {
|
||||||
m := Marker{SubjectSrc: SrcManual, SubjectUID: "", MarkerName: "Hans Mayer"}
|
m := Marker{SubjSrc: SrcManual, SubjUID: "", MarkerName: "Hans Mayer"}
|
||||||
|
|
||||||
if s := m.Subject(); s == nil {
|
if s := m.Subject(); s == nil {
|
||||||
t.Fatal("return value must not be nil")
|
t.Fatal("return value must not be nil")
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, "Hans Mayer", s.SubjectName)
|
assert.Equal(t, "Hans Mayer", s.SubjName)
|
||||||
assert.NotEmpty(t, s.SubjectUID)
|
assert.NotEmpty(t, s.SubjUID)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -453,11 +453,11 @@ func TestMarker_GetFace(t *testing.T) {
|
|||||||
if f := m.Face(); f == nil {
|
if f := m.Face(); f == nil {
|
||||||
t.Fatal("return value must not be nil")
|
t.Fatal("return value must not be nil")
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, "jqy3y652h8njw0sx", f.SubjectUID)
|
assert.Equal(t, "jqy3y652h8njw0sx", f.SubjUID)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("low quality marker", func(t *testing.T) {
|
t.Run("low quality marker", func(t *testing.T) {
|
||||||
m := Marker{FaceID: "", SubjectSrc: SrcManual, Size: 130}
|
m := Marker{FaceID: "", SubjSrc: SrcManual, Size: 130}
|
||||||
|
|
||||||
assert.Nil(t, m.Face())
|
assert.Nil(t, m.Face())
|
||||||
})
|
})
|
||||||
@@ -465,7 +465,7 @@ func TestMarker_GetFace(t *testing.T) {
|
|||||||
m := Marker{
|
m := Marker{
|
||||||
FaceID: "",
|
FaceID: "",
|
||||||
EmbeddingsJSON: MarkerFixtures.Get("actress-a-1").EmbeddingsJSON,
|
EmbeddingsJSON: MarkerFixtures.Get("actress-a-1").EmbeddingsJSON,
|
||||||
SubjectSrc: SrcManual,
|
SubjSrc: SrcManual,
|
||||||
Size: 160,
|
Size: 160,
|
||||||
Score: 40,
|
Score: 40,
|
||||||
}
|
}
|
||||||
@@ -499,13 +499,13 @@ func TestMarker_SetFace(t *testing.T) {
|
|||||||
assert.Equal(t, "", m.FaceID)
|
assert.Equal(t, "", m.FaceID)
|
||||||
})
|
})
|
||||||
t.Run("skip same face", func(t *testing.T) {
|
t.Run("skip same face", func(t *testing.T) {
|
||||||
m := Marker{MarkerType: MarkerFace, SubjectUID: "jqu0xs11qekk9jx8", FaceID: "99876uyt"}
|
m := Marker{MarkerType: MarkerFace, SubjUID: "jqu0xs11qekk9jx8", FaceID: "99876uyt"}
|
||||||
updated, _ := m.SetFace(&Face{ID: "99876uyt", SubjectUID: "jqu0xs11qekk9jx8"}, -1)
|
updated, _ := m.SetFace(&Face{ID: "99876uyt", SubjUID: "jqu0xs11qekk9jx8"}, -1)
|
||||||
assert.False(t, updated)
|
assert.False(t, updated)
|
||||||
assert.Equal(t, "99876uyt", m.FaceID)
|
assert.Equal(t, "99876uyt", m.FaceID)
|
||||||
})
|
})
|
||||||
t.Run("set new face", func(t *testing.T) {
|
t.Run("set new face", func(t *testing.T) {
|
||||||
m := Marker{MarkerUID: "mqyz9x61edicxf8j", MarkerType: MarkerFace, SubjectUID: "", FaceID: ""}
|
m := Marker{MarkerUID: "mqyz9x61edicxf8j", MarkerType: MarkerFace, SubjUID: "", FaceID: ""}
|
||||||
|
|
||||||
updated, _ := m.SetFace(FaceFixtures.Pointer("john-doe"), -1)
|
updated, _ := m.SetFace(FaceFixtures.Pointer("john-doe"), -1)
|
||||||
assert.True(t, updated)
|
assert.True(t, updated)
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func UpdatePhotoCounts() (err error) {
|
|||||||
if err = Db().Table(Subject{}.TableName()).
|
if err = Db().Table(Subject{}.TableName()).
|
||||||
UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(*) FROM files f "+
|
UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(*) FROM files f "+
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"JOIN %s m ON f.file_uid = m.file_uid AND m.subject_uid = %s.subject_uid ",
|
"JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ",
|
||||||
Marker{}.TableName(),
|
Marker{}.TableName(),
|
||||||
Subject{}.TableName())+
|
Subject{}.TableName())+
|
||||||
" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL)")).Error; err != nil {
|
" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL)")).Error; err != nil {
|
||||||
|
|||||||
@@ -20,19 +20,19 @@ type Subjects []Subject
|
|||||||
|
|
||||||
// Subject represents a named photo subject, typically a person.
|
// Subject represents a named photo subject, typically a person.
|
||||||
type Subject struct {
|
type Subject struct {
|
||||||
SubjectUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID"`
|
SubjUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"UID" yaml:"UID"`
|
||||||
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb,omitempty" yaml:"Thumb,omitempty"`
|
Thumb string `gorm:"type:VARBINARY(128);index;default:''" json:"Thumb,omitempty" yaml:"Thumb,omitempty"`
|
||||||
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
|
ThumbSrc string `gorm:"type:VARBINARY(8);default:''" json:"ThumbSrc,omitempty" yaml:"ThumbSrc,omitempty"`
|
||||||
SubjectType string `gorm:"type:VARBINARY(8);default:''" json:"Type,omitempty" yaml:"Type,omitempty"`
|
SubjType string `gorm:"type:VARBINARY(8);default:''" json:"Type,omitempty" yaml:"Type,omitempty"`
|
||||||
SubjectSrc string `gorm:"type:VARBINARY(8);default:''" json:"Src,omitempty" yaml:"Src,omitempty"`
|
SubjSrc string `gorm:"type:VARBINARY(8);default:''" json:"Src,omitempty" yaml:"Src,omitempty"`
|
||||||
SubjectSlug string `gorm:"type:VARBINARY(255);index;default:''" json:"Slug" yaml:"-"`
|
SubjSlug string `gorm:"type:VARBINARY(255);index;default:''" json:"Slug" yaml:"-"`
|
||||||
SubjectName string `gorm:"type:VARCHAR(255);unique_index;default:''" json:"Name" yaml:"Name"`
|
SubjName string `gorm:"type:VARCHAR(255);unique_index;default:''" json:"Name" yaml:"Name"`
|
||||||
SubjectAlias string `gorm:"type:VARCHAR(255);default:''" json:"Alias" yaml:"Alias"`
|
SubjAlias string `gorm:"type:VARCHAR(255);default:''" json:"Alias" yaml:"Alias"`
|
||||||
SubjectBio string `gorm:"type:TEXT;default:''" json:"Bio" yaml:"Bio,omitempty"`
|
SubjBio string `gorm:"type:TEXT;default:''" json:"Bio" yaml:"Bio,omitempty"`
|
||||||
SubjectNotes string `gorm:"type:TEXT;default:''" json:"Notes,omitempty" yaml:"Notes,omitempty"`
|
SubjNotes string `gorm:"type:TEXT;default:''" json:"Notes,omitempty" yaml:"Notes,omitempty"`
|
||||||
Favorite bool `gorm:"default:false" json:"Favorite" yaml:"Favorite,omitempty"`
|
SubjFavorite bool `gorm:"default:false" json:"Favorite" yaml:"Favorite,omitempty"`
|
||||||
Private bool `gorm:"default:false" json:"Private" yaml:"Private,omitempty"`
|
SubjPrivate bool `gorm:"default:false" json:"Private" yaml:"Private,omitempty"`
|
||||||
Excluded bool `gorm:"default:false" json:"Excluded" yaml:"Excluded,omitempty"`
|
SubjExcluded bool `gorm:"default:false" json:"Excluded" yaml:"Excluded,omitempty"`
|
||||||
FileCount int `gorm:"default:0" json:"Files" yaml:"-"`
|
FileCount int `gorm:"default:0" json:"Files" yaml:"-"`
|
||||||
MetadataJSON json.RawMessage `gorm:"type:MEDIUMBLOB;" json:"Metadata,omitempty" yaml:"Metadata,omitempty"`
|
MetadataJSON json.RawMessage `gorm:"type:MEDIUMBLOB;" json:"Metadata,omitempty" yaml:"Metadata,omitempty"`
|
||||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||||
@@ -42,38 +42,38 @@ type Subject struct {
|
|||||||
|
|
||||||
// TableName returns the entity database table name.
|
// TableName returns the entity database table name.
|
||||||
func (Subject) TableName() string {
|
func (Subject) TableName() string {
|
||||||
return "subjects_dev8"
|
return "subjects_dev9"
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||||
func (m *Subject) BeforeCreate(scope *gorm.Scope) error {
|
func (m *Subject) BeforeCreate(scope *gorm.Scope) error {
|
||||||
if rnd.IsUID(m.SubjectUID, 'j') {
|
if rnd.IsUID(m.SubjUID, 'j') {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return scope.SetColumn("SubjectUID", rnd.PPID('j'))
|
return scope.SetColumn("SubjUID", rnd.PPID('j'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSubject returns a new entity.
|
// NewSubject returns a new entity.
|
||||||
func NewSubject(name, subjectType, subjectSrc string) *Subject {
|
func NewSubject(name, subjType, subjSrc string) *Subject {
|
||||||
if subjectType == "" {
|
if subjType == "" {
|
||||||
subjectType = SubjectPerson
|
subjType = SubjPerson
|
||||||
}
|
}
|
||||||
|
|
||||||
subjectName := txt.Title(txt.Clip(name, txt.ClipDefault))
|
subjName := txt.Title(txt.Clip(name, txt.ClipDefault))
|
||||||
subjectSlug := slug.Make(txt.Clip(name, txt.ClipSlug))
|
subjSlug := slug.Make(txt.Clip(name, txt.ClipSlug))
|
||||||
|
|
||||||
// Name is required.
|
// Name is required.
|
||||||
if subjectName == "" || subjectSlug == "" {
|
if subjName == "" || subjSlug == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := &Subject{
|
result := &Subject{
|
||||||
SubjectSlug: subjectSlug,
|
SubjSlug: subjSlug,
|
||||||
SubjectName: subjectName,
|
SubjName: subjName,
|
||||||
SubjectType: subjectType,
|
SubjType: subjType,
|
||||||
SubjectSrc: subjectSrc,
|
SubjSrc: subjSrc,
|
||||||
FileCount: 1,
|
FileCount: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -101,10 +101,12 @@ func (m *Subject) Delete() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("subject: deleting %s %s", m.SubjectType, txt.Quote(m.SubjectName))
|
log.Infof("subject: deleting %s %s", m.SubjType, txt.Quote(m.SubjName))
|
||||||
|
|
||||||
|
event.EntitiesDeleted("subjects", []string{m.SubjUID})
|
||||||
|
|
||||||
if m.IsPerson() {
|
if m.IsPerson() {
|
||||||
event.EntitiesDeleted("people", []string{m.SubjectUID})
|
event.EntitiesDeleted("people", []string{m.SubjUID})
|
||||||
event.Publish("count.people", event.Data{
|
event.Publish("count.people", event.Data{
|
||||||
"count": -1,
|
"count": -1,
|
||||||
})
|
})
|
||||||
@@ -123,11 +125,12 @@ func (m *Subject) Restore() error {
|
|||||||
if m.Deleted() {
|
if m.Deleted() {
|
||||||
m.DeletedAt = nil
|
m.DeletedAt = nil
|
||||||
|
|
||||||
log.Infof("subject: restoring %s %s", m.SubjectType, txt.Quote(m.SubjectName))
|
log.Infof("subject: restoring %s %s", m.SubjType, txt.Quote(m.SubjName))
|
||||||
|
|
||||||
|
event.EntitiesCreated("subjects", []*Subject{m})
|
||||||
|
|
||||||
if m.IsPerson() {
|
if m.IsPerson() {
|
||||||
event.EntitiesCreated("people", []*Person{m.Person()})
|
event.EntitiesCreated("people", []*Person{m.Person()})
|
||||||
|
|
||||||
event.Publish("count.people", event.Data{
|
event.Publish("count.people", event.Data{
|
||||||
"count": 1,
|
"count": 1,
|
||||||
})
|
})
|
||||||
@@ -153,14 +156,16 @@ func (m *Subject) Updates(values interface{}) error {
|
|||||||
func FirstOrCreateSubject(m *Subject) *Subject {
|
func FirstOrCreateSubject(m *Subject) *Subject {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return nil
|
return nil
|
||||||
} else if m.SubjectName == "" {
|
} else if m.SubjName == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if found := FindSubjectByName(m.SubjectName); found != nil {
|
if found := FindSubjectByName(m.SubjName); found != nil {
|
||||||
return found
|
return found
|
||||||
} else if createErr := m.Create(); createErr == nil {
|
} else if createErr := m.Create(); createErr == nil {
|
||||||
log.Infof("subject: added %s %s", m.SubjectType, txt.Quote(m.SubjectName))
|
log.Infof("subject: added %s %s", m.SubjType, txt.Quote(m.SubjName))
|
||||||
|
|
||||||
|
event.EntitiesCreated("subjects", []*Subject{m})
|
||||||
|
|
||||||
if m.IsPerson() {
|
if m.IsPerson() {
|
||||||
event.EntitiesCreated("people", []*Person{m.Person()})
|
event.EntitiesCreated("people", []*Person{m.Person()})
|
||||||
@@ -170,10 +175,10 @@ func FirstOrCreateSubject(m *Subject) *Subject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return m
|
return m
|
||||||
} else if found = FindSubjectByName(m.SubjectName); found != nil {
|
} else if found = FindSubjectByName(m.SubjName); found != nil {
|
||||||
return found
|
return found
|
||||||
} else {
|
} else {
|
||||||
log.Errorf("subject: %s while creating %s", createErr, txt.Quote(m.SubjectName))
|
log.Errorf("subject: %s while creating %s", createErr, txt.Quote(m.SubjName))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -187,7 +192,7 @@ func FindSubject(s string) *Subject {
|
|||||||
|
|
||||||
result := Subject{}
|
result := Subject{}
|
||||||
|
|
||||||
db := Db().Where("subject_uid = ?", s)
|
db := Db().Where("subj_uid = ?", s)
|
||||||
|
|
||||||
if err := db.First(&result).Error; err != nil {
|
if err := db.First(&result).Error; err != nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -205,7 +210,7 @@ func FindSubjectByName(s string) *Subject {
|
|||||||
result := Subject{}
|
result := Subject{}
|
||||||
|
|
||||||
// Search database.
|
// Search database.
|
||||||
db := UnscopedDb().Where("subject_name LIKE ?", s).First(&result)
|
db := UnscopedDb().Where("subj_name LIKE ?", s).First(&result)
|
||||||
|
|
||||||
if err := db.First(&result).Error; err != nil {
|
if err := db.First(&result).Error; err != nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -213,9 +218,9 @@ func FindSubjectByName(s string) *Subject {
|
|||||||
|
|
||||||
// Restore if currently deleted.
|
// Restore if currently deleted.
|
||||||
if err := result.Restore(); err != nil {
|
if err := result.Restore(); err != nil {
|
||||||
log.Errorf("subject: %s could not be restored", result.SubjectUID)
|
log.Errorf("subject: %s could not be restored", result.SubjUID)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("subject: %s restored", result.SubjectUID)
|
log.Debugf("subject: %s restored", result.SubjUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result
|
return &result
|
||||||
@@ -223,7 +228,7 @@ func FindSubjectByName(s string) *Subject {
|
|||||||
|
|
||||||
// IsPerson tests if the subject is a person.
|
// IsPerson tests if the subject is a person.
|
||||||
func (m *Subject) IsPerson() bool {
|
func (m *Subject) IsPerson() bool {
|
||||||
return m.SubjectType == SubjectPerson
|
return m.SubjType == SubjPerson
|
||||||
}
|
}
|
||||||
|
|
||||||
// Person creates and returns a Person based on this subject.
|
// Person creates and returns a Person based on this subject.
|
||||||
@@ -239,8 +244,8 @@ func (m *Subject) SetName(name string) error {
|
|||||||
return fmt.Errorf("subject: name must not be empty")
|
return fmt.Errorf("subject: name must not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SubjectName = txt.Title(newName)
|
m.SubjName = txt.Title(newName)
|
||||||
m.SubjectSlug = slug.Make(txt.Clip(name, txt.ClipSlug))
|
m.SubjSlug = slug.Make(txt.Clip(name, txt.ClipSlug))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -249,15 +254,17 @@ func (m *Subject) SetName(name string) error {
|
|||||||
func (m *Subject) UpdateName(name string) (*Subject, error) {
|
func (m *Subject) UpdateName(name string) (*Subject, error) {
|
||||||
if err := m.SetName(name); err != nil {
|
if err := m.SetName(name); err != nil {
|
||||||
return m, err
|
return m, err
|
||||||
} else if err := m.Updates(Values{"SubjectName": m.SubjectName, "SubjectSlug": m.SubjectSlug}); err == nil {
|
} else if err := m.Updates(Values{"SubjName": m.SubjName, "SubjSlug": m.SubjSlug}); err == nil {
|
||||||
log.Infof("subject: renamed %s %s", m.SubjectType, txt.Quote(m.SubjectName))
|
log.Infof("subject: renamed %s %s", m.SubjType, txt.Quote(m.SubjName))
|
||||||
|
|
||||||
|
event.EntitiesUpdated("subjects", []*Subject{m})
|
||||||
|
|
||||||
if m.IsPerson() {
|
if m.IsPerson() {
|
||||||
event.EntitiesUpdated("people", []*Person{m.Person()})
|
event.EntitiesUpdated("people", []*Person{m.Person()})
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, m.UpdateMarkerNames()
|
return m, m.UpdateMarkerNames()
|
||||||
} else if existing := FindSubjectByName(m.SubjectName); existing == nil {
|
} else if existing := FindSubjectByName(m.SubjName); existing == nil {
|
||||||
return m, err
|
return m, err
|
||||||
} else {
|
} else {
|
||||||
return existing, m.MergeWith(existing)
|
return existing, m.MergeWith(existing)
|
||||||
@@ -266,16 +273,16 @@ func (m *Subject) UpdateName(name string) (*Subject, error) {
|
|||||||
|
|
||||||
// UpdateMarkerNames updates related marker names.
|
// UpdateMarkerNames updates related marker names.
|
||||||
func (m *Subject) UpdateMarkerNames() error {
|
func (m *Subject) UpdateMarkerNames() error {
|
||||||
if m.SubjectName == "" {
|
if m.SubjName == "" {
|
||||||
return fmt.Errorf("subject name is empty")
|
return fmt.Errorf("subject name is empty")
|
||||||
} else if m.SubjectUID == "" {
|
} else if m.SubjUID == "" {
|
||||||
return fmt.Errorf("subject uid is empty")
|
return fmt.Errorf("subject uid is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := Db().Model(&Marker{}).
|
if err := Db().Model(&Marker{}).
|
||||||
Where("subject_uid = ? AND subject_src <> ?", m.SubjectUID, SrcAuto).
|
Where("subj_uid = ? AND subj_src <> ?", m.SubjUID, SrcAuto).
|
||||||
Where("marker_name <> ?", m.SubjectName).
|
Where("marker_name <> ?", m.SubjName).
|
||||||
Update(Values{"MarkerName": m.SubjectName}).Error; err != nil {
|
Update(Values{"MarkerName": m.SubjName}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,33 +291,33 @@ func (m *Subject) UpdateMarkerNames() error {
|
|||||||
|
|
||||||
// RefreshPhotos flags related photos for metadata maintenance.
|
// RefreshPhotos flags related photos for metadata maintenance.
|
||||||
func (m *Subject) RefreshPhotos() error {
|
func (m *Subject) RefreshPhotos() error {
|
||||||
if m.SubjectUID == "" {
|
if m.SubjUID == "" {
|
||||||
return fmt.Errorf("empty subject uid")
|
return fmt.Errorf("empty subject uid")
|
||||||
}
|
}
|
||||||
|
|
||||||
return UnscopedDb().Exec(`UPDATE photos SET checked_at = NULL WHERE id IN
|
return UnscopedDb().Exec(`UPDATE photos SET checked_at = NULL WHERE id IN
|
||||||
(SELECT f.photo_id FROM files f JOIN ? m ON m.file_uid = f.file_uid WHERE m.subject_uid = ? GROUP BY f.photo_id)`,
|
(SELECT f.photo_id FROM files f JOIN ? m ON m.file_uid = f.file_uid WHERE m.subj_uid = ? GROUP BY f.photo_id)`,
|
||||||
gorm.Expr(Marker{}.TableName()), m.SubjectUID).Error
|
gorm.Expr(Marker{}.TableName()), m.SubjUID).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeWith merges this subject with another subject and then deletes it.
|
// MergeWith merges this subject with another subject and then deletes it.
|
||||||
func (m *Subject) MergeWith(other *Subject) error {
|
func (m *Subject) MergeWith(other *Subject) error {
|
||||||
if other == nil {
|
if other == nil {
|
||||||
return fmt.Errorf("other subject is nil")
|
return fmt.Errorf("other subject is nil")
|
||||||
} else if other.SubjectUID == "" {
|
} else if other.SubjUID == "" {
|
||||||
return fmt.Errorf("other subject's uid is empty")
|
return fmt.Errorf("other subject's uid is empty")
|
||||||
} else if m.SubjectUID == "" {
|
} else if m.SubjUID == "" {
|
||||||
return fmt.Errorf("subject uid is empty")
|
return fmt.Errorf("subject uid is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update markers and faces with new SubjectUID.
|
// Update markers and faces with new SubjUID.
|
||||||
if err := Db().Model(&Marker{}).
|
if err := Db().Model(&Marker{}).
|
||||||
Where("subject_uid = ?", m.SubjectUID).
|
Where("subj_uid = ?", m.SubjUID).
|
||||||
Update(Values{"SubjectUID": other.SubjectUID}).Error; err != nil {
|
Update(Values{"SubjUID": other.SubjUID}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
} else if err := Db().Model(&Face{}).
|
} else if err := Db().Model(&Face{}).
|
||||||
Where("subject_uid = ?", m.SubjectUID).
|
Where("subj_uid = ?", m.SubjUID).
|
||||||
Update(Values{"SubjectUID": other.SubjectUID}).Error; err != nil {
|
Update(Values{"SubjUID": other.SubjUID}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
} else if err := other.UpdateMarkerNames(); err != nil {
|
} else if err := other.UpdateMarkerNames(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -321,5 +328,5 @@ func (m *Subject) MergeWith(other *Subject) error {
|
|||||||
|
|
||||||
// Links returns all share links for this entity.
|
// Links returns all share links for this entity.
|
||||||
func (m *Subject) Links() Links {
|
func (m *Subject) Links() Links {
|
||||||
return FindLinks("", m.SubjectUID)
|
return FindLinks("", m.SubjUID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ func (m SubjectMap) Pointer(name string) *Subject {
|
|||||||
|
|
||||||
var SubjectFixtures = SubjectMap{
|
var SubjectFixtures = SubjectMap{
|
||||||
"john-doe": Subject{
|
"john-doe": Subject{
|
||||||
SubjectUID: "jqu0xs11qekk9jx8",
|
SubjUID: "jqu0xs11qekk9jx8",
|
||||||
SubjectSlug: "john-doe",
|
SubjSlug: "john-doe",
|
||||||
SubjectName: "John Doe",
|
SubjName: "John Doe",
|
||||||
SubjectType: SubjectPerson,
|
SubjType: SubjPerson,
|
||||||
SubjectSrc: SrcManual,
|
SubjSrc: SrcManual,
|
||||||
Favorite: true,
|
SubjFavorite: true,
|
||||||
Private: false,
|
SubjPrivate: false,
|
||||||
Excluded: false,
|
SubjExcluded: false,
|
||||||
SubjectBio: "Subject Description",
|
SubjBio: "Subject Description",
|
||||||
SubjectNotes: "Short Note",
|
SubjNotes: "Short Note",
|
||||||
MetadataJSON: []byte(""),
|
MetadataJSON: []byte(""),
|
||||||
FileCount: 1,
|
FileCount: 1,
|
||||||
CreatedAt: TimeStamp(),
|
CreatedAt: TimeStamp(),
|
||||||
@@ -37,16 +37,16 @@ var SubjectFixtures = SubjectMap{
|
|||||||
DeletedAt: nil,
|
DeletedAt: nil,
|
||||||
},
|
},
|
||||||
"joe-biden": Subject{
|
"joe-biden": Subject{
|
||||||
SubjectUID: "jqy3y652h8njw0sx",
|
SubjUID: "jqy3y652h8njw0sx",
|
||||||
SubjectSlug: "joe-biden",
|
SubjSlug: "joe-biden",
|
||||||
SubjectName: "Joe Biden",
|
SubjName: "Joe Biden",
|
||||||
SubjectType: SubjectPerson,
|
SubjType: SubjPerson,
|
||||||
SubjectSrc: SrcMarker,
|
SubjSrc: SrcMarker,
|
||||||
Favorite: false,
|
SubjFavorite: false,
|
||||||
Private: false,
|
SubjPrivate: false,
|
||||||
Excluded: false,
|
SubjExcluded: false,
|
||||||
SubjectBio: "",
|
SubjBio: "",
|
||||||
SubjectNotes: "",
|
SubjNotes: "",
|
||||||
MetadataJSON: []byte(""),
|
MetadataJSON: []byte(""),
|
||||||
FileCount: 1,
|
FileCount: 1,
|
||||||
CreatedAt: TimeStamp(),
|
CreatedAt: TimeStamp(),
|
||||||
@@ -54,17 +54,17 @@ var SubjectFixtures = SubjectMap{
|
|||||||
DeletedAt: nil,
|
DeletedAt: nil,
|
||||||
},
|
},
|
||||||
"dangling": Subject{
|
"dangling": Subject{
|
||||||
SubjectUID: "jqy1y111h1njaaaa",
|
SubjUID: "jqy1y111h1njaaaa",
|
||||||
SubjectSlug: "dangling-subject",
|
SubjSlug: "dangling-subject",
|
||||||
SubjectName: "Dangling Subject",
|
SubjName: "Dangling Subject",
|
||||||
SubjectAlias: "Powell",
|
SubjAlias: "Powell",
|
||||||
SubjectType: SubjectPerson,
|
SubjType: SubjPerson,
|
||||||
SubjectSrc: SrcMarker,
|
SubjSrc: SrcMarker,
|
||||||
Favorite: false,
|
SubjFavorite: false,
|
||||||
Private: false,
|
SubjPrivate: false,
|
||||||
Excluded: false,
|
SubjExcluded: false,
|
||||||
SubjectBio: "",
|
SubjBio: "",
|
||||||
SubjectNotes: "",
|
SubjNotes: "",
|
||||||
MetadataJSON: []byte(""),
|
MetadataJSON: []byte(""),
|
||||||
FileCount: 0,
|
FileCount: 0,
|
||||||
CreatedAt: TimeStamp(),
|
CreatedAt: TimeStamp(),
|
||||||
@@ -72,16 +72,16 @@ var SubjectFixtures = SubjectMap{
|
|||||||
DeletedAt: nil,
|
DeletedAt: nil,
|
||||||
},
|
},
|
||||||
"jane-doe": Subject{
|
"jane-doe": Subject{
|
||||||
SubjectUID: "jqy1y111h1njaaab",
|
SubjUID: "jqy1y111h1njaaab",
|
||||||
SubjectSlug: "jane-doe",
|
SubjSlug: "jane-doe",
|
||||||
SubjectName: "Jane Doe",
|
SubjName: "Jane Doe",
|
||||||
SubjectType: SubjectPerson,
|
SubjType: SubjPerson,
|
||||||
SubjectSrc: SrcMarker,
|
SubjSrc: SrcMarker,
|
||||||
Favorite: false,
|
SubjFavorite: false,
|
||||||
Private: false,
|
SubjPrivate: false,
|
||||||
Excluded: false,
|
SubjExcluded: false,
|
||||||
SubjectBio: "",
|
SubjBio: "",
|
||||||
SubjectNotes: "",
|
SubjNotes: "",
|
||||||
MetadataJSON: []byte(""),
|
MetadataJSON: []byte(""),
|
||||||
FileCount: 3,
|
FileCount: 3,
|
||||||
CreatedAt: TimeStamp(),
|
CreatedAt: TimeStamp(),
|
||||||
@@ -89,28 +89,28 @@ var SubjectFixtures = SubjectMap{
|
|||||||
DeletedAt: nil,
|
DeletedAt: nil,
|
||||||
},
|
},
|
||||||
"actress-1": Subject{
|
"actress-1": Subject{
|
||||||
SubjectUID: "jqy1y111h1njaaac",
|
SubjUID: "jqy1y111h1njaaac",
|
||||||
SubjectSlug: "actress-a",
|
SubjSlug: "actress-a",
|
||||||
SubjectName: "Actress A",
|
SubjName: "Actress A",
|
||||||
SubjectType: SubjectPerson,
|
SubjType: SubjPerson,
|
||||||
SubjectSrc: SrcMarker,
|
SubjSrc: SrcMarker,
|
||||||
Favorite: false,
|
SubjFavorite: false,
|
||||||
Private: false,
|
SubjPrivate: false,
|
||||||
SubjectNotes: "",
|
SubjNotes: "",
|
||||||
MetadataJSON: []byte(""),
|
MetadataJSON: []byte(""),
|
||||||
CreatedAt: TimeStamp(),
|
CreatedAt: TimeStamp(),
|
||||||
UpdatedAt: TimeStamp(),
|
UpdatedAt: TimeStamp(),
|
||||||
DeletedAt: nil,
|
DeletedAt: nil,
|
||||||
},
|
},
|
||||||
"actor-1": Subject{
|
"actor-1": Subject{
|
||||||
SubjectUID: "jqy1y111h1njaaad",
|
SubjUID: "jqy1y111h1njaaad",
|
||||||
SubjectSlug: "actor-a",
|
SubjSlug: "actor-a",
|
||||||
SubjectName: "Actor A",
|
SubjName: "Actor A",
|
||||||
SubjectType: SubjectPerson,
|
SubjType: SubjPerson,
|
||||||
SubjectSrc: SrcMarker,
|
SubjSrc: SrcMarker,
|
||||||
Favorite: false,
|
SubjFavorite: false,
|
||||||
Private: false,
|
SubjPrivate: false,
|
||||||
SubjectNotes: "",
|
SubjNotes: "",
|
||||||
MetadataJSON: []byte(""),
|
MetadataJSON: []byte(""),
|
||||||
CreatedAt: TimeStamp(),
|
CreatedAt: TimeStamp(),
|
||||||
UpdatedAt: TimeStamp(),
|
UpdatedAt: TimeStamp(),
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import (
|
|||||||
func TestSubjectMap_Get(t *testing.T) {
|
func TestSubjectMap_Get(t *testing.T) {
|
||||||
t.Run("get existing subject", func(t *testing.T) {
|
t.Run("get existing subject", func(t *testing.T) {
|
||||||
r := SubjectFixtures.Get("joe-biden")
|
r := SubjectFixtures.Get("joe-biden")
|
||||||
assert.Equal(t, "Joe Biden", r.SubjectName)
|
assert.Equal(t, "Joe Biden", r.SubjName)
|
||||||
assert.IsType(t, Subject{}, r)
|
assert.IsType(t, Subject{}, r)
|
||||||
})
|
})
|
||||||
t.Run("get not existing subject", func(t *testing.T) {
|
t.Run("get not existing subject", func(t *testing.T) {
|
||||||
r := SubjectFixtures.Get("monstera")
|
r := SubjectFixtures.Get("monstera")
|
||||||
assert.Equal(t, "", r.SubjectName)
|
assert.Equal(t, "", r.SubjName)
|
||||||
assert.IsType(t, Subject{}, r)
|
assert.IsType(t, Subject{}, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -22,12 +22,12 @@ func TestSubjectMap_Get(t *testing.T) {
|
|||||||
func TestSubjectMap_Pointer(t *testing.T) {
|
func TestSubjectMap_Pointer(t *testing.T) {
|
||||||
t.Run("get existing subject", func(t *testing.T) {
|
t.Run("get existing subject", func(t *testing.T) {
|
||||||
r := SubjectFixtures.Pointer("joe-biden")
|
r := SubjectFixtures.Pointer("joe-biden")
|
||||||
assert.Equal(t, "Joe Biden", r.SubjectName)
|
assert.Equal(t, "Joe Biden", r.SubjName)
|
||||||
assert.IsType(t, &Subject{}, r)
|
assert.IsType(t, &Subject{}, r)
|
||||||
})
|
})
|
||||||
t.Run("get not existing subject", func(t *testing.T) {
|
t.Run("get not existing subject", func(t *testing.T) {
|
||||||
r := SubjectFixtures.Pointer("monstera")
|
r := SubjectFixtures.Pointer("monstera")
|
||||||
assert.Equal(t, "", r.SubjectName)
|
assert.Equal(t, "", r.SubjName)
|
||||||
assert.IsType(t, &Subject{}, r)
|
assert.IsType(t, &Subject{}, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SubjectPerson = "person"
|
SubjPerson = "person"
|
||||||
)
|
)
|
||||||
|
|
||||||
// People represents a list of people.
|
// People represents a list of people.
|
||||||
@@ -15,20 +15,20 @@ type People []Person
|
|||||||
|
|
||||||
// Person represents a subject with type person.
|
// Person represents a subject with type person.
|
||||||
type Person struct {
|
type Person struct {
|
||||||
SubjectUID string `json:"UID"`
|
SubjUID string `json:"UID"`
|
||||||
SubjectName string `json:"Name"`
|
SubjName string `json:"Name"`
|
||||||
SubjectAlias string `json:"Alias,omitempty"`
|
SubjAlias string `json:"Alias"`
|
||||||
Favorite bool `json:"Favorite,omitempty"`
|
SubjFavorite bool `json:"Favorite"`
|
||||||
Thumb string `json:",omitempty"`
|
Thumb string `json:"Thumb"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPerson returns a new entity.
|
// NewPerson returns a new entity.
|
||||||
func NewPerson(subj Subject) *Person {
|
func NewPerson(subj Subject) *Person {
|
||||||
result := &Person{
|
result := &Person{
|
||||||
SubjectUID: subj.SubjectUID,
|
SubjUID: subj.SubjUID,
|
||||||
SubjectName: subj.SubjectName,
|
SubjName: subj.SubjName,
|
||||||
SubjectAlias: subj.SubjectAlias,
|
SubjAlias: subj.SubjAlias,
|
||||||
Favorite: subj.Favorite,
|
SubjFavorite: subj.SubjFavorite,
|
||||||
Thumb: subj.Thumb,
|
Thumb: subj.Thumb,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,10 +44,10 @@ func (m *Person) MarshalJSON() ([]byte, error) {
|
|||||||
Favorite bool `json:",omitempty"`
|
Favorite bool `json:",omitempty"`
|
||||||
Thumb string `json:",omitempty"`
|
Thumb string `json:",omitempty"`
|
||||||
}{
|
}{
|
||||||
UID: m.SubjectUID,
|
UID: m.SubjUID,
|
||||||
Name: m.SubjectName,
|
Name: m.SubjName,
|
||||||
Keywords: txt.NameKeywords(m.SubjectName, m.SubjectAlias),
|
Keywords: txt.NameKeywords(m.SubjName, m.SubjAlias),
|
||||||
Favorite: m.Favorite,
|
Favorite: m.SubjFavorite,
|
||||||
Thumb: m.Thumb,
|
Thumb: m.Thumb,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,19 @@ import (
|
|||||||
func TestNewPerson(t *testing.T) {
|
func TestNewPerson(t *testing.T) {
|
||||||
t.Run("BillGates", func(t *testing.T) {
|
t.Run("BillGates", func(t *testing.T) {
|
||||||
subj := Subject{
|
subj := Subject{
|
||||||
SubjectUID: "jqytw12v8jjeu3e6",
|
SubjUID: "jqytw12v8jjeu3e6",
|
||||||
SubjectName: "William Henry Gates III",
|
SubjName: "William Henry Gates III",
|
||||||
SubjectAlias: "Windows Guru",
|
SubjAlias: "Windows Guru",
|
||||||
Favorite: true,
|
SubjFavorite: true,
|
||||||
Thumb: "622c7287967f2800e873fbc55f0328973056ce1d",
|
Thumb: "622c7287967f2800e873fbc55f0328973056ce1d",
|
||||||
}
|
}
|
||||||
|
|
||||||
m := NewPerson(subj)
|
m := NewPerson(subj)
|
||||||
|
|
||||||
assert.Equal(t, "jqytw12v8jjeu3e6", m.SubjectUID)
|
assert.Equal(t, "jqytw12v8jjeu3e6", m.SubjUID)
|
||||||
assert.Equal(t, "William Henry Gates III", m.SubjectName)
|
assert.Equal(t, "William Henry Gates III", m.SubjName)
|
||||||
assert.Equal(t, "Windows Guru", m.SubjectAlias)
|
assert.Equal(t, "Windows Guru", m.SubjAlias)
|
||||||
assert.Equal(t, true, m.Favorite)
|
assert.Equal(t, true, m.SubjFavorite)
|
||||||
assert.Equal(t, "622c7287967f2800e873fbc55f0328973056ce1d", m.Thumb)
|
assert.Equal(t, "622c7287967f2800e873fbc55f0328973056ce1d", m.Thumb)
|
||||||
|
|
||||||
if j, err := m.MarshalJSON(); err != nil {
|
if j, err := m.MarshalJSON(); err != nil {
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ func TestSubject_TableName(t *testing.T) {
|
|||||||
|
|
||||||
func TestNewSubject(t *testing.T) {
|
func TestNewSubject(t *testing.T) {
|
||||||
t.Run("Jens_Mander", func(t *testing.T) {
|
t.Run("Jens_Mander", func(t *testing.T) {
|
||||||
m := NewSubject("Jens Mander", SubjectPerson, SrcAuto)
|
m := NewSubject("Jens Mander", SubjPerson, SrcAuto)
|
||||||
assert.Equal(t, "Jens Mander", m.SubjectName)
|
assert.Equal(t, "Jens Mander", m.SubjName)
|
||||||
assert.Equal(t, "jens-mander", m.SubjectSlug)
|
assert.Equal(t, "jens-mander", m.SubjSlug)
|
||||||
assert.Equal(t, "person", m.SubjectType)
|
assert.Equal(t, "person", m.SubjType)
|
||||||
})
|
})
|
||||||
t.Run("subject Type empty", func(t *testing.T) {
|
t.Run("subject Type empty", func(t *testing.T) {
|
||||||
m := NewSubject("Anna Mander", "", SrcAuto)
|
m := NewSubject("Anna Mander", "", SrcAuto)
|
||||||
assert.Equal(t, "Anna Mander", m.SubjectName)
|
assert.Equal(t, "Anna Mander", m.SubjName)
|
||||||
assert.Equal(t, "anna-mander", m.SubjectSlug)
|
assert.Equal(t, "anna-mander", m.SubjSlug)
|
||||||
assert.Equal(t, "person", m.SubjectType)
|
assert.Equal(t, "person", m.SubjType)
|
||||||
})
|
})
|
||||||
t.Run("subject name empty", func(t *testing.T) {
|
t.Run("subject name empty", func(t *testing.T) {
|
||||||
m := NewSubject("", "", SrcAuto)
|
m := NewSubject("", "", SrcAuto)
|
||||||
@@ -33,44 +33,44 @@ func TestNewSubject(t *testing.T) {
|
|||||||
|
|
||||||
func TestSubject_SetName(t *testing.T) {
|
func TestSubject_SetName(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
m := NewSubject("Jens Mander", SubjectPerson, SrcAuto)
|
m := NewSubject("Jens Mander", SubjPerson, SrcAuto)
|
||||||
|
|
||||||
assert.Equal(t, "Jens Mander", m.SubjectName)
|
assert.Equal(t, "Jens Mander", m.SubjName)
|
||||||
assert.Equal(t, "jens-mander", m.SubjectSlug)
|
assert.Equal(t, "jens-mander", m.SubjSlug)
|
||||||
|
|
||||||
if err := m.SetName("Foo McBar"); err != nil {
|
if err := m.SetName("Foo McBar"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "Foo McBar", m.SubjectName)
|
assert.Equal(t, "Foo McBar", m.SubjName)
|
||||||
assert.Equal(t, "foo-mcbar", m.SubjectSlug)
|
assert.Equal(t, "foo-mcbar", m.SubjSlug)
|
||||||
})
|
})
|
||||||
t.Run("new name empty", func(t *testing.T) {
|
t.Run("new name empty", func(t *testing.T) {
|
||||||
m := NewSubject("Jens Mander", SubjectPerson, SrcAuto)
|
m := NewSubject("Jens Mander", SubjPerson, SrcAuto)
|
||||||
|
|
||||||
assert.Equal(t, "Jens Mander", m.SubjectName)
|
assert.Equal(t, "Jens Mander", m.SubjName)
|
||||||
assert.Equal(t, "jens-mander", m.SubjectSlug)
|
assert.Equal(t, "jens-mander", m.SubjSlug)
|
||||||
|
|
||||||
err := m.SetName("")
|
err := m.SetName("")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
assert.Equal(t, "subject: name must not be empty", err.Error())
|
assert.Equal(t, "subject: name must not be empty", err.Error())
|
||||||
assert.Equal(t, "Jens Mander", m.SubjectName)
|
assert.Equal(t, "Jens Mander", m.SubjName)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFirstOrCreatePerson(t *testing.T) {
|
func TestFirstOrCreatePerson(t *testing.T) {
|
||||||
t.Run("not yet existing person", func(t *testing.T) {
|
t.Run("not yet existing person", func(t *testing.T) {
|
||||||
m := NewSubject("Create Me", SubjectPerson, SrcAuto)
|
m := NewSubject("Create Me", SubjPerson, SrcAuto)
|
||||||
result := FirstOrCreateSubject(m)
|
result := FirstOrCreateSubject(m)
|
||||||
|
|
||||||
if result == nil {
|
if result == nil {
|
||||||
t.Fatal("result should not be nil")
|
t.Fatal("result should not be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "Create Me", m.SubjectName)
|
assert.Equal(t, "Create Me", m.SubjName)
|
||||||
assert.Equal(t, "create-me", m.SubjectSlug)
|
assert.Equal(t, "create-me", m.SubjSlug)
|
||||||
})
|
})
|
||||||
t.Run("existing person", func(t *testing.T) {
|
t.Run("existing person", func(t *testing.T) {
|
||||||
m := SubjectFixtures.Pointer("john-doe")
|
m := SubjectFixtures.Pointer("john-doe")
|
||||||
@@ -80,15 +80,15 @@ func TestFirstOrCreatePerson(t *testing.T) {
|
|||||||
t.Fatal("result should not be nil")
|
t.Fatal("result should not be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "John Doe", m.SubjectName)
|
assert.Equal(t, "John Doe", m.SubjName)
|
||||||
assert.Equal(t, "john-doe", m.SubjectSlug)
|
assert.Equal(t, "john-doe", m.SubjSlug)
|
||||||
assert.Equal(t, "Short Note", m.SubjectNotes)
|
assert.Equal(t, "Short Note", m.SubjNotes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSubject_Save(t *testing.T) {
|
func TestSubject_Save(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
m := NewSubject("Save Me", SubjectPerson, SrcAuto)
|
m := NewSubject("Save Me", SubjPerson, SrcAuto)
|
||||||
initialDate := m.UpdatedAt
|
initialDate := m.UpdatedAt
|
||||||
err := m.Save()
|
err := m.Save()
|
||||||
|
|
||||||
@@ -105,13 +105,13 @@ func TestSubject_Save(t *testing.T) {
|
|||||||
|
|
||||||
func TestSubject_Delete(t *testing.T) {
|
func TestSubject_Delete(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
m := NewSubject("Jens Mander", SubjectPerson, SrcAuto)
|
m := NewSubject("Jens Mander", SubjPerson, SrcAuto)
|
||||||
err := m.Save()
|
err := m.Save()
|
||||||
assert.False(t, m.Deleted())
|
assert.False(t, m.Deleted())
|
||||||
|
|
||||||
var subj Subjects
|
var subj Subjects
|
||||||
|
|
||||||
if err := Db().Where("subject_name = ?", m.SubjectName).Find(&subj).Error; err != nil {
|
if err := Db().Where("subj_name = ?", m.SubjName).Find(&subj).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ func TestSubject_Delete(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := Db().Where("subject_name = ?", m.SubjectName).Find(&subj).Error; err != nil {
|
if err := Db().Where("subj_name = ?", m.SubjName).Find(&subj).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ func TestSubject_Restore(t *testing.T) {
|
|||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
var deleteTime = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
var deleteTime = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
m := &Subject{DeletedAt: &deleteTime, SubjectName: "ToBeRestored"}
|
m := &Subject{DeletedAt: &deleteTime, SubjName: "ToBeRestored"}
|
||||||
err := m.Save()
|
err := m.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -148,7 +148,7 @@ func TestSubject_Restore(t *testing.T) {
|
|||||||
assert.False(t, m.Deleted())
|
assert.False(t, m.Deleted())
|
||||||
})
|
})
|
||||||
t.Run("subject not deleted", func(t *testing.T) {
|
t.Run("subject not deleted", func(t *testing.T) {
|
||||||
m := &Subject{DeletedAt: nil, SubjectName: "NotDeleted1234"}
|
m := &Subject{DeletedAt: nil, SubjName: "NotDeleted1234"}
|
||||||
err := m.Restore()
|
err := m.Restore()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -159,18 +159,18 @@ func TestSubject_Restore(t *testing.T) {
|
|||||||
|
|
||||||
func TestFindSubject(t *testing.T) {
|
func TestFindSubject(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
m := NewSubject("Find Me", SubjectPerson, SrcAuto)
|
m := NewSubject("Find Me", SubjPerson, SrcAuto)
|
||||||
|
|
||||||
if err := m.Save(); err != nil {
|
if err := m.Save(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s := FindSubject(m.SubjectName); s != nil {
|
if s := FindSubject(m.SubjName); s != nil {
|
||||||
t.Fatal("result must be nil")
|
t.Fatal("result must be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s := FindSubject(m.SubjectUID); s != nil {
|
if s := FindSubject(m.SubjUID); s != nil {
|
||||||
assert.Equal(t, "Find Me", s.SubjectName)
|
assert.Equal(t, "Find Me", s.SubjName)
|
||||||
} else {
|
} else {
|
||||||
t.Fatal("result must not be nil")
|
t.Fatal("result must not be nil")
|
||||||
}
|
}
|
||||||
@@ -195,33 +195,33 @@ func TestSubject_Links(t *testing.T) {
|
|||||||
|
|
||||||
func TestSubject_Update(t *testing.T) {
|
func TestSubject_Update(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
m := NewSubject("Update Me", SubjectPerson, SrcAuto)
|
m := NewSubject("Update Me", SubjPerson, SrcAuto)
|
||||||
|
|
||||||
if err := m.Save(); err != nil {
|
if err := m.Save(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.Update("SubjectName", "Updated Name"); err != nil {
|
if err := m.Update("SubjName", "Updated Name"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, "Updated Name", m.SubjectName)
|
assert.Equal(t, "Updated Name", m.SubjName)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
func TestSubject_Updates(t *testing.T) {
|
func TestSubject_Updates(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
m := NewSubject("Update Me", SubjectPerson, SrcAuto)
|
m := NewSubject("Update Me", SubjPerson, SrcAuto)
|
||||||
|
|
||||||
if err := m.Save(); err != nil {
|
if err := m.Save(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.Updates(Subject{SubjectName: "UpdatedName", SubjectType: "UpdatedType"}); err != nil {
|
if err := m.Updates(Subject{SubjName: "UpdatedName", SubjType: "UpdatedType"}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, "UpdatedName", m.SubjectName)
|
assert.Equal(t, "UpdatedName", m.SubjName)
|
||||||
assert.Equal(t, "UpdatedType", m.SubjectType)
|
assert.Equal(t, "UpdatedType", m.SubjType)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -229,45 +229,45 @@ func TestSubject_Updates(t *testing.T) {
|
|||||||
|
|
||||||
func TestSubject_UpdateName(t *testing.T) {
|
func TestSubject_UpdateName(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
m := NewSubject("Test Person", SubjectPerson, SrcAuto)
|
m := NewSubject("Test Person", SubjPerson, SrcAuto)
|
||||||
|
|
||||||
if err := m.Save(); err != nil {
|
if err := m.Save(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "Test Person", m.SubjectName)
|
assert.Equal(t, "Test Person", m.SubjName)
|
||||||
assert.Equal(t, "test-person", m.SubjectSlug)
|
assert.Equal(t, "test-person", m.SubjSlug)
|
||||||
|
|
||||||
if s, err := m.UpdateName("New New"); err != nil {
|
if s, err := m.UpdateName("New New"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
} else if s == nil {
|
} else if s == nil {
|
||||||
t.Fatal("subject is nil")
|
t.Fatal("subject is nil")
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, "New New", m.SubjectName)
|
assert.Equal(t, "New New", m.SubjName)
|
||||||
assert.Equal(t, "new-new", m.SubjectSlug)
|
assert.Equal(t, "new-new", m.SubjSlug)
|
||||||
assert.Equal(t, "New New", s.SubjectName)
|
assert.Equal(t, "New New", s.SubjName)
|
||||||
assert.Equal(t, "new-new", s.SubjectSlug)
|
assert.Equal(t, "new-new", s.SubjSlug)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("empty name", func(t *testing.T) {
|
t.Run("empty name", func(t *testing.T) {
|
||||||
m := NewSubject("Test Person2", SubjectPerson, SrcAuto)
|
m := NewSubject("Test Person2", SubjPerson, SrcAuto)
|
||||||
|
|
||||||
if err := m.Save(); err != nil {
|
if err := m.Save(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "Test Person2", m.SubjectName)
|
assert.Equal(t, "Test Person2", m.SubjName)
|
||||||
assert.Equal(t, "test-person2", m.SubjectSlug)
|
assert.Equal(t, "test-person2", m.SubjSlug)
|
||||||
|
|
||||||
if s, err := m.UpdateName(""); err == nil {
|
if s, err := m.UpdateName(""); err == nil {
|
||||||
t.Error("error expected")
|
t.Error("error expected")
|
||||||
} else if s == nil {
|
} else if s == nil {
|
||||||
t.Fatal("subject is nil")
|
t.Fatal("subject is nil")
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, "Test Person2", m.SubjectName)
|
assert.Equal(t, "Test Person2", m.SubjName)
|
||||||
assert.Equal(t, "test-person2", m.SubjectSlug)
|
assert.Equal(t, "test-person2", m.SubjSlug)
|
||||||
assert.Equal(t, "Test Person2", s.SubjectName)
|
assert.Equal(t, "Test Person2", s.SubjName)
|
||||||
assert.Equal(t, "test-person2", s.SubjectSlug)
|
assert.Equal(t, "test-person2", s.SubjSlug)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import "github.com/ulule/deepcopier"
|
|||||||
|
|
||||||
// Marker represents an image marker edit form.
|
// Marker represents an image marker edit form.
|
||||||
type Marker struct {
|
type Marker struct {
|
||||||
SubjectSrc string `json:"SubjectSrc"`
|
SubjSrc string `json:"SubjSrc"`
|
||||||
MarkerName string `json:"Name"`
|
MarkerName string `json:"Name"`
|
||||||
Review bool `json:"Review"`
|
MarkerReview bool `json:"MarkerReview"`
|
||||||
MarkerInvalid bool `json:"Invalid"`
|
MarkerInvalid bool `json:"Invalid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import (
|
|||||||
func TestNewMarker(t *testing.T) {
|
func TestNewMarker(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
var m = struct {
|
var m = struct {
|
||||||
SubjectSrc string
|
SubjSrc string
|
||||||
MarkerName string
|
MarkerName string
|
||||||
Review bool
|
MarkerReview bool
|
||||||
MarkerInvalid bool
|
MarkerInvalid bool
|
||||||
}{
|
}{
|
||||||
SubjectSrc: "manual",
|
SubjSrc: "manual",
|
||||||
MarkerName: "Foo",
|
MarkerName: "Foo",
|
||||||
Review: true,
|
MarkerReview: true,
|
||||||
MarkerInvalid: true,
|
MarkerInvalid: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,9 +26,9 @@ func TestNewMarker(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, "manual", f.SubjectSrc)
|
assert.Equal(t, "manual", f.SubjSrc)
|
||||||
assert.Equal(t, "Foo", f.MarkerName)
|
assert.Equal(t, "Foo", f.MarkerName)
|
||||||
assert.Equal(t, true, f.Review)
|
assert.Equal(t, true, f.MarkerReview)
|
||||||
assert.Equal(t, true, f.MarkerInvalid)
|
assert.Equal(t, true, f.MarkerInvalid)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package form
|
|||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
type Selection struct {
|
type Selection struct {
|
||||||
Files []string `json:"files"`
|
Files []string `json:"files"`
|
||||||
Photos []string `json:"photos"`
|
Photos []string `json:"photos"`
|
||||||
Albums []string `json:"albums"`
|
Albums []string `json:"albums"`
|
||||||
Labels []string `json:"labels"`
|
Labels []string `json:"labels"`
|
||||||
Places []string `json:"places"`
|
Places []string `json:"places"`
|
||||||
|
Subjects []string `json:"subjects"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f Selection) Empty() bool {
|
func (f Selection) Empty() bool {
|
||||||
@@ -22,6 +23,8 @@ func (f Selection) Empty() bool {
|
|||||||
return false
|
return false
|
||||||
case len(f.Places) > 0:
|
case len(f.Places) > 0:
|
||||||
return false
|
return false
|
||||||
|
case len(f.Subjects) > 0:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -36,6 +39,7 @@ func (f Selection) All() []string {
|
|||||||
all = append(all, f.Albums...)
|
all = append(all, f.Albums...)
|
||||||
all = append(all, f.Labels...)
|
all = append(all, f.Labels...)
|
||||||
all = append(all, f.Places...)
|
all = append(all, f.Places...)
|
||||||
|
all = append(all, f.Subjects...)
|
||||||
|
|
||||||
return all
|
return all
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ func TestSelection_Empty(t *testing.T) {
|
|||||||
sel := Selection{Photos: []string{}, Albums: []string{}, Labels: []string{}, Files: []string{}, Places: []string{"foo", "bar"}}
|
sel := Selection{Photos: []string{}, Albums: []string{}, Labels: []string{}, Files: []string{}, Places: []string{"foo", "bar"}}
|
||||||
assert.Equal(t, false, sel.Empty())
|
assert.Equal(t, false, sel.Empty())
|
||||||
})
|
})
|
||||||
|
t.Run("not empty subjects", func(t *testing.T) {
|
||||||
|
sel := Selection{Photos: []string{}, Albums: []string{}, Labels: []string{}, Files: []string{}, Places: []string{}, Subjects: []string{"jqzkpo13j8ngpgv4", "jqzkq8j10hj39sxp"}}
|
||||||
|
assert.Equal(t, false, sel.Empty())
|
||||||
|
assert.Equal(t, []string{"jqzkpo13j8ngpgv4", "jqzkq8j10hj39sxp"}, sel.Subjects)
|
||||||
|
})
|
||||||
t.Run("empty", func(t *testing.T) {
|
t.Run("empty", func(t *testing.T) {
|
||||||
sel := Selection{Photos: []string{}, Albums: []string{}, Labels: []string{}}
|
sel := Selection{Photos: []string{}, Albums: []string{}, Labels: []string{}}
|
||||||
assert.Equal(t, true, sel.Empty())
|
assert.Equal(t, true, sel.Empty())
|
||||||
@@ -35,8 +40,8 @@ func TestSelection_Empty(t *testing.T) {
|
|||||||
|
|
||||||
func TestSelection_All(t *testing.T) {
|
func TestSelection_All(t *testing.T) {
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
sel := Selection{Photos: []string{"p123", "p456"}, Albums: []string{"a123"}, Labels: []string{"l123", "l456", "l789"}, Files: []string{"f567", "f111"}, Places: []string{"p568"}}
|
sel := Selection{Photos: []string{"p123", "p456"}, Albums: []string{"a123"}, Labels: []string{"l123", "l456", "l789"}, Files: []string{"f567", "f111"}, Places: []string{"p568"}, Subjects: []string{"jqzkpo13j8ngpgv4"}}
|
||||||
assert.Equal(t, []string{"p123", "p456", "a123", "l123", "l456", "l789", "p568"}, sel.All())
|
assert.Equal(t, []string{"p123", "p456", "a123", "l123", "l456", "l789", "p568", "jqzkpo13j8ngpgv4"}, sel.All())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
internal/form/subject.go
Normal file
20
internal/form/subject.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package form
|
||||||
|
|
||||||
|
import "github.com/ulule/deepcopier"
|
||||||
|
|
||||||
|
// Subject represents an image subject edit form.
|
||||||
|
type Subject struct {
|
||||||
|
SubjName string `json:"Name"`
|
||||||
|
SubjAlias string `json:"Alias"`
|
||||||
|
SubjBio string `json:"Bio"`
|
||||||
|
SubjNotes string `json:"Notes"`
|
||||||
|
SubjFavorite bool `json:"Favorite"`
|
||||||
|
SubjPrivate bool `json:"Private"`
|
||||||
|
SubjExcluded bool `json:"Excluded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubject(m interface{}) (f Subject, err error) {
|
||||||
|
err = deepcopier.Copy(m).To(&f)
|
||||||
|
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
37
internal/form/subject_test.go
Normal file
37
internal/form/subject_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package form
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewSubject(t *testing.T) {
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
var m = struct {
|
||||||
|
SubjName string `json:"Name"`
|
||||||
|
SubjAlias string `json:"Alias"`
|
||||||
|
SubjBio string `json:"Bio"`
|
||||||
|
SubjNotes string `json:"Notes"`
|
||||||
|
SubjFavorite bool `json:"Favorite"`
|
||||||
|
SubjPrivate bool `json:"Private"`
|
||||||
|
SubjExcluded bool `json:"Excluded"`
|
||||||
|
}{
|
||||||
|
SubjName: "Foo",
|
||||||
|
SubjAlias: "bar",
|
||||||
|
SubjFavorite: true,
|
||||||
|
SubjExcluded: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := NewSubject(m)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "Foo", f.SubjName)
|
||||||
|
assert.Equal(t, "bar", f.SubjAlias)
|
||||||
|
assert.Equal(t, true, f.SubjFavorite)
|
||||||
|
assert.Equal(t, false, f.SubjExcluded)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -60,6 +60,8 @@ const (
|
|||||||
MsgCopyingFilesFrom
|
MsgCopyingFilesFrom
|
||||||
MsgLabelsDeleted
|
MsgLabelsDeleted
|
||||||
MsgLabelSaved
|
MsgLabelSaved
|
||||||
|
MsgSubjectSaved
|
||||||
|
MsgSubjectDeleted
|
||||||
MsgFilesUploadedIn
|
MsgFilesUploadedIn
|
||||||
MsgSelectionApproved
|
MsgSelectionApproved
|
||||||
MsgSelectionArchived
|
MsgSelectionArchived
|
||||||
@@ -132,6 +134,8 @@ var Messages = MessageMap{
|
|||||||
MsgCopyingFilesFrom: gettext("Copying files from %s"),
|
MsgCopyingFilesFrom: gettext("Copying files from %s"),
|
||||||
MsgLabelsDeleted: gettext("Labels deleted"),
|
MsgLabelsDeleted: gettext("Labels deleted"),
|
||||||
MsgLabelSaved: gettext("Label saved"),
|
MsgLabelSaved: gettext("Label saved"),
|
||||||
|
MsgSubjectSaved: gettext("Subject saved"),
|
||||||
|
MsgSubjectDeleted: gettext("Subject deleted"),
|
||||||
MsgFilesUploadedIn: gettext("%d files uploaded in %d s"),
|
MsgFilesUploadedIn: gettext("%d files uploaded in %d s"),
|
||||||
MsgSelectionApproved: gettext("Selection approved"),
|
MsgSelectionApproved: gettext("Selection approved"),
|
||||||
MsgSelectionArchived: gettext("Selection archived"),
|
MsgSelectionArchived: gettext("Selection archived"),
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (w *Faces) Audit(fix bool) (err error) {
|
|||||||
|
|
||||||
for _, f2 := range faces {
|
for _, f2 := range faces {
|
||||||
if matched, dist := f1.Match(entity.Embeddings{f2.Embedding()}); matched {
|
if matched, dist := f1.Match(entity.Embeddings{f2.Embedding()}); matched {
|
||||||
if f1.SubjectUID == f2.SubjectUID {
|
if f1.SubjUID == f2.SubjUID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,14 +75,14 @@ func (w *Faces) Audit(fix bool) (err error) {
|
|||||||
|
|
||||||
log.Infof("face %s: conflict at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius)
|
log.Infof("face %s: conflict at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius)
|
||||||
|
|
||||||
if f1.SubjectUID != "" {
|
if f1.SubjUID != "" {
|
||||||
log.Infof("face %s: subject %s (%s %s)", f1.ID, txt.Quote(subj[f1.SubjectUID].SubjectName), f1.SubjectUID, entity.SrcString(f1.FaceSrc))
|
log.Infof("face %s: subject %s (%s %s)", f1.ID, txt.Quote(subj[f1.SubjUID].SubjName), f1.SubjUID, entity.SrcString(f1.FaceSrc))
|
||||||
} else {
|
} else {
|
||||||
log.Infof("face %s: no subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc))
|
log.Infof("face %s: no subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc))
|
||||||
}
|
}
|
||||||
|
|
||||||
if f2.SubjectUID != "" {
|
if f2.SubjUID != "" {
|
||||||
log.Infof("face %s: subject %s (%s %s)", f2.ID, txt.Quote(subj[f2.SubjectUID].SubjectName), f2.SubjectUID, entity.SrcString(f2.FaceSrc))
|
log.Infof("face %s: subject %s (%s %s)", f2.ID, txt.Quote(subj[f2.SubjUID].SubjName), f2.SubjUID, entity.SrcString(f2.FaceSrc))
|
||||||
} else {
|
} else {
|
||||||
log.Infof("face %s: no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc))
|
log.Infof("face %s: no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc))
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ func (w *Faces) Audit(fix bool) (err error) {
|
|||||||
log.Error(err)
|
log.Error(err)
|
||||||
} else {
|
} else {
|
||||||
for _, m := range markers {
|
for _, m := range markers {
|
||||||
log.Infof("marker %s: %s subject %s conflicts with face %s subject %s", m.MarkerUID, entity.SrcString(m.SubjectSrc), txt.Quote(subj[m.SubjectUID].SubjectName), m.FaceID, txt.Quote(subj[faceMap[m.FaceID].SubjectUID].SubjectName))
|
log.Infof("marker %s: %s subject %s conflicts with face %s subject %s", m.MarkerUID, entity.SrcString(m.SubjSrc), txt.Quote(subj[m.SubjUID].SubjName), m.FaceID, txt.Quote(subj[faceMap[m.FaceID].SubjUID].SubjName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ func (w *Faces) MatchFaces(faces entity.Faces, force bool, matchedBefore *time.T
|
|||||||
result.Updated++
|
result.Updated++
|
||||||
}
|
}
|
||||||
|
|
||||||
if marker.SubjectUID != "" {
|
if marker.SubjUID != "" {
|
||||||
result.Recognized++
|
result.Recognized++
|
||||||
} else {
|
} else {
|
||||||
result.Unknown++
|
result.Unknown++
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func (w *Faces) Optimize() (result FacesOptimizeResult, err error) {
|
|||||||
for j := 0; j <= n; j++ {
|
for j := 0; j <= n; j++ {
|
||||||
if len(merge) == 0 {
|
if len(merge) == 0 {
|
||||||
merge = entity.Faces{faces[j]}
|
merge = entity.Faces{faces[j]}
|
||||||
} else if faces[j].SubjectUID != merge[len(merge)-1].SubjectUID || j == n {
|
} else if faces[j].SubjUID != merge[len(merge)-1].SubjUID || j == n {
|
||||||
if len(merge) < 2 {
|
if len(merge) < 2 {
|
||||||
// Nothing to merge.
|
// Nothing to merge.
|
||||||
} else if _, err := query.MergeFaces(merge); err != nil {
|
} else if _, err := query.MergeFaces(merge); err != nil {
|
||||||
@@ -48,7 +48,7 @@ func (w *Faces) Optimize() (result FacesOptimizeResult, err error) {
|
|||||||
|
|
||||||
merge = nil
|
merge = nil
|
||||||
} else if ok, dist := merge[0].Match(entity.Embeddings{faces[j].Embedding()}); ok {
|
} else if ok, dist := merge[0].Match(entity.Embeddings{faces[j].Embedding()}); ok {
|
||||||
log.Debugf("faces: can merge %s with %s, subject %s, dist %f", merge[0].ID, faces[j].ID, merge[0].SubjectUID, dist)
|
log.Debugf("faces: can merge %s with %s, subject %s, dist %f", merge[0].ID, faces[j].ID, merge[0].SubjUID, dist)
|
||||||
merge = append(merge, faces[j])
|
merge = append(merge, faces[j])
|
||||||
} else if len(merge) == 1 {
|
} else if len(merge) == 1 {
|
||||||
merge = nil
|
merge = nil
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func (w *Faces) Stats() (err error) {
|
|||||||
min := -1.0
|
min := -1.0
|
||||||
max := -1.0
|
max := -1.0
|
||||||
|
|
||||||
if k, ok := dist[f1.SubjectUID]; ok {
|
if k, ok := dist[f1.SubjUID]; ok {
|
||||||
min = k[0]
|
min = k[0]
|
||||||
max = k[1]
|
max = k[1]
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ func (w *Faces) Stats() (err error) {
|
|||||||
|
|
||||||
f2 := faces[j]
|
f2 := faces[j]
|
||||||
|
|
||||||
if f1.SubjectUID != f2.SubjectUID {
|
if f1.SubjUID != f2.SubjUID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ func (w *Faces) Stats() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if max > 0 {
|
if max > 0 {
|
||||||
dist[f1.SubjectUID] = []float64{min, max}
|
dist[f1.SubjUID] = []float64{min, max}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ func Faces(knownOnly, unmatched bool) (result entity.Faces, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if knownOnly {
|
if knownOnly {
|
||||||
stmt = stmt.Where("subject_uid <> ''")
|
stmt = stmt.Where("subj_uid <> ''")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = stmt.Order("subject_uid, samples DESC").Find(&result).Error
|
err = stmt.Order("subj_uid, samples DESC").Find(&result).Error
|
||||||
|
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ func Faces(knownOnly, unmatched bool) (result entity.Faces, err error) {
|
|||||||
func ManuallyAddedFaces() (result entity.Faces, err error) {
|
func ManuallyAddedFaces() (result entity.Faces, err error) {
|
||||||
err = Db().
|
err = Db().
|
||||||
Where("face_src = ?", entity.SrcManual).
|
Where("face_src = ?", entity.SrcManual).
|
||||||
Where("subject_uid <> ''").Order("subject_uid, samples DESC").
|
Where("subj_uid <> ''").Order("subj_uid, samples DESC").
|
||||||
Find(&result).Error
|
Find(&result).Error
|
||||||
|
|
||||||
return result, err
|
return result, err
|
||||||
@@ -48,9 +48,9 @@ func MatchFaceMarkers() (affected int64, err error) {
|
|||||||
for _, f := range faces {
|
for _, f := range faces {
|
||||||
if res := Db().Model(&entity.Marker{}).
|
if res := Db().Model(&entity.Marker{}).
|
||||||
Where("face_id = ?", f.ID).
|
Where("face_id = ?", f.ID).
|
||||||
Where("subject_src = ?", entity.SrcAuto).
|
Where("subj_src = ?", entity.SrcAuto).
|
||||||
Where("subject_uid <> ?", f.SubjectUID).
|
Where("subj_uid <> ?", f.SubjUID).
|
||||||
Updates(entity.Values{"SubjectUID": f.SubjectUID, "Review": false}); res.Error != nil {
|
Updates(entity.Values{"SubjUID": f.SubjUID, "MarkerReview": false}); res.Error != nil {
|
||||||
return affected, err
|
return affected, err
|
||||||
} else if res.RowsAffected > 0 {
|
} else if res.RowsAffected > 0 {
|
||||||
affected += res.RowsAffected
|
affected += res.RowsAffected
|
||||||
@@ -64,7 +64,7 @@ func MatchFaceMarkers() (affected int64, err error) {
|
|||||||
func RemoveAnonymousFaceClusters() (removed int64, err error) {
|
func RemoveAnonymousFaceClusters() (removed int64, err error) {
|
||||||
res := UnscopedDb().Delete(
|
res := UnscopedDb().Delete(
|
||||||
entity.Face{},
|
entity.Face{},
|
||||||
"face_src = ? AND subject_uid = ''", entity.SrcAuto)
|
"face_src = ? AND subj_uid = ''", entity.SrcAuto)
|
||||||
|
|
||||||
return res.RowsAffected, res.Error
|
return res.RowsAffected, res.Error
|
||||||
}
|
}
|
||||||
@@ -131,20 +131,20 @@ func MergeFaces(merge entity.Faces) (merged *entity.Face, err error) {
|
|||||||
return merged, fmt.Errorf("faces: two or more clusters required for merging")
|
return merged, fmt.Errorf("faces: two or more clusters required for merging")
|
||||||
}
|
}
|
||||||
|
|
||||||
subjectUID := merge[0].SubjectUID
|
subjUID := merge[0].SubjUID
|
||||||
|
|
||||||
for i := 1; i < len(merge); i++ {
|
for i := 1; i < len(merge); i++ {
|
||||||
if merge[i].SubjectUID != subjectUID {
|
if merge[i].SubjUID != subjUID {
|
||||||
return merged, fmt.Errorf("faces: can't merge clusters with conflicting subjects %s <> %s",
|
return merged, fmt.Errorf("faces: can't merge clusters with conflicting subjects %s <> %s",
|
||||||
txt.Quote(subjectUID), txt.Quote(merge[i].SubjectUID))
|
txt.Quote(subjUID), txt.Quote(merge[i].SubjUID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find or create merged face cluster.
|
// Find or create merged face cluster.
|
||||||
if merged = entity.NewFace(merge[0].SubjectUID, merge[0].FaceSrc, merge.Embeddings()); merged == nil {
|
if merged = entity.NewFace(merge[0].SubjUID, merge[0].FaceSrc, merge.Embeddings()); merged == nil {
|
||||||
return merged, fmt.Errorf("faces: new cluster is nil for subject %s", txt.Quote(subjectUID))
|
return merged, fmt.Errorf("faces: new cluster is nil for subject %s", txt.Quote(subjUID))
|
||||||
} else if merged = entity.FirstOrCreateFace(merged); merged == nil {
|
} else if merged = entity.FirstOrCreateFace(merged); merged == nil {
|
||||||
return merged, fmt.Errorf("faces: failed creating new cluster for subject %s", txt.Quote(subjectUID))
|
return merged, fmt.Errorf("faces: failed creating new cluster for subject %s", txt.Quote(subjUID))
|
||||||
} else if err := merged.MatchMarkers(append(merge.IDs(), "")); err != nil {
|
} else if err := merged.MatchMarkers(append(merge.IDs(), "")); err != nil {
|
||||||
return merged, err
|
return merged, err
|
||||||
}
|
}
|
||||||
@@ -153,9 +153,9 @@ func MergeFaces(merge entity.Faces) (merged *entity.Face, err error) {
|
|||||||
if removed, err := PurgeOrphanFaces(merge.IDs()); err != nil {
|
if removed, err := PurgeOrphanFaces(merge.IDs()); err != nil {
|
||||||
return merged, err
|
return merged, err
|
||||||
} else if removed > 0 {
|
} else if removed > 0 {
|
||||||
log.Debugf("faces: removed %d orphans for subject %s", removed, txt.Quote(subjectUID))
|
log.Debugf("faces: removed %d orphans for subject %s", removed, txt.Quote(subjUID))
|
||||||
} else {
|
} else {
|
||||||
log.Warnf("faces: failed removing merged clusters for subject %s", txt.Quote(subjectUID))
|
log.Warnf("faces: failed removing merged clusters for subject %s", txt.Quote(subjUID))
|
||||||
}
|
}
|
||||||
|
|
||||||
return merged, err
|
return merged, err
|
||||||
@@ -172,7 +172,7 @@ func ResolveFaceCollisions() (conflicts, resolved int, err error) {
|
|||||||
for _, f1 := range faces {
|
for _, f1 := range faces {
|
||||||
for _, f2 := range faces {
|
for _, f2 := range faces {
|
||||||
if matched, dist := f1.Match(entity.Embeddings{f2.Embedding()}); matched {
|
if matched, dist := f1.Match(entity.Embeddings{f2.Embedding()}); matched {
|
||||||
if f1.SubjectUID == f2.SubjectUID {
|
if f1.SubjUID == f2.SubjUID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,14 +182,14 @@ func ResolveFaceCollisions() (conflicts, resolved int, err error) {
|
|||||||
|
|
||||||
log.Infof("face %s: conflict at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius)
|
log.Infof("face %s: conflict at dist %f, Ø %f from %d samples, collision Ø %f", f1.ID, dist, r, f1.Samples, f1.CollisionRadius)
|
||||||
|
|
||||||
if f1.SubjectUID != "" {
|
if f1.SubjUID != "" {
|
||||||
log.Debugf("face %s: subject %s (%s %s)", f1.ID, txt.Quote(f1.SubjectUID), f1.SubjectUID, entity.SrcString(f1.FaceSrc))
|
log.Debugf("face %s: subject %s (%s %s)", f1.ID, txt.Quote(f1.SubjUID), f1.SubjUID, entity.SrcString(f1.FaceSrc))
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("face %s: no subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc))
|
log.Debugf("face %s: no subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc))
|
||||||
}
|
}
|
||||||
|
|
||||||
if f2.SubjectUID != "" {
|
if f2.SubjUID != "" {
|
||||||
log.Debugf("face %s: subject %s (%s %s)", f2.ID, txt.Quote(f2.SubjectUID), f2.SubjectUID, entity.SrcString(f2.FaceSrc))
|
log.Debugf("face %s: subject %s (%s %s)", f2.ID, txt.Quote(f2.SubjUID), f2.SubjUID, entity.SrcString(f2.FaceSrc))
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("face %s: no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc))
|
log.Debugf("face %s: no subject (%s)", f2.ID, entity.SrcString(f2.FaceSrc))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ func TestMatchFaceMarkers(t *testing.T) {
|
|||||||
} else if m == nil {
|
} else if m == nil {
|
||||||
t.Fatal("marker is nil")
|
t.Fatal("marker is nil")
|
||||||
} else {
|
} else {
|
||||||
assert.Empty(t, m.SubjectUID)
|
assert.Empty(t, m.SubjUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset subject_uid.
|
// Reset subj_uid.
|
||||||
if err := Db().Model(&entity.Marker{}).
|
if err := Db().Model(&entity.Marker{}).
|
||||||
Where("subject_src = ?", entity.SrcAuto).
|
Where("subj_src = ?", entity.SrcAuto).
|
||||||
Where("subject_uid = ?", "jqu0xs11qekk9jx8").
|
Where("subj_uid = ?", "jqu0xs11qekk9jx8").
|
||||||
Updates(entity.Values{"SubjectUID": ""}).Error; err != nil {
|
Updates(entity.Values{"SubjUID": ""}).Error; err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ func TestMatchFaceMarkers(t *testing.T) {
|
|||||||
} else if m == nil {
|
} else if m == nil {
|
||||||
t.Fatal("marker is nil")
|
t.Fatal("marker is nil")
|
||||||
} else {
|
} else {
|
||||||
assert.Equal(t, "jqu0xs11qekk9jx8", m.SubjectUID)
|
assert.Equal(t, "jqu0xs11qekk9jx8", m.SubjUID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ func TestMergeFaces(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, "5LH5E35ZGUMF5AYLM42BIZH4DGQHJDAV", result.ID)
|
assert.Equal(t, "5LH5E35ZGUMF5AYLM42BIZH4DGQHJDAV", result.ID)
|
||||||
assert.Equal(t, entity.SrcManual, result.FaceSrc)
|
assert.Equal(t, entity.SrcManual, result.FaceSrc)
|
||||||
assert.Equal(t, "jqynvsf28rhn6b0c", result.SubjectUID)
|
assert.Equal(t, "jqynvsf28rhn6b0c", result.SubjUID)
|
||||||
assert.Equal(t, 2, result.Samples)
|
assert.Equal(t, 2, result.Samples)
|
||||||
assert.Equal(t, 0.03948165743305488, result.SampleRadius)
|
assert.Equal(t, 0.03948165743305488, result.SampleRadius)
|
||||||
assert.Equal(t, 0, result.Collisions)
|
assert.Equal(t, 0, result.Collisions)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||||||
|
|
||||||
// Modify query if it contains subject names.
|
// Modify query if it contains subject names.
|
||||||
if f.Query != "" && f.Subject == "" {
|
if f.Query != "" && f.Subject == "" {
|
||||||
if subj, names, remaining := SearchSubjectUIDs(f.Query); len(subj) > 0 {
|
if subj, names, remaining := SearchSubjUIDs(f.Query); len(subj) > 0 {
|
||||||
f.Subject = strings.Join(subj, And)
|
f.Subject = strings.Join(subj, And)
|
||||||
log.Debugf("search: subject %s", txt.Quote(strings.Join(names, ", ")))
|
log.Debugf("search: subject %s", txt.Quote(strings.Join(names, ", ")))
|
||||||
f.Query = remaining
|
f.Query = remaining
|
||||||
@@ -115,12 +115,12 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||||||
// Filter for one or more subjects?
|
// Filter for one or more subjects?
|
||||||
if f.Subject != "" {
|
if f.Subject != "" {
|
||||||
for _, subj := range strings.Split(strings.ToLower(f.Subject), And) {
|
for _, subj := range strings.Split(strings.ToLower(f.Subject), And) {
|
||||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subject_uid IN (?))",
|
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))",
|
||||||
entity.Marker{}.TableName()), strings.Split(subj, Or))
|
entity.Marker{}.TableName()), strings.Split(subj, Or))
|
||||||
}
|
}
|
||||||
} else if f.Subjects != "" {
|
} else if f.Subjects != "" {
|
||||||
for _, where := range LikeAnyWord("s.subject_name", f.Subjects) {
|
for _, where := range LikeAnyWord("s.subj_name", f.Subjects) {
|
||||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subject_uid = m.subject_uid WHERE (?))",
|
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))",
|
||||||
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
|
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func Markers(limit, offset int, markerType string, embeddings, subjects bool, ma
|
|||||||
}
|
}
|
||||||
|
|
||||||
if subjects {
|
if subjects {
|
||||||
db = db.Where("subject_uid <> ''")
|
db = db.Where("subj_uid <> ''")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matchedBefore.IsZero() {
|
if !matchedBefore.IsZero() {
|
||||||
@@ -119,8 +119,8 @@ func Embeddings(single, unclustered bool, size, score int) (result entity.Embedd
|
|||||||
func RemoveInvalidMarkerReferences() (removed int64, err error) {
|
func RemoveInvalidMarkerReferences() (removed int64, err error) {
|
||||||
res := Db().
|
res := Db().
|
||||||
Model(&entity.Marker{}).
|
Model(&entity.Marker{}).
|
||||||
Where("marker_invalid = 1 AND (subject_uid <> '' OR face_id <> '')").
|
Where("marker_invalid = 1 AND (subj_uid <> '' OR face_id <> '')").
|
||||||
UpdateColumns(entity.Values{"subject_uid": "", "face_id": "", "face_dist": -1.0, "matched_at": nil})
|
UpdateColumns(entity.Values{"subj_uid": "", "face_id": "", "face_dist": -1.0, "matched_at": nil})
|
||||||
|
|
||||||
return res.RowsAffected, res.Error
|
return res.RowsAffected, res.Error
|
||||||
}
|
}
|
||||||
@@ -141,8 +141,8 @@ func RemoveNonExistentMarkerFaces() (removed int64, err error) {
|
|||||||
func RemoveNonExistentMarkerSubjects() (removed int64, err error) {
|
func RemoveNonExistentMarkerSubjects() (removed int64, err error) {
|
||||||
res := Db().
|
res := Db().
|
||||||
Model(&entity.Marker{}).
|
Model(&entity.Marker{}).
|
||||||
Where(fmt.Sprintf("subject_uid <> '' AND subject_uid NOT IN (SELECT subject_uid FROM %s)", entity.Subject{}.TableName())).
|
Where(fmt.Sprintf("subj_uid <> '' AND subj_uid NOT IN (SELECT subj_uid FROM %s)", entity.Subject{}.TableName())).
|
||||||
UpdateColumns(entity.Values{"subject_uid": "", "matched_at": nil})
|
UpdateColumns(entity.Values{"subj_uid": "", "matched_at": nil})
|
||||||
|
|
||||||
return res.RowsAffected, res.Error
|
return res.RowsAffected, res.Error
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ func MarkersWithNonExistentReferences() (faces entity.Markers, subjects entity.M
|
|||||||
|
|
||||||
// Find markers with invalid subject UIDs.
|
// Find markers with invalid subject UIDs.
|
||||||
if res := Db().
|
if res := Db().
|
||||||
Where(fmt.Sprintf("subject_uid <> '' AND subject_uid NOT IN (SELECT subject_uid FROM %s)", entity.Subject{}.TableName())).
|
Where(fmt.Sprintf("subj_uid <> '' AND subj_uid NOT IN (SELECT subj_uid FROM %s)", entity.Subject{}.TableName())).
|
||||||
Find(&subjects); res.Error != nil {
|
Find(&subjects); res.Error != nil {
|
||||||
err = res.Error
|
err = res.Error
|
||||||
}
|
}
|
||||||
@@ -193,7 +193,7 @@ func MarkersWithNonExistentReferences() (faces entity.Markers, subjects entity.M
|
|||||||
// MarkersWithSubjectConflict finds markers with conflicting subjects.
|
// MarkersWithSubjectConflict finds markers with conflicting subjects.
|
||||||
func MarkersWithSubjectConflict() (results entity.Markers, err error) {
|
func MarkersWithSubjectConflict() (results entity.Markers, err error) {
|
||||||
err = Db().
|
err = Db().
|
||||||
Joins(fmt.Sprintf("JOIN %s f ON f.id = face_id AND f.subject_uid <> %s.subject_uid", entity.Face{}.TableName(), entity.Marker{}.TableName())).
|
Joins(fmt.Sprintf("JOIN %s f ON f.id = face_id AND f.subj_uid <> %s.subj_uid", entity.Face{}.TableName(), entity.Marker{}.TableName())).
|
||||||
Order("face_id").
|
Order("face_id").
|
||||||
Find(&results).Error
|
Find(&results).Error
|
||||||
|
|
||||||
@@ -203,8 +203,8 @@ func MarkersWithSubjectConflict() (results entity.Markers, err error) {
|
|||||||
// ResetFaceMarkerMatches removes automatically added subject and face references from the markers table.
|
// ResetFaceMarkerMatches removes automatically added subject and face references from the markers table.
|
||||||
func ResetFaceMarkerMatches() (removed int64, err error) {
|
func ResetFaceMarkerMatches() (removed int64, err error) {
|
||||||
res := Db().Model(&entity.Marker{}).
|
res := Db().Model(&entity.Marker{}).
|
||||||
Where("subject_src = ? AND marker_type = ?", entity.SrcAuto, entity.MarkerFace).
|
Where("subj_src = ? AND marker_type = ?", entity.SrcAuto, entity.MarkerFace).
|
||||||
UpdateColumns(entity.Values{"marker_name": "", "subject_uid": "", "subject_src": "", "face_id": "", "face_dist": -1.0, "matched_at": nil})
|
UpdateColumns(entity.Values{"marker_name": "", "subj_uid": "", "subj_src": "", "face_id": "", "face_dist": -1.0, "matched_at": nil})
|
||||||
|
|
||||||
return res.RowsAffected, res.Error
|
return res.RowsAffected, res.Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||||||
|
|
||||||
// Modify query if it contains subject names.
|
// Modify query if it contains subject names.
|
||||||
if f.Query != "" && f.Subject == "" {
|
if f.Query != "" && f.Subject == "" {
|
||||||
if subj, names, remaining := SearchSubjectUIDs(f.Query); len(subj) > 0 {
|
if subj, names, remaining := SearchSubjUIDs(f.Query); len(subj) > 0 {
|
||||||
f.Subject = strings.Join(subj, And)
|
f.Subject = strings.Join(subj, And)
|
||||||
log.Debugf("people: searching for %s", txt.Quote(txt.JoinNames(names)))
|
log.Debugf("people: searching for %s", txt.Quote(txt.JoinNames(names)))
|
||||||
f.Query = remaining
|
f.Query = remaining
|
||||||
@@ -222,12 +222,12 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||||||
// Filter for one or more subjects?
|
// Filter for one or more subjects?
|
||||||
if f.Subject != "" {
|
if f.Subject != "" {
|
||||||
for _, subj := range strings.Split(strings.ToLower(f.Subject), And) {
|
for _, subj := range strings.Split(strings.ToLower(f.Subject), And) {
|
||||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subject_uid IN (?))",
|
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))",
|
||||||
entity.Marker{}.TableName()), strings.Split(subj, Or))
|
entity.Marker{}.TableName()), strings.Split(subj, Or))
|
||||||
}
|
}
|
||||||
} else if f.Subjects != "" {
|
} else if f.Subjects != "" {
|
||||||
for _, where := range LikeAnyWord("s.subject_name", f.Subjects) {
|
for _, where := range LikeAnyWord("s.subj_name", f.Subjects) {
|
||||||
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subject_uid = m.subject_uid WHERE (?))",
|
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 JOIN %s s ON s.subj_uid = m.subj_uid WHERE (?))",
|
||||||
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
|
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func UpdateSubjectPreviews() (err error) {
|
|||||||
return Db().Table(entity.Subject{}.TableName()).
|
return Db().Table(entity.Subject{}.TableName()).
|
||||||
UpdateColumn("thumb", gorm.Expr("(SELECT f.file_hash FROM files f "+
|
UpdateColumn("thumb", gorm.Expr("(SELECT f.file_hash FROM files f "+
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"JOIN %s m ON f.file_uid = m.file_uid AND m.subject_uid = %s.subject_uid",
|
"JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid",
|
||||||
entity.Marker{}.TableName(),
|
entity.Marker{}.TableName(),
|
||||||
entity.Subject{}.TableName())+
|
entity.Subject{}.TableName())+
|
||||||
` JOIN photos p ON f.photo_id = p.id
|
` JOIN photos p ON f.photo_id = p.id
|
||||||
@@ -172,7 +172,7 @@ func UpdateSubjectPreviews() (err error) {
|
|||||||
err = Db().Table(entity.Subject{}.TableName()).
|
err = Db().Table(entity.Subject{}.TableName()).
|
||||||
UpdateColumn("thumb", gorm.Expr("(SELECT m.file_hash FROM "+
|
UpdateColumn("thumb", gorm.Expr("(SELECT m.file_hash FROM "+
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"%s m WHERE m.subject_uid = %s.subject_uid AND m.subject_src = 'manual' ",
|
"%s m WHERE m.subj_uid = %s.subj_uid AND m.subj_src = 'manual' ",
|
||||||
entity.Marker{}.TableName(),
|
entity.Marker{}.TableName(),
|
||||||
entity.Subject{}.TableName())+
|
entity.Subject{}.TableName())+
|
||||||
` AND m.file_hash <> '' ORDER BY m.size DESC LIMIT 1)
|
` AND m.file_hash <> '' ORDER BY m.size DESC LIMIT 1)
|
||||||
|
|||||||
@@ -32,13 +32,14 @@ func PhotoSelection(f form.Selection) (results entity.Photos, err error) {
|
|||||||
SELECT a.path FROM folders a WHERE a.folder_uid IN (?) UNION
|
SELECT a.path FROM folders a WHERE a.folder_uid IN (?) UNION
|
||||||
SELECT b.path FROM folders a JOIN folders b ON b.path LIKE %s WHERE a.folder_uid IN (?))
|
SELECT b.path FROM folders a JOIN folders b ON b.path LIKE %s WHERE a.folder_uid IN (?))
|
||||||
OR photos.photo_uid IN (SELECT photo_uid FROM photos_albums WHERE hidden = 0 AND album_uid IN (?))
|
OR photos.photo_uid IN (SELECT photo_uid FROM photos_albums WHERE hidden = 0 AND album_uid IN (?))
|
||||||
|
OR photos.id IN (SELECT f.photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid WHERE f.deleted_at IS NULL AND m.subj_uid IN (?))
|
||||||
OR photos.id IN (SELECT pl.photo_id FROM photos_labels pl JOIN labels l ON pl.label_id = l.id AND l.deleted_at IS NULL WHERE l.label_uid IN (?))
|
OR photos.id IN (SELECT pl.photo_id FROM photos_labels pl JOIN labels l ON pl.label_id = l.id AND l.deleted_at IS NULL WHERE l.label_uid IN (?))
|
||||||
OR photos.id IN (SELECT pl.photo_id FROM photos_labels pl JOIN categories c ON c.label_id = pl.label_id JOIN labels lc ON lc.id = c.category_id AND lc.deleted_at IS NULL WHERE lc.label_uid IN (?))`,
|
OR photos.id IN (SELECT pl.photo_id FROM photos_labels pl JOIN categories c ON c.label_id = pl.label_id JOIN labels lc ON lc.id = c.category_id AND lc.deleted_at IS NULL WHERE lc.label_uid IN (?))`,
|
||||||
concat)
|
concat, entity.Marker{}.TableName())
|
||||||
|
|
||||||
s := UnscopedDb().Table("photos").
|
s := UnscopedDb().Table("photos").
|
||||||
Select("photos.*").
|
Select("photos.*").
|
||||||
Where(where, f.Photos, f.Places, f.Files, f.Files, f.Files, f.Albums, f.Labels, f.Labels)
|
Where(where, f.Photos, f.Places, f.Files, f.Files, f.Files, f.Albums, f.Subjects, f.Labels, f.Labels)
|
||||||
|
|
||||||
if result := s.Scan(&results); result.Error != nil {
|
if result := s.Scan(&results); result.Error != nil {
|
||||||
return results, result.Error
|
return results, result.Error
|
||||||
@@ -71,16 +72,17 @@ func FileSelection(f form.Selection) (results entity.Files, err error) {
|
|||||||
SELECT a.path FROM folders a WHERE a.folder_uid IN (?) UNION
|
SELECT a.path FROM folders a WHERE a.folder_uid IN (?) UNION
|
||||||
SELECT b.path FROM folders a JOIN folders b ON b.path LIKE %s WHERE a.folder_uid IN (?))
|
SELECT b.path FROM folders a JOIN folders b ON b.path LIKE %s WHERE a.folder_uid IN (?))
|
||||||
OR photos.photo_uid IN (SELECT photo_uid FROM photos_albums WHERE hidden = 0 AND album_uid IN (?))
|
OR photos.photo_uid IN (SELECT photo_uid FROM photos_albums WHERE hidden = 0 AND album_uid IN (?))
|
||||||
|
OR files.file_uid IN (SELECT file_uid FROM %s m WHERE m.subj_uid IN (?))
|
||||||
OR photos.id IN (SELECT pl.photo_id FROM photos_labels pl JOIN labels l ON pl.label_id = l.id AND l.deleted_at IS NULL WHERE l.label_uid IN (?))
|
OR photos.id IN (SELECT pl.photo_id FROM photos_labels pl JOIN labels l ON pl.label_id = l.id AND l.deleted_at IS NULL WHERE l.label_uid IN (?))
|
||||||
OR photos.id IN (SELECT pl.photo_id FROM photos_labels pl JOIN categories c ON c.label_id = pl.label_id JOIN labels lc ON lc.id = c.category_id AND lc.deleted_at IS NULL WHERE lc.label_uid IN (?))`,
|
OR photos.id IN (SELECT pl.photo_id FROM photos_labels pl JOIN categories c ON c.label_id = pl.label_id JOIN labels lc ON lc.id = c.category_id AND lc.deleted_at IS NULL WHERE lc.label_uid IN (?))`,
|
||||||
concat)
|
concat, entity.Marker{}.TableName())
|
||||||
|
|
||||||
s := UnscopedDb().Table("files").
|
s := UnscopedDb().Table("files").
|
||||||
Select("files.*").
|
Select("files.*").
|
||||||
Joins("JOIN photos ON photos.id = files.photo_id").
|
Joins("JOIN photos ON photos.id = files.photo_id").
|
||||||
Where("photos.deleted_at IS NULL").
|
Where("photos.deleted_at IS NULL").
|
||||||
Where("files.file_missing = 0").
|
Where("files.file_missing = 0").
|
||||||
Where(where, f.Photos, f.Places, f.Files, f.Files, f.Files, f.Albums, f.Labels, f.Labels).
|
Where(where, f.Photos, f.Places, f.Files, f.Files, f.Files, f.Albums, f.Subjects, f.Labels, f.Labels).
|
||||||
Group("files.id")
|
Group("files.id")
|
||||||
|
|
||||||
if result := s.Scan(&results); result.Error != nil {
|
if result := s.Scan(&results); result.Error != nil {
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ import (
|
|||||||
|
|
||||||
// SubjectResult represents a subject search result.
|
// SubjectResult represents a subject search result.
|
||||||
type SubjectResult struct {
|
type SubjectResult struct {
|
||||||
SubjectUID string `json:"UID"`
|
SubjUID string `json:"UID"`
|
||||||
SubjectType string `json:"Type"`
|
SubjType string `json:"Type"`
|
||||||
SubjectSlug string `json:"Slug"`
|
SubjSlug string `json:"Slug"`
|
||||||
SubjectName string `json:"Name"`
|
SubjName string `json:"Name"`
|
||||||
SubjectAlias string `json:"Alias,omitempty"`
|
SubjAlias string `json:"Alias"`
|
||||||
Thumb string `json:"Thumb,omitempty"`
|
SubjFavorite bool `json:"Favorite"`
|
||||||
Favorite bool `json:"Favorite,omitempty"`
|
SubjPrivate bool `json:"Private"`
|
||||||
Private bool `json:"Private,omitempty"`
|
SubjExcluded bool `json:"Excluded"`
|
||||||
Excluded bool `json:"Excluded,omitempty"`
|
FileCount int `json:"Files"`
|
||||||
FileCount int `json:"Files,omitempty"`
|
Thumb string `json:"Thumb"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubjectResults represents subject search results.
|
// SubjectResults represents subject search results.
|
||||||
@@ -38,7 +38,7 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
|
|||||||
|
|
||||||
// Base query.
|
// Base query.
|
||||||
s := UnscopedDb().Table(entity.Subject{}.TableName()).
|
s := UnscopedDb().Table(entity.Subject{}.TableName()).
|
||||||
Select("subject_uid, subject_slug, subject_name, subject_alias, subject_type, thumb, favorite, private, excluded, file_count")
|
Select("subj_uid, subj_slug, subj_name, subj_alias, subj_type, thumb, subj_favorite, subj_private, subj_excluded, file_count")
|
||||||
|
|
||||||
// Limit result count.
|
// Limit result count.
|
||||||
if f.Count > 0 && f.Count <= MaxResults {
|
if f.Count > 0 && f.Count <= MaxResults {
|
||||||
@@ -49,14 +49,20 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
|
|||||||
|
|
||||||
// Set sort order.
|
// Set sort order.
|
||||||
switch f.Order {
|
switch f.Order {
|
||||||
|
case "name":
|
||||||
|
s = s.Order("subj_name")
|
||||||
case "count":
|
case "count":
|
||||||
s = s.Order("file_count DESC")
|
s = s.Order("file_count DESC")
|
||||||
|
case "added":
|
||||||
|
s = s.Order("created_at DESC")
|
||||||
|
case "relevance":
|
||||||
|
s = s.Order("subj_favorite DESC, subj_name")
|
||||||
default:
|
default:
|
||||||
s = s.Order("subject_name")
|
s = s.Order("subj_favorite DESC, subj_name")
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.ID != "" {
|
if f.ID != "" {
|
||||||
s = s.Where("subject_uid IN (?)", strings.Split(f.ID, Or))
|
s = s.Where("subj_uid IN (?)", strings.Split(f.ID, Or))
|
||||||
|
|
||||||
if result := s.Scan(&results); result.Error != nil {
|
if result := s.Scan(&results); result.Error != nil {
|
||||||
return results, result.Error
|
return results, result.Error
|
||||||
@@ -66,27 +72,28 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if f.Query != "" {
|
if f.Query != "" {
|
||||||
for _, where := range LikeAnyWord("subject_name", f.Query) {
|
for _, where := range LikeAnyWord("subj_name", f.Query) {
|
||||||
s = s.Where("(?)", gorm.Expr(where))
|
s = s.Where("(?)", gorm.Expr(where))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Type != "" {
|
if f.Type != "" {
|
||||||
s = s.Where("subject_type IN (?)", strings.Split(f.Type, Or))
|
s = s.Where("subj_type IN (?)", strings.Split(f.Type, Or))
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Favorite {
|
if f.Favorite {
|
||||||
s = s.Where("favorite = 1")
|
s = s.Where("subj_favorite = 1")
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Private {
|
if f.Private {
|
||||||
s = s.Where("private = 1")
|
s = s.Where("subj_private = 1")
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Excluded {
|
if f.Excluded {
|
||||||
s = s.Where("excluded = 1")
|
s = s.Where("subj_excluded = 1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Omit deleted rows.
|
||||||
s = s.Where("deleted_at IS NULL")
|
s = s.Where("deleted_at IS NULL")
|
||||||
|
|
||||||
if result := s.Scan(&results); result.Error != nil {
|
if result := s.Scan(&results); result.Error != nil {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
func TestSubjectSearch(t *testing.T) {
|
func TestSubjectSearch(t *testing.T) {
|
||||||
t.Run("FindAll", func(t *testing.T) {
|
t.Run("FindAll", func(t *testing.T) {
|
||||||
results, err := SubjectSearch(form.SubjectSearch{Type: entity.SubjectPerson})
|
results, err := SubjectSearch(form.SubjectSearch{Type: entity.SubjPerson})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.LessOrEqual(t, 3, len(results))
|
assert.LessOrEqual(t, 3, len(results))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import (
|
|||||||
func People() (people entity.People, err error) {
|
func People() (people entity.People, err error) {
|
||||||
err = UnscopedDb().
|
err = UnscopedDb().
|
||||||
Table(entity.Subject{}.TableName()).
|
Table(entity.Subject{}.TableName()).
|
||||||
Select("subject_uid, subject_name, subject_alias, favorite ").
|
Select("subj_uid, subj_name, subj_alias, subj_favorite ").
|
||||||
Where("deleted_at IS NULL AND subject_type = ?", entity.SubjectPerson).
|
Where("deleted_at IS NULL AND subj_type = ?", entity.SubjPerson).
|
||||||
Order("favorite, subject_name").
|
Order("subj_favorite, subj_name").
|
||||||
Limit(2000).Offset(0).
|
Limit(2000).Offset(0).
|
||||||
Scan(&people).Error
|
Scan(&people).Error
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ func PeopleCount() (count int, err error) {
|
|||||||
err = Db().
|
err = Db().
|
||||||
Table(entity.Subject{}.TableName()).
|
Table(entity.Subject{}.TableName()).
|
||||||
Where("deleted_at IS NULL").
|
Where("deleted_at IS NULL").
|
||||||
Where("subject_type = ?", entity.SubjectPerson).
|
Where("subj_type = ?", entity.SubjPerson).
|
||||||
Count(&count).Error
|
Count(&count).Error
|
||||||
|
|
||||||
return count, err
|
return count, err
|
||||||
@@ -38,7 +38,7 @@ func PeopleCount() (count int, err error) {
|
|||||||
func Subjects(limit, offset int) (result entity.Subjects, err error) {
|
func Subjects(limit, offset int) (result entity.Subjects, err error) {
|
||||||
stmt := Db()
|
stmt := Db()
|
||||||
|
|
||||||
stmt = stmt.Order("subject_name").Limit(limit).Offset(offset)
|
stmt = stmt.Order("subj_name").Limit(limit).Offset(offset)
|
||||||
err = stmt.Find(&result).Error
|
err = stmt.Find(&result).Error
|
||||||
|
|
||||||
return result, err
|
return result, err
|
||||||
@@ -57,7 +57,7 @@ func SubjectMap() (result map[string]entity.Subject, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range subj {
|
for _, s := range subj {
|
||||||
result[s.SubjectUID] = s
|
result[s.SubjUID] = s
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, err
|
return result, err
|
||||||
@@ -66,9 +66,9 @@ func SubjectMap() (result map[string]entity.Subject, err error) {
|
|||||||
// RemoveDanglingMarkerSubjects permanently deletes dangling marker subjects from the index.
|
// RemoveDanglingMarkerSubjects permanently deletes dangling marker subjects from the index.
|
||||||
func RemoveDanglingMarkerSubjects() (removed int64, err error) {
|
func RemoveDanglingMarkerSubjects() (removed int64, err error) {
|
||||||
res := UnscopedDb().
|
res := UnscopedDb().
|
||||||
Where("subject_src = ?", entity.SrcMarker).
|
Where("subj_src = ?", entity.SrcMarker).
|
||||||
Where(fmt.Sprintf("subject_uid NOT IN (SELECT subject_uid FROM %s)", entity.Face{}.TableName())).
|
Where(fmt.Sprintf("subj_uid NOT IN (SELECT subj_uid FROM %s)", entity.Face{}.TableName())).
|
||||||
Where(fmt.Sprintf("subject_uid NOT IN (SELECT subject_uid FROM %s)", entity.Marker{}.TableName())).
|
Where(fmt.Sprintf("subj_uid NOT IN (SELECT subj_uid FROM %s)", entity.Marker{}.TableName())).
|
||||||
Delete(&entity.Subject{})
|
Delete(&entity.Subject{})
|
||||||
|
|
||||||
return res.RowsAffected, res.Error
|
return res.RowsAffected, res.Error
|
||||||
@@ -79,7 +79,7 @@ func CreateMarkerSubjects() (affected int64, err error) {
|
|||||||
var markers entity.Markers
|
var markers entity.Markers
|
||||||
|
|
||||||
if err := Db().
|
if err := Db().
|
||||||
Where("subject_uid = '' AND marker_name <> '' AND subject_src <> ?", entity.SrcAuto).
|
Where("subj_uid = '' AND marker_name <> '' AND subj_src <> ?", entity.SrcAuto).
|
||||||
Where("marker_invalid = 0 AND marker_type = ?", entity.MarkerFace).
|
Where("marker_invalid = 0 AND marker_type = ?", entity.MarkerFace).
|
||||||
Order("marker_name").
|
Order("marker_name").
|
||||||
Find(&markers).Error; err != nil {
|
Find(&markers).Error; err != nil {
|
||||||
@@ -94,7 +94,7 @@ func CreateMarkerSubjects() (affected int64, err error) {
|
|||||||
for _, m := range markers {
|
for _, m := range markers {
|
||||||
if name == m.MarkerName && subj != nil {
|
if name == m.MarkerName && subj != nil {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
} else if subj = entity.NewSubject(m.MarkerName, entity.SubjectPerson, entity.SrcMarker); subj == nil {
|
} else if subj = entity.NewSubject(m.MarkerName, entity.SubjPerson, entity.SrcMarker); subj == nil {
|
||||||
log.Errorf("faces: subject should not be nil - bug?")
|
log.Errorf("faces: subject should not be nil - bug?")
|
||||||
continue
|
continue
|
||||||
} else if subj = entity.FirstOrCreateSubject(subj); subj == nil {
|
} else if subj = entity.FirstOrCreateSubject(subj); subj == nil {
|
||||||
@@ -106,13 +106,13 @@ func CreateMarkerSubjects() (affected int64, err error) {
|
|||||||
|
|
||||||
name = m.MarkerName
|
name = m.MarkerName
|
||||||
|
|
||||||
if err := m.Updates(entity.Values{"SubjectUID": subj.SubjectUID, "Review": false}); err != nil {
|
if err := m.Updates(entity.Values{"SubjUID": subj.SubjUID, "MarkerReview": false}); err != nil {
|
||||||
return affected, err
|
return affected, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.FaceID == "" {
|
if m.FaceID == "" {
|
||||||
continue
|
continue
|
||||||
} else if err := Db().Model(&entity.Face{}).Where("id = ? AND subject_uid = ''", m.FaceID).Update("SubjectUID", subj.SubjectUID).Error; err != nil {
|
} else if err := Db().Model(&entity.Face{}).Where("id = ? AND subj_uid = ''", m.FaceID).Update("SubjUID", subj.SubjUID).Error; err != nil {
|
||||||
return affected, err
|
return affected, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,21 +120,21 @@ func CreateMarkerSubjects() (affected int64, err error) {
|
|||||||
return affected, err
|
return affected, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchSubjectUIDs finds subject UIDs matching the search string, and removes names from the remaining query.
|
// SearchSubjUIDs finds subject UIDs matching the search string, and removes names from the remaining query.
|
||||||
func SearchSubjectUIDs(s string) (result []string, names []string, remaining string) {
|
func SearchSubjUIDs(s string) (result []string, names []string, remaining string) {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return result, names, s
|
return result, names, s
|
||||||
}
|
}
|
||||||
|
|
||||||
type Matches struct {
|
type Matches struct {
|
||||||
SubjectUID string
|
SubjUID string
|
||||||
SubjectName string
|
SubjName string
|
||||||
SubjectAlias string
|
SubjAlias string
|
||||||
}
|
}
|
||||||
|
|
||||||
var matches []Matches
|
var matches []Matches
|
||||||
|
|
||||||
wheres := LikeAllNames(Cols{"subject_name", "subject_alias"}, s)
|
wheres := LikeAllNames(Cols{"subj_name", "subj_alias"}, s)
|
||||||
|
|
||||||
if len(wheres) == 0 {
|
if len(wheres) == 0 {
|
||||||
return result, names, s
|
return result, names, s
|
||||||
@@ -155,16 +155,16 @@ func SearchSubjectUIDs(s string) (result []string, names []string, remaining str
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range matches {
|
for _, m := range matches {
|
||||||
subj = append(subj, m.SubjectUID)
|
subj = append(subj, m.SubjUID)
|
||||||
names = append(names, m.SubjectName)
|
names = append(names, m.SubjName)
|
||||||
|
|
||||||
for _, r := range txt.Words(strings.ToLower(m.SubjectName)) {
|
for _, r := range txt.Words(strings.ToLower(m.SubjName)) {
|
||||||
if len(r) > 1 {
|
if len(r) > 1 {
|
||||||
remaining = strings.ReplaceAll(remaining, r, "")
|
remaining = strings.ReplaceAll(remaining, r, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range txt.Words(strings.ToLower(m.SubjectAlias)) {
|
for _, r := range txt.Words(strings.ToLower(m.SubjAlias)) {
|
||||||
if len(r) > 1 {
|
if len(r) > 1 {
|
||||||
remaining = strings.ReplaceAll(remaining, r, "")
|
remaining = strings.ReplaceAll(remaining, r, "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,9 +72,9 @@ func TestCreateMarkerSubjects(t *testing.T) {
|
|||||||
assert.LessOrEqual(t, int64(0), affected)
|
assert.LessOrEqual(t, int64(0), affected)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSearchSubjectUIDs(t *testing.T) {
|
func TestSearchSubjUIDs(t *testing.T) {
|
||||||
t.Run("john & his | cats", func(t *testing.T) {
|
t.Run("john & his | cats", func(t *testing.T) {
|
||||||
result, names, remaining := SearchSubjectUIDs("john & his | cats")
|
result, names, remaining := SearchSubjUIDs("john & his | cats")
|
||||||
|
|
||||||
if len(result) != 1 {
|
if len(result) != 1 {
|
||||||
t.Fatal("expected one result")
|
t.Fatal("expected one result")
|
||||||
@@ -85,11 +85,11 @@ func TestSearchSubjectUIDs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
t.Run("xxx", func(t *testing.T) {
|
t.Run("xxx", func(t *testing.T) {
|
||||||
result, _, _ := SearchSubjectUIDs("xxx")
|
result, _, _ := SearchSubjUIDs("xxx")
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
})
|
})
|
||||||
t.Run("empty string", func(t *testing.T) {
|
t.Run("empty string", func(t *testing.T) {
|
||||||
result, _, _ := SearchSubjectUIDs("")
|
result, _, _ := SearchSubjUIDs("")
|
||||||
assert.Empty(t, result)
|
assert.Empty(t, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||||||
|
|
||||||
api.GetSubjects(v1)
|
api.GetSubjects(v1)
|
||||||
api.GetSubject(v1)
|
api.GetSubject(v1)
|
||||||
|
api.UpdateSubject(v1)
|
||||||
|
api.LikeSubject(v1)
|
||||||
|
api.DislikeSubject(v1)
|
||||||
|
|
||||||
api.LabelCover(v1)
|
api.LabelCover(v1)
|
||||||
api.GetLabels(v1)
|
api.GetLabels(v1)
|
||||||
|
|||||||
Reference in New Issue
Block a user