People: Add overview page with search and context menu #22
This commit is contained in:
parent
9260c93df0
commit
ece15c6ade
|
@ -146,6 +146,11 @@ export default class Config {
|
|||
this.values.people = [];
|
||||
}
|
||||
|
||||
if (!data || !data.entities) {
|
||||
console.warn("empty event data", ev, data);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "created":
|
||||
this.values.people.unshift(...data.entities);
|
||||
|
|
|
@ -43,6 +43,7 @@ import PAlbumClipboard from "./album/clipboard.vue";
|
|||
import PAlbumToolbar from "./album/toolbar.vue";
|
||||
import PLabelClipboard from "./label/clipboard.vue";
|
||||
import PFileClipboard from "./file/clipboard.vue";
|
||||
import PSubjectClipboard from "./subject/clipboard.vue";
|
||||
import PAboutFooter from "./footer.vue";
|
||||
|
||||
const components = {};
|
||||
|
@ -63,6 +64,7 @@ components.install = (Vue) => {
|
|||
Vue.component("PAlbumToolbar", PAlbumToolbar);
|
||||
Vue.component("PLabelClipboard", PLabelClipboard);
|
||||
Vue.component("PFileClipboard", PFileClipboard);
|
||||
Vue.component("PSubjectClipboard", PSubjectClipboard);
|
||||
Vue.component("PAboutFooter", PAboutFooter);
|
||||
};
|
||||
|
||||
|
|
|
@ -223,6 +223,20 @@
|
|||
</v-list-tile-content>
|
||||
</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"
|
||||
@click.stop="">
|
||||
<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"
|
||||
aspect-ratio="1"
|
||||
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')"
|
||||
@click.stop.prevent="onReject(marker)">
|
||||
<v-icon color="white" class="action-reject">clear</v-icon>
|
||||
|
@ -43,11 +43,11 @@
|
|||
large depressed block :round="false"
|
||||
class="action-approve text-xs-center"
|
||||
:title="$gettext('Approve')" @click.stop="onApprove(marker)">
|
||||
<v-icon dark>check</v-icon>
|
||||
<v-icon dark>undo</v-icon>
|
||||
</v-btn>
|
||||
</v-flex>
|
||||
</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-text-field
|
||||
v-model="marker.Name"
|
||||
|
|
|
@ -53,8 +53,8 @@ export class Marker extends RestModel {
|
|||
H: 0.0,
|
||||
CropID: "",
|
||||
FaceID: "",
|
||||
SubjectSrc: "",
|
||||
SubjectUID: "",
|
||||
SubjSrc: "",
|
||||
SubjUID: "",
|
||||
Score: 0,
|
||||
Size: 0,
|
||||
};
|
||||
|
@ -118,9 +118,9 @@ export class Marker extends RestModel {
|
|||
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) =>
|
||||
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: {
|
||||
viewType() {
|
||||
let queryParam = this.$route.query['view'];
|
||||
let queryParam = this.$route.query["view"];
|
||||
let storedType = window.localStorage.getItem("photo_view");
|
||||
|
||||
if (queryParam) {
|
||||
|
@ -178,7 +178,7 @@ export default {
|
|||
return 'cards';
|
||||
},
|
||||
sortOrder() {
|
||||
let queryParam = this.$route.query['order'];
|
||||
let queryParam = this.$route.query["order"];
|
||||
let storedType = window.localStorage.getItem("photo_order");
|
||||
|
||||
if (queryParam) {
|
||||
|
@ -188,7 +188,7 @@ export default {
|
|||
return storedType;
|
||||
}
|
||||
|
||||
return 'newest';
|
||||
return "newest";
|
||||
},
|
||||
openLocation(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 Errors from "pages/library/errors.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 Settings from "pages/settings.vue";
|
||||
import Login from "pages/login.vue";
|
||||
|
@ -261,7 +261,7 @@ export default [
|
|||
{
|
||||
name: "people",
|
||||
path: "/people",
|
||||
component: People,
|
||||
component: Subjects,
|
||||
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)
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// GetLabels finds and returns labels as JSON.
|
||||
//
|
||||
// GET /api/v1/labels
|
||||
func GetLabels(router *gin.RouterGroup) {
|
||||
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
|
||||
func UpdateLabel(router *gin.RouterGroup) {
|
||||
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
|
||||
//
|
||||
// Parameters:
|
||||
|
@ -123,6 +129,8 @@ func LikeLabel(router *gin.RouterGroup) {
|
|||
})
|
||||
}
|
||||
|
||||
// DislikeLabel removes the favorite flag from a label.
|
||||
//
|
||||
// DELETE /api/v1/labels/:uid/like
|
||||
//
|
||||
// Parameters:
|
||||
|
|
|
@ -93,7 +93,7 @@ func UpdateMarker(router *gin.RouterGroup) {
|
|||
log.Errorf("photo: %s (save marker form)", err)
|
||||
AbortSaveFailed(c)
|
||||
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 {
|
||||
log.Errorf("faces: %s (optimize)", err)
|
||||
} else if res.Merged > 0 {
|
||||
|
|
|
@ -35,7 +35,7 @@ func TestUpdateMarker(t *testing.T) {
|
|||
u := fmt.Sprintf("/api/v1/markers/%s", markerUID)
|
||||
|
||||
var m = form.Marker{
|
||||
SubjectSrc: "manual",
|
||||
SubjSrc: "manual",
|
||||
MarkerInvalid: true,
|
||||
MarkerName: "Foo",
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ func TestUpdateMarker(t *testing.T) {
|
|||
UpdateMarker(router)
|
||||
|
||||
var m = form.Marker{
|
||||
SubjectSrc: "manual",
|
||||
SubjSrc: "manual",
|
||||
MarkerInvalid: false,
|
||||
MarkerName: "Actress A",
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ func TestUpdateMarker(t *testing.T) {
|
|||
UpdateMarker(router)
|
||||
|
||||
var m = form.Marker{
|
||||
SubjectSrc: "manual",
|
||||
SubjSrc: "manual",
|
||||
MarkerInvalid: false,
|
||||
MarkerName: "Actress A",
|
||||
}
|
||||
|
@ -150,20 +150,20 @@ func TestUpdateMarker(t *testing.T) {
|
|||
UpdateMarker(router)
|
||||
|
||||
var m = struct {
|
||||
ID int
|
||||
Type string
|
||||
Src int
|
||||
Name int
|
||||
SubjectUID string
|
||||
SubjectSrc string
|
||||
FaceID string
|
||||
ID int
|
||||
Type string
|
||||
Src int
|
||||
Name int
|
||||
SubjUID string
|
||||
SubjSrc string
|
||||
FaceID string
|
||||
}{ID: 8,
|
||||
Type: "face",
|
||||
Src: 123,
|
||||
Name: 456,
|
||||
SubjectUID: "jqy1y111h1njaaac",
|
||||
SubjectSrc: "manual",
|
||||
FaceID: "GMH5NISEEULNJL6RATITOA3TMZXMTMCI"}
|
||||
Type: "face",
|
||||
Src: 123,
|
||||
Name: 456,
|
||||
SubjUID: "jqy1y111h1njaaac",
|
||||
SubjSrc: "manual",
|
||||
FaceID: "GMH5NISEEULNJL6RATITOA3TMZXMTMCI"}
|
||||
if b, err := json.Marshal(m); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
|
@ -68,6 +69,111 @@ func GetSubject(router *gin.RouterGroup) {
|
|||
} else {
|
||||
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_dev6",
|
||||
"subjects_dev7",
|
||||
"subjects_dev8",
|
||||
"markers_dev1",
|
||||
"markers_dev2",
|
||||
"markers_dev3",
|
||||
|
@ -28,6 +29,7 @@ var DeprecatedTables = Deprecated{
|
|||
"markers_dev5",
|
||||
"markers_dev6",
|
||||
"markers_dev7",
|
||||
"markers_dev8",
|
||||
"faces_dev1",
|
||||
"faces_dev2",
|
||||
"faces_dev3",
|
||||
|
@ -35,4 +37,5 @@ var DeprecatedTables = Deprecated{
|
|||
"faces_dev5",
|
||||
"faces_dev6",
|
||||
"faces_dev7",
|
||||
"faces_dev8",
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ var faceMutex = sync.Mutex{}
|
|||
type Face struct {
|
||||
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"`
|
||||
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"`
|
||||
SampleRadius float64 `json:"SampleRadius" yaml:"SampleRadius,omitempty"`
|
||||
Collisions int `json:"Collisions" yaml:"Collisions,omitempty"`
|
||||
|
@ -38,14 +38,14 @@ var Faceless = []string{""}
|
|||
|
||||
// TableName returns the entity database table name.
|
||||
func (Face) TableName() string {
|
||||
return "faces_dev8"
|
||||
return "faces_dev9"
|
||||
}
|
||||
|
||||
// NewFace returns a new face.
|
||||
func NewFace(subjectUID, faceSrc string, embeddings Embeddings) *Face {
|
||||
func NewFace(subjUID, faceSrc string, embeddings Embeddings) *Face {
|
||||
result := &Face{
|
||||
SubjectUID: subjectUID,
|
||||
FaceSrc: faceSrc,
|
||||
SubjUID: subjUID,
|
||||
FaceSrc: faceSrc,
|
||||
}
|
||||
|
||||
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.
|
||||
func (m *Face) ResolveCollision(embeddings Embeddings) (resolved bool, err error) {
|
||||
if m.SubjectUID == "" {
|
||||
if m.SubjUID == "" {
|
||||
// Ignore reports for anonymous faces.
|
||||
return false, nil
|
||||
} 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)
|
||||
|
||||
// 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 {
|
||||
m.MatchedAt = nil
|
||||
m.Collisions++
|
||||
|
@ -242,20 +242,20 @@ func (m *Face) MatchMarkers(faceIds []string) error {
|
|||
}
|
||||
|
||||
// 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.
|
||||
if err = m.Update("SubjectUID", uid); err != nil {
|
||||
if err = m.Update("SubjUID", subjUID); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m.SubjectUID = uid
|
||||
m.SubjUID = subjUID
|
||||
}
|
||||
|
||||
// Update related markers.
|
||||
if err = Db().Model(&Marker{}).
|
||||
Where("face_id = ?", m.ID).
|
||||
Where("subject_src = ?", SrcAuto).
|
||||
Where("subject_uid <> ?", m.SubjectUID).
|
||||
Updates(Values{"SubjectUID": m.SubjectUID, "Review": false}).Error; err != nil {
|
||||
Where("subj_src = ?", SrcAuto).
|
||||
Where("subj_uid <> ?", m.SubjUID).
|
||||
Updates(Values{"SubjUID": m.SubjUID, "MarkerReview": false}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -309,12 +309,12 @@ func FirstOrCreateFace(m *Face) *Face {
|
|||
result := Face{}
|
||||
|
||||
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
|
||||
} else if createErr := m.Create(); createErr == nil {
|
||||
return m
|
||||
} 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
|
||||
} else {
|
||||
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) {
|
||||
t.Run("get existing face", func(t *testing.T) {
|
||||
r := FaceFixtures.Get("jane-doe")
|
||||
assert.Equal(t, "jqy1y111h1njaaab", r.SubjectUID)
|
||||
assert.Equal(t, "jqy1y111h1njaaab", r.SubjUID)
|
||||
assert.Equal(t, "VF7ANLDET2BKZNT4VQWJMMC6HBEFDOG7", r.ID)
|
||||
assert.IsType(t, Face{}, r)
|
||||
})
|
||||
|
@ -23,7 +23,7 @@ func TestFaceMap_Get(t *testing.T) {
|
|||
func TestFaceMap_Pointer(t *testing.T) {
|
||||
t.Run("get existing face", func(t *testing.T) {
|
||||
r := FaceFixtures.Pointer("jane-doe")
|
||||
assert.Equal(t, "jqy1y111h1njaaab", r.SubjectUID)
|
||||
assert.Equal(t, "jqy1y111h1njaaab", r.SubjUID)
|
||||
assert.Equal(t, "VF7ANLDET2BKZNT4VQWJMMC6HBEFDOG7", r.ID)
|
||||
assert.IsType(t, &Face{}, r)
|
||||
})
|
||||
|
|
|
@ -133,7 +133,7 @@ func TestNewFace(t *testing.T) {
|
|||
|
||||
r := NewFace("123", SrcAuto, e)
|
||||
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))
|
||||
m.Save()
|
||||
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) {
|
||||
|
@ -193,11 +193,11 @@ func TestFace_Update(t *testing.T) {
|
|||
assert.Nil(t, FindFace(m.ID))
|
||||
m.Save()
|
||||
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.Update("SubjectUID", "new")
|
||||
assert.Equal(t, "new", FindFace(m.ID).SubjectUID)
|
||||
m2.Update("SubjUID", "new")
|
||||
assert.Equal(t, "new", FindFace(m.ID).SubjUID)
|
||||
}
|
||||
|
||||
func TestFace_RefreshPhotos(t *testing.T) {
|
||||
|
@ -212,12 +212,12 @@ func TestFirstOrCreateFace(t *testing.T) {
|
|||
t.Run("create new face", func(t *testing.T) {
|
||||
m := NewFace("12345unique", SrcAuto, Embeddings{Embedding{99}, Embedding{2}})
|
||||
r := FirstOrCreateFace(m)
|
||||
assert.Equal(t, "12345unique", r.SubjectUID)
|
||||
assert.Equal(t, "12345unique", r.SubjUID)
|
||||
})
|
||||
t.Run("return existing entity", func(t *testing.T) {
|
||||
m := FaceFixtures.Pointer("joe-biden")
|
||||
r := FirstOrCreateFace(m)
|
||||
assert.Equal(t, "jqy3y652h8njw0sx", r.SubjectUID)
|
||||
assert.Equal(t, "jqy3y652h8njw0sx", r.SubjUID)
|
||||
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.
|
||||
func (m *File) AddFace(f face.Face, subjectUID string) {
|
||||
marker := *NewFaceMarker(f, *m, subjectUID)
|
||||
func (m *File) AddFace(f face.Face, subjUID string) {
|
||||
marker := *NewFaceMarker(f, *m, subjUID)
|
||||
|
||||
if markers := m.Markers(); !markers.Contains(marker) {
|
||||
markers.Append(marker)
|
||||
|
|
|
@ -34,9 +34,10 @@ type Marker struct {
|
|||
MarkerSrc string `gorm:"type:VARBINARY(8);default:'';" json:"Src" yaml:"Src,omitempty"`
|
||||
MarkerName string `gorm:"type:VARCHAR(255);" json:"Name" yaml:"Name,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"`
|
||||
SubjectSrc string `gorm:"type:VARBINARY(8);index:idx_markers_subject_uid_src;default:'';" json:"SubjectSrc" yaml:"SubjectSrc,omitempty"`
|
||||
subject *Subject `gorm:"foreignkey:SubjectUID;association_foreignkey:SubjectUID;association_autoupdate:false;association_autocreate:false;association_save_reference:false"`
|
||||
MarkerReview bool `json:"Review" yaml:"Review,omitempty"`
|
||||
SubjUID string `gorm:"type:VARBINARY(42);index:idx_markers_subj_uid_src;" json:"SubjUID" yaml:"SubjUID,omitempty"`
|
||||
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"`
|
||||
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"`
|
||||
|
@ -49,7 +50,6 @@ type Marker struct {
|
|||
H float32 `gorm:"type:FLOAT;" json:"H" yaml:"H,omitempty"`
|
||||
Size int `gorm:"default:-1" json:"Size" yaml:"Size,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"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
|
@ -57,7 +57,7 @@ type Marker struct {
|
|||
|
||||
// TableName returns the entity database table name.
|
||||
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.
|
||||
|
@ -70,14 +70,14 @@ func (m *Marker) BeforeCreate(scope *gorm.Scope) error {
|
|||
}
|
||||
|
||||
// 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{
|
||||
FileUID: file.FileUID,
|
||||
FileHash: file.FileHash,
|
||||
CropArea: area.String(),
|
||||
MarkerSrc: markerSrc,
|
||||
MarkerType: markerType,
|
||||
SubjectUID: subjectUID,
|
||||
SubjUID: subjUID,
|
||||
X: area.X,
|
||||
Y: area.Y,
|
||||
W: area.W,
|
||||
|
@ -89,12 +89,12 @@ func NewMarker(file File, area crop.Area, subjectUID, markerSrc, markerType stri
|
|||
}
|
||||
|
||||
// NewFaceMarker creates a new entity.
|
||||
func NewFaceMarker(f face.Face, file File, subjectUID string) *Marker {
|
||||
m := NewMarker(file, f.CropArea(), subjectUID, SrcImage, MarkerFace)
|
||||
func NewFaceMarker(f face.Face, file File, subjUID string) *Marker {
|
||||
m := NewMarker(file, f.CropArea(), subjUID, SrcImage, MarkerFace)
|
||||
|
||||
m.Size = f.Size()
|
||||
m.Score = f.Score
|
||||
m.Review = f.Score < 30
|
||||
m.MarkerReview = f.Score < 30
|
||||
m.FaceDist = -1
|
||||
m.EmbeddingsJSON = f.EmbeddingsJSON()
|
||||
m.LandmarksJSON = f.RelativeLandmarksJSON()
|
||||
|
@ -121,13 +121,13 @@ func (m *Marker) SaveForm(f form.Marker) error {
|
|||
changed = true
|
||||
}
|
||||
|
||||
if m.Review != f.Review {
|
||||
m.Review = f.Review
|
||||
if m.MarkerReview != f.MarkerReview {
|
||||
m.MarkerReview = f.MarkerReview
|
||||
changed = true
|
||||
}
|
||||
|
||||
if f.SubjectSrc == SrcManual && strings.TrimSpace(f.MarkerName) != "" {
|
||||
m.SubjectSrc = SrcManual
|
||||
if f.SubjSrc == SrcManual && strings.TrimSpace(f.MarkerName) != "" {
|
||||
m.SubjSrc = SrcManual
|
||||
m.MarkerName = txt.Title(txt.Clip(f.MarkerName, txt.ClipDefault))
|
||||
|
||||
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?
|
||||
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.
|
||||
} else if reported, err := f.ResolveCollision(m.Embeddings()); err != nil {
|
||||
return false, err
|
||||
} 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
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
} else if err = f.SetSubjectUID(m.SubjectUID); err != nil {
|
||||
} else if err = f.SetSubjectUID(m.SubjUID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
@ -194,7 +194,7 @@ func (m *Marker) SetFace(f *Face, dist float64) (updated bool, err error) {
|
|||
m.face = f
|
||||
|
||||
// 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.
|
||||
m.MatchedAt = TimePointer()
|
||||
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.
|
||||
faceID := m.FaceID
|
||||
subjectUID := m.SubjectUID
|
||||
SubjectSrc := m.SubjectSrc
|
||||
subjUID := m.SubjUID
|
||||
subjSrc := m.SubjSrc
|
||||
|
||||
m.FaceID = f.ID
|
||||
m.FaceDist = dist
|
||||
|
@ -211,7 +211,7 @@ func (m *Marker) SetFace(f *Face, dist float64) (updated bool, err error) {
|
|||
if m.FaceDist < 0 {
|
||||
faceEmbedding := f.Embedding()
|
||||
|
||||
// Calculate smallest distance to embeddings.
|
||||
// Calculate the smallest distance to embeddings.
|
||||
for _, e := range m.Embeddings() {
|
||||
if len(e) != len(faceEmbedding) {
|
||||
continue
|
||||
|
@ -223,8 +223,8 @@ func (m *Marker) SetFace(f *Face, dist float64) (updated bool, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if f.SubjectUID != "" {
|
||||
m.SubjectUID = f.SubjectUID
|
||||
if f.SubjUID != "" {
|
||||
m.SubjUID = f.SubjUID
|
||||
}
|
||||
|
||||
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?
|
||||
if m.SubjectSrc == SrcAuto || m.SubjectUID == "" || f.SubjectUID == m.SubjectUID {
|
||||
if m.SubjSrc == SrcAuto || m.SubjUID == "" || f.SubjUID == m.SubjUID {
|
||||
// Not needed.
|
||||
} else if err = f.SetSubjectUID(m.SubjectUID); err != nil {
|
||||
} else if err = f.SetSubjectUID(m.SubjUID); err != nil {
|
||||
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.
|
||||
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
|
||||
} else if !updated {
|
||||
return false, nil
|
||||
|
@ -261,20 +261,20 @@ func (m *Marker) SyncSubject(updateRelated bool) (err error) {
|
|||
|
||||
subj := m.Subject()
|
||||
|
||||
if subj == nil || m.SubjectSrc == SrcAuto {
|
||||
if subj == nil || m.SubjSrc == SrcAuto {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update subject with marker name?
|
||||
if m.MarkerName == "" || subj.SubjectName == m.MarkerName {
|
||||
if m.MarkerName == "" || subj.SubjName == m.MarkerName {
|
||||
// Do nothing.
|
||||
} else if subj, err = subj.UpdateName(m.MarkerName); err != nil {
|
||||
return err
|
||||
} else if subj != nil {
|
||||
// Update subject fields in case it was merged.
|
||||
m.subject = subj
|
||||
m.SubjectUID = subj.SubjectUID
|
||||
m.MarkerName = subj.SubjectName
|
||||
m.SubjUID = subj.SubjUID
|
||||
m.MarkerName = subj.SubjName
|
||||
}
|
||||
|
||||
// Create known face for subject?
|
||||
|
@ -285,21 +285,21 @@ func (m *Marker) SyncSubject(updateRelated bool) (err error) {
|
|||
}
|
||||
|
||||
// Update related markers?
|
||||
if m.FaceID == "" || m.SubjectUID == "" {
|
||||
if m.FaceID == "" || m.SubjUID == "" {
|
||||
// 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)
|
||||
} else if !updateRelated {
|
||||
return nil
|
||||
} else if err := Db().Model(&Marker{}).
|
||||
Where("marker_uid <> ?", m.MarkerUID).
|
||||
Where("face_id = ?", m.FaceID).
|
||||
Where("subject_src = ?", SrcAuto).
|
||||
Where("subject_uid <> ?", m.SubjectUID).
|
||||
Updates(Values{"SubjectUID": m.SubjectUID, "SubjectSrc": SrcAuto, "Review": false}).Error; err != nil {
|
||||
Where("subj_src = ?", SrcAuto).
|
||||
Where("subj_uid <> ?", m.SubjUID).
|
||||
Updates(Values{"SubjUID": m.SubjUID, "SubjSrc": SrcAuto, "MarkerReview": false}).Error; err != nil {
|
||||
return fmt.Errorf("%s (update related markers)", err)
|
||||
} 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()
|
||||
}
|
||||
|
||||
|
@ -342,7 +342,7 @@ func (m *Marker) SubjectName() string {
|
|||
if m.MarkerName != "" {
|
||||
return m.MarkerName
|
||||
} else if s := m.Subject(); s != nil {
|
||||
return s.SubjectName
|
||||
return s.SubjName
|
||||
}
|
||||
|
||||
return ""
|
||||
|
@ -351,27 +351,27 @@ func (m *Marker) SubjectName() string {
|
|||
// Subject returns the matching subject or nil.
|
||||
func (m *Marker) Subject() (subj *Subject) {
|
||||
if m.subject != nil {
|
||||
if m.SubjectUID == m.subject.SubjectUID {
|
||||
if m.SubjUID == m.subject.SubjUID {
|
||||
return m.subject
|
||||
}
|
||||
}
|
||||
|
||||
// Create subject?
|
||||
if m.SubjectSrc != SrcAuto && m.MarkerName != "" && m.SubjectUID == "" {
|
||||
if subj = NewSubject(m.MarkerName, SubjectPerson, m.SubjectSrc); subj == nil {
|
||||
if m.SubjSrc != SrcAuto && m.MarkerName != "" && m.SubjUID == "" {
|
||||
if subj = NewSubject(m.MarkerName, SubjPerson, m.SubjSrc); subj == nil {
|
||||
return nil
|
||||
} else if subj = FirstOrCreateSubject(subj); subj == nil {
|
||||
log.Debugf("marker: invalid subject %s", txt.Quote(m.MarkerName))
|
||||
return nil
|
||||
} else {
|
||||
m.subject = subj
|
||||
m.SubjectUID = subj.SubjectUID
|
||||
m.SubjUID = subj.SubjUID
|
||||
}
|
||||
|
||||
return m.subject
|
||||
}
|
||||
|
||||
m.subject = FindSubject(m.SubjectUID)
|
||||
m.subject = FindSubject(m.SubjUID)
|
||||
|
||||
return m.subject
|
||||
}
|
||||
|
@ -384,7 +384,7 @@ func (m *Marker) ClearSubject(src string) error {
|
|||
}
|
||||
|
||||
// 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
|
||||
} else if m.face == nil {
|
||||
m.subject = nil
|
||||
|
@ -411,14 +411,14 @@ func (m *Marker) Face() (f *Face) {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
log.Debugf("faces: skipped adding face for low-quality marker %s, size %d, score %d", m.MarkerUID, m.Size, m.Score)
|
||||
return nil
|
||||
} else if emb := m.Embeddings(); len(emb) == 0 {
|
||||
log.Warnf("marker: %s has no embeddings", m.MarkerUID)
|
||||
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)
|
||||
return nil
|
||||
} else if f = FirstOrCreateFace(f); f == nil {
|
||||
|
@ -452,9 +452,9 @@ func (m *Marker) ClearFace() (updated bool, err error) {
|
|||
m.MatchedAt = TimePointer()
|
||||
|
||||
// Remove subject if set automatically.
|
||||
if m.SubjectSrc == SrcAuto {
|
||||
m.SubjectUID = ""
|
||||
err = m.Updates(Values{"FaceID": "", "FaceDist": -1.0, "SubjectUID": "", "MatchedAt": m.MatchedAt})
|
||||
if m.SubjSrc == SrcAuto {
|
||||
m.SubjUID = ""
|
||||
err = m.Updates(Values{"FaceID": "", "FaceDist": -1.0, "SubjUID": "", "MatchedAt": m.MatchedAt})
|
||||
} else {
|
||||
err = m.Updates(Values{"FaceID": "", "FaceDist": -1.0, "MatchedAt": m.MatchedAt})
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-1": Marker{ //Photo04
|
||||
MarkerUID: "mqu0xs11qekk9jx8",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
SubjectUID: "jqu0xs11qekk9jx8",
|
||||
SubjUID: "jqu0xs11qekk9jx8",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerLabel,
|
||||
X: 0.308333,
|
||||
|
@ -40,7 +40,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-2": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy3c3",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
SubjectUID: "lt9k3pw1wowuy3c3",
|
||||
SubjUID: "lt9k3pw1wowuy3c3",
|
||||
FaceID: "LRG2HJBDZE66LYG7Q5SRFXO2MDTOES52",
|
||||
MarkerName: "Unknown",
|
||||
MarkerSrc: SrcImage,
|
||||
|
@ -55,7 +55,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-3": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy111",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
SubjectUID: "",
|
||||
SubjUID: "",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerLabel,
|
||||
MarkerName: "Center",
|
||||
|
@ -69,7 +69,7 @@ var MarkerFixtures = MarkerMap{
|
|||
"1000003-4": Marker{ //Photo04
|
||||
MarkerUID: "mt9k3pw1wowuy222",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
SubjectUID: "",
|
||||
SubjUID: "",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "Jens Mander",
|
||||
|
@ -86,8 +86,8 @@ var MarkerFixtures = MarkerMap{
|
|||
MarkerUID: "mt9k3pw1wowuy333",
|
||||
FileUID: "ft2es39w45bnlqdw",
|
||||
FaceID: FaceFixtures.Get("unknown").ID,
|
||||
SubjectUID: "",
|
||||
SubjectSrc: SrcAuto,
|
||||
SubjUID: "",
|
||||
SubjSrc: SrcAuto,
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "Corn McCornface",
|
||||
|
@ -105,8 +105,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FileUID: "ft2es39w45bnlqdw",
|
||||
FaceID: FaceFixtures.Get("john-doe").ID,
|
||||
FaceDist: 0.2,
|
||||
SubjectSrc: SrcAuto,
|
||||
SubjectUID: "",
|
||||
SubjSrc: SrcAuto,
|
||||
SubjUID: "",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "",
|
||||
|
@ -124,8 +124,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FileUID: "ft2es49qhhinlple",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.5,
|
||||
SubjectSrc: "",
|
||||
SubjectUID: "",
|
||||
SubjSrc: "",
|
||||
SubjUID: "",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "",
|
||||
|
@ -143,8 +143,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FileUID: "ft2es49qhhinlple",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.6,
|
||||
SubjectSrc: SrcAuto,
|
||||
SubjectUID: "",
|
||||
SubjSrc: SrcAuto,
|
||||
SubjUID: "",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "",
|
||||
|
@ -162,8 +162,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FileUID: "ft2es49w15bnlqdw",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.6,
|
||||
SubjectSrc: SrcAuto,
|
||||
SubjectUID: "",
|
||||
SubjSrc: SrcAuto,
|
||||
SubjUID: "",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "",
|
||||
|
@ -181,8 +181,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FileUID: "ft8es39w45bnlqdw",
|
||||
FaceID: FaceFixtures.Get("fa-gr").ID,
|
||||
FaceDist: 0.6,
|
||||
SubjectSrc: SrcAuto,
|
||||
SubjectUID: "",
|
||||
SubjSrc: SrcAuto,
|
||||
SubjUID: "",
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "",
|
||||
|
@ -201,8 +201,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||
CropArea: "045038063041",
|
||||
FaceDist: 0.26852392873736236,
|
||||
SubjectSrc: SrcManual,
|
||||
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
||||
SubjSrc: SrcManual,
|
||||
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "Actress A",
|
||||
|
@ -221,8 +221,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||
CropArea: "046045043065",
|
||||
FaceDist: 0.4507357278575355,
|
||||
SubjectSrc: "",
|
||||
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
||||
SubjSrc: "",
|
||||
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "",
|
||||
|
@ -241,8 +241,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FaceID: FaceFixtures.Get("actress-1").ID,
|
||||
CropArea: "05403304060446",
|
||||
FaceDist: 0.5099754448545762,
|
||||
SubjectSrc: "",
|
||||
SubjectUID: SubjectFixtures.Get("actress-1").SubjectUID,
|
||||
SubjSrc: "",
|
||||
SubjUID: SubjectFixtures.Get("actress-1").SubjUID,
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "",
|
||||
|
@ -260,8 +260,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FileUID: "ft3es39w45bnlqdw",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.5223304453393212,
|
||||
SubjectSrc: "",
|
||||
SubjectUID: SubjectFixtures.Get("actor-1").SubjectUID,
|
||||
SubjSrc: "",
|
||||
SubjUID: SubjectFixtures.Get("actor-1").SubjUID,
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "",
|
||||
|
@ -279,8 +279,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FileUID: "ft2es39q45bnlqd0",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.5088545446490167,
|
||||
SubjectSrc: "",
|
||||
SubjectUID: SubjectFixtures.Get("actor-1").SubjectUID,
|
||||
SubjSrc: "",
|
||||
SubjUID: SubjectFixtures.Get("actor-1").SubjUID,
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "",
|
||||
|
@ -298,8 +298,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FileUID: "fikjs39w45bnlqdw",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.3139983399779298,
|
||||
SubjectSrc: "",
|
||||
SubjectUID: SubjectFixtures.Get("actor-1").SubjectUID,
|
||||
SubjSrc: "",
|
||||
SubjUID: SubjectFixtures.Get("actor-1").SubjUID,
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "",
|
||||
|
@ -317,8 +317,8 @@ var MarkerFixtures = MarkerMap{
|
|||
FileUID: "ft8es39w45bnlqdw",
|
||||
FaceID: FaceFixtures.Get("actor-1").ID,
|
||||
FaceDist: 0.3139983399779298,
|
||||
SubjectSrc: "",
|
||||
SubjectUID: SubjectFixtures.Get("actor-1").SubjectUID,
|
||||
SubjSrc: "",
|
||||
SubjUID: SubjectFixtures.Get("actor-1").SubjUID,
|
||||
MarkerSrc: SrcImage,
|
||||
MarkerType: MarkerFace,
|
||||
MarkerName: "",
|
||||
|
|
|
@ -13,48 +13,48 @@ func (m *Marker) MarshalJSON() ([]byte, error) {
|
|||
if subj = m.Subject(); subj == nil {
|
||||
name = m.MarkerName
|
||||
} else {
|
||||
name = subj.SubjectName
|
||||
name = subj.SubjName
|
||||
}
|
||||
|
||||
return json.Marshal(&struct {
|
||||
UID string
|
||||
FileUID string
|
||||
FileHash string
|
||||
CropArea string
|
||||
Type string
|
||||
Src string
|
||||
Name string
|
||||
Invalid bool
|
||||
Review bool
|
||||
FaceID string
|
||||
SubjectUID string
|
||||
SubjectSrc string
|
||||
X float32
|
||||
Y float32
|
||||
W float32 `json:",omitempty"`
|
||||
H float32 `json:",omitempty"`
|
||||
Size int `json:",omitempty"`
|
||||
Score int `json:",omitempty"`
|
||||
CreatedAt time.Time
|
||||
UID string
|
||||
FileUID string
|
||||
FileHash string
|
||||
CropArea string
|
||||
Type string
|
||||
Src string
|
||||
Name string
|
||||
Invalid bool
|
||||
Review bool
|
||||
FaceID string
|
||||
SubjUID string
|
||||
SubjSrc string
|
||||
X float32
|
||||
Y float32
|
||||
W float32 `json:",omitempty"`
|
||||
H float32 `json:",omitempty"`
|
||||
Size int `json:",omitempty"`
|
||||
Score int `json:",omitempty"`
|
||||
CreatedAt time.Time
|
||||
}{
|
||||
UID: m.MarkerUID,
|
||||
FileUID: m.FileUID,
|
||||
FileHash: m.FileHash,
|
||||
CropArea: m.CropArea,
|
||||
Type: m.MarkerType,
|
||||
Src: m.MarkerSrc,
|
||||
Name: name,
|
||||
Invalid: m.MarkerInvalid,
|
||||
Review: m.Review,
|
||||
FaceID: m.FaceID,
|
||||
SubjectUID: m.SubjectUID,
|
||||
SubjectSrc: m.SubjectSrc,
|
||||
X: m.X,
|
||||
Y: m.Y,
|
||||
W: m.W,
|
||||
H: m.H,
|
||||
Size: m.Size,
|
||||
Score: m.Score,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UID: m.MarkerUID,
|
||||
FileUID: m.FileUID,
|
||||
FileHash: m.FileHash,
|
||||
CropArea: m.CropArea,
|
||||
Type: m.MarkerType,
|
||||
Src: m.MarkerSrc,
|
||||
Name: name,
|
||||
Invalid: m.MarkerInvalid,
|
||||
Review: m.MarkerReview,
|
||||
FaceID: m.FaceID,
|
||||
SubjUID: m.SubjUID,
|
||||
SubjSrc: m.SubjSrc,
|
||||
X: m.X,
|
||||
Y: m.Y,
|
||||
W: m.W,
|
||||
H: m.H,
|
||||
Size: m.Size,
|
||||
Score: m.Score,
|
||||
CreatedAt: m.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ func TestNewMarker(t *testing.T) {
|
|||
assert.Equal(t, "ft8es39w45bnlqdw", m.FileUID)
|
||||
assert.Equal(t, "2cad9168fa6acc5c5c2965ddf6ec465ca42fd818", m.FileHash)
|
||||
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, MarkerLabel, m.MarkerType)
|
||||
}
|
||||
|
@ -38,16 +38,16 @@ func TestMarker_SaveForm(t *testing.T) {
|
|||
m2 := MarkerFixtures.Get("fa-gr-2")
|
||||
m3 := MarkerFixtures.Get("fa-gr-3")
|
||||
|
||||
assert.Empty(t, m.SubjectUID)
|
||||
assert.Empty(t, m2.SubjectUID)
|
||||
assert.Empty(t, m3.SubjectUID)
|
||||
assert.Empty(t, m.SubjUID)
|
||||
assert.Empty(t, m2.SubjUID)
|
||||
assert.Empty(t, m3.SubjUID)
|
||||
|
||||
m.MarkerInvalid = true
|
||||
m.Score = 50
|
||||
|
||||
//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)
|
||||
|
||||
|
@ -55,20 +55,20 @@ func TestMarker_SaveForm(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, m.SubjectUID)
|
||||
assert.NotEmpty(t, m.SubjUID)
|
||||
|
||||
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 {
|
||||
assert.Equal(t, "Jane Doe", m.Subject().SubjectName)
|
||||
assert.Equal(t, "Jane Doe", m.Subject().SubjName)
|
||||
}
|
||||
if m := FindMarker("mt9k3pw1wowuy888"); m != nil {
|
||||
assert.Equal(t, "Jane Doe", m.Subject().SubjectName)
|
||||
assert.Equal(t, "Jane Doe", m.Subject().SubjName)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
t.Fatal("result is nil")
|
||||
|
@ -77,13 +77,13 @@ func TestMarker_SaveForm(t *testing.T) {
|
|||
}
|
||||
|
||||
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 {
|
||||
assert.Equal(t, "Franzilein", m.Subject().SubjectName)
|
||||
assert.Equal(t, "Franzilein", m.Subject().SubjName)
|
||||
}
|
||||
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)
|
||||
assert.IsType(t, &Marker{}, m)
|
||||
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, MarkerLabel, m.MarkerType)
|
||||
|
||||
|
@ -227,10 +227,10 @@ func TestMarker_ClearSubject(t *testing.T) {
|
|||
m3 := MarkerFixtures.Get("actor-a-2") // id 16
|
||||
m4 := MarkerFixtures.Get("actor-a-1") // id 15
|
||||
|
||||
assert.Equal(t, "jqy1y111h1njaaad", m.SubjectUID)
|
||||
assert.Equal(t, "jqy1y111h1njaaad", m2.SubjectUID)
|
||||
assert.Equal(t, "jqy1y111h1njaaad", m3.SubjectUID)
|
||||
assert.Equal(t, "jqy1y111h1njaaad", m4.SubjectUID)
|
||||
assert.Equal(t, "jqy1y111h1njaaad", m.SubjUID)
|
||||
assert.Equal(t, "jqy1y111h1njaaad", m2.SubjUID)
|
||||
assert.Equal(t, "jqy1y111h1njaaad", m3.SubjUID)
|
||||
assert.Equal(t, "jqy1y111h1njaaad", m4.SubjUID)
|
||||
assert.NotNil(t, m.Face())
|
||||
assert.NotNil(t, m2.Face())
|
||||
assert.NotNil(t, m3.Face())
|
||||
|
@ -260,10 +260,10 @@ func TestMarker_ClearSubject(t *testing.T) {
|
|||
assert.NotNil(t, FindMarker("mt9k3pw1wowu1002"))
|
||||
assert.NotNil(t, FindFace("PI6A2XGOTUXEFI7CBF4KCI5I2I3JEJHS"))
|
||||
|
||||
assert.Empty(t, m.SubjectUID)
|
||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1004").SubjectUID)
|
||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1003").SubjectUID)
|
||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1002").SubjectUID)
|
||||
assert.Empty(t, m.SubjUID)
|
||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1004").SubjUID)
|
||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1003").SubjUID)
|
||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1002").SubjUID)
|
||||
assert.Empty(t, m.FaceID)
|
||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1004").FaceID)
|
||||
assert.Equal(t, "", FindMarker("mt9k3pw1wowu1003").FaceID)
|
||||
|
@ -385,43 +385,43 @@ func TestMarker_HasFace(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMarker_Subject(t *testing.T) {
|
||||
t.Run("EmptySubjectUID", func(t *testing.T) {
|
||||
m := Marker{SubjectUID: "", subject: &Subject{SubjectUID: "", SubjectName: "Test Subject"}}
|
||||
t.Run("EmptySubjUID", func(t *testing.T) {
|
||||
m := Marker{SubjUID: "", subject: &Subject{SubjUID: "", SubjName: "Test Subject"}}
|
||||
|
||||
if s := m.Subject(); s == nil {
|
||||
t.Fatal("return value must not be nil")
|
||||
} else {
|
||||
assert.Equal(t, "Test Subject", s.SubjectName)
|
||||
assert.Equal(t, "", m.SubjectUID)
|
||||
assert.Equal(t, "", s.SubjectUID)
|
||||
assert.Equal(t, "Test Subject", s.SubjName)
|
||||
assert.Equal(t, "", m.SubjUID)
|
||||
assert.Equal(t, "", s.SubjUID)
|
||||
}
|
||||
})
|
||||
t.Run("ConflictingSubjectUID", func(t *testing.T) {
|
||||
m := Marker{SubjectUID: "", subject: &Subject{SubjectUID: "xyz", SubjectName: "Test Subject"}}
|
||||
t.Run("ConflictingSubjUID", func(t *testing.T) {
|
||||
m := Marker{SubjUID: "", subject: &Subject{SubjUID: "xyz", SubjName: "Test Subject"}}
|
||||
|
||||
if s := m.Subject(); s != nil {
|
||||
t.Fatal("return value must be nil")
|
||||
}
|
||||
})
|
||||
t.Run("SubjectSrcAuto", func(t *testing.T) {
|
||||
m := Marker{SubjectSrc: SrcAuto, SubjectUID: "", MarkerName: "Hans Mayer"}
|
||||
t.Run("SubjSrcAuto", func(t *testing.T) {
|
||||
m := Marker{SubjSrc: SrcAuto, SubjUID: "", MarkerName: "Hans Mayer"}
|
||||
|
||||
if s := m.Subject(); s != nil {
|
||||
t.Fatal("return value must be nil")
|
||||
} else {
|
||||
assert.Equal(t, "Hans Mayer", m.MarkerName)
|
||||
assert.Empty(t, m.SubjectUID)
|
||||
assert.Equal(t, SrcAuto, m.SubjectSrc)
|
||||
assert.Empty(t, m.SubjUID)
|
||||
assert.Equal(t, SrcAuto, m.SubjSrc)
|
||||
}
|
||||
})
|
||||
t.Run("SubjectSrcManual", func(t *testing.T) {
|
||||
m := Marker{SubjectSrc: SrcManual, SubjectUID: "", MarkerName: "Hans Mayer"}
|
||||
t.Run("SubjSrcManual", func(t *testing.T) {
|
||||
m := Marker{SubjSrc: SrcManual, SubjUID: "", MarkerName: "Hans Mayer"}
|
||||
|
||||
if s := m.Subject(); s == nil {
|
||||
t.Fatal("return value must not be nil")
|
||||
} else {
|
||||
assert.Equal(t, "Hans Mayer", s.SubjectName)
|
||||
assert.NotEmpty(t, s.SubjectUID)
|
||||
assert.Equal(t, "Hans Mayer", s.SubjName)
|
||||
assert.NotEmpty(t, s.SubjUID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -453,11 +453,11 @@ func TestMarker_GetFace(t *testing.T) {
|
|||
if f := m.Face(); f == nil {
|
||||
t.Fatal("return value must not be nil")
|
||||
} else {
|
||||
assert.Equal(t, "jqy3y652h8njw0sx", f.SubjectUID)
|
||||
assert.Equal(t, "jqy3y652h8njw0sx", f.SubjUID)
|
||||
}
|
||||
})
|
||||
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())
|
||||
})
|
||||
|
@ -465,7 +465,7 @@ func TestMarker_GetFace(t *testing.T) {
|
|||
m := Marker{
|
||||
FaceID: "",
|
||||
EmbeddingsJSON: MarkerFixtures.Get("actress-a-1").EmbeddingsJSON,
|
||||
SubjectSrc: SrcManual,
|
||||
SubjSrc: SrcManual,
|
||||
Size: 160,
|
||||
Score: 40,
|
||||
}
|
||||
|
@ -499,13 +499,13 @@ func TestMarker_SetFace(t *testing.T) {
|
|||
assert.Equal(t, "", m.FaceID)
|
||||
})
|
||||
t.Run("skip same face", func(t *testing.T) {
|
||||
m := Marker{MarkerType: MarkerFace, SubjectUID: "jqu0xs11qekk9jx8", FaceID: "99876uyt"}
|
||||
updated, _ := m.SetFace(&Face{ID: "99876uyt", SubjectUID: "jqu0xs11qekk9jx8"}, -1)
|
||||
m := Marker{MarkerType: MarkerFace, SubjUID: "jqu0xs11qekk9jx8", FaceID: "99876uyt"}
|
||||
updated, _ := m.SetFace(&Face{ID: "99876uyt", SubjUID: "jqu0xs11qekk9jx8"}, -1)
|
||||
assert.False(t, updated)
|
||||
assert.Equal(t, "99876uyt", m.FaceID)
|
||||
})
|
||||
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)
|
||||
assert.True(t, updated)
|
||||
|
|
|
@ -65,7 +65,7 @@ func UpdatePhotoCounts() (err error) {
|
|||
if err = Db().Table(Subject{}.TableName()).
|
||||
UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(*) FROM files f "+
|
||||
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(),
|
||||
Subject{}.TableName())+
|
||||
" 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.
|
||||
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"`
|
||||
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"`
|
||||
SubjectSrc string `gorm:"type:VARBINARY(8);default:''" json:"Src,omitempty" yaml:"Src,omitempty"`
|
||||
SubjectSlug string `gorm:"type:VARBINARY(255);index;default:''" json:"Slug" yaml:"-"`
|
||||
SubjectName string `gorm:"type:VARCHAR(255);unique_index;default:''" json:"Name" yaml:"Name"`
|
||||
SubjectAlias string `gorm:"type:VARCHAR(255);default:''" json:"Alias" yaml:"Alias"`
|
||||
SubjectBio string `gorm:"type:TEXT;default:''" json:"Bio" yaml:"Bio,omitempty"`
|
||||
SubjectNotes string `gorm:"type:TEXT;default:''" json:"Notes,omitempty" yaml:"Notes,omitempty"`
|
||||
Favorite bool `gorm:"default:false" json:"Favorite" yaml:"Favorite,omitempty"`
|
||||
Private bool `gorm:"default:false" json:"Private" yaml:"Private,omitempty"`
|
||||
Excluded bool `gorm:"default:false" json:"Excluded" yaml:"Excluded,omitempty"`
|
||||
SubjType string `gorm:"type:VARBINARY(8);default:''" json:"Type,omitempty" yaml:"Type,omitempty"`
|
||||
SubjSrc string `gorm:"type:VARBINARY(8);default:''" json:"Src,omitempty" yaml:"Src,omitempty"`
|
||||
SubjSlug string `gorm:"type:VARBINARY(255);index;default:''" json:"Slug" yaml:"-"`
|
||||
SubjName string `gorm:"type:VARCHAR(255);unique_index;default:''" json:"Name" yaml:"Name"`
|
||||
SubjAlias string `gorm:"type:VARCHAR(255);default:''" json:"Alias" yaml:"Alias"`
|
||||
SubjBio string `gorm:"type:TEXT;default:''" json:"Bio" yaml:"Bio,omitempty"`
|
||||
SubjNotes string `gorm:"type:TEXT;default:''" json:"Notes,omitempty" yaml:"Notes,omitempty"`
|
||||
SubjFavorite bool `gorm:"default:false" json:"Favorite" yaml:"Favorite,omitempty"`
|
||||
SubjPrivate bool `gorm:"default:false" json:"Private" yaml:"Private,omitempty"`
|
||||
SubjExcluded bool `gorm:"default:false" json:"Excluded" yaml:"Excluded,omitempty"`
|
||||
FileCount int `gorm:"default:0" json:"Files" yaml:"-"`
|
||||
MetadataJSON json.RawMessage `gorm:"type:MEDIUMBLOB;" json:"Metadata,omitempty" yaml:"Metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
|
@ -42,38 +42,38 @@ type Subject struct {
|
|||
|
||||
// TableName returns the entity database table name.
|
||||
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.
|
||||
func (m *Subject) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsUID(m.SubjectUID, 'j') {
|
||||
if rnd.IsUID(m.SubjUID, 'j') {
|
||||
return nil
|
||||
}
|
||||
|
||||
return scope.SetColumn("SubjectUID", rnd.PPID('j'))
|
||||
return scope.SetColumn("SubjUID", rnd.PPID('j'))
|
||||
}
|
||||
|
||||
// NewSubject returns a new entity.
|
||||
func NewSubject(name, subjectType, subjectSrc string) *Subject {
|
||||
if subjectType == "" {
|
||||
subjectType = SubjectPerson
|
||||
func NewSubject(name, subjType, subjSrc string) *Subject {
|
||||
if subjType == "" {
|
||||
subjType = SubjPerson
|
||||
}
|
||||
|
||||
subjectName := txt.Title(txt.Clip(name, txt.ClipDefault))
|
||||
subjectSlug := slug.Make(txt.Clip(name, txt.ClipSlug))
|
||||
subjName := txt.Title(txt.Clip(name, txt.ClipDefault))
|
||||
subjSlug := slug.Make(txt.Clip(name, txt.ClipSlug))
|
||||
|
||||
// Name is required.
|
||||
if subjectName == "" || subjectSlug == "" {
|
||||
if subjName == "" || subjSlug == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &Subject{
|
||||
SubjectSlug: subjectSlug,
|
||||
SubjectName: subjectName,
|
||||
SubjectType: subjectType,
|
||||
SubjectSrc: subjectSrc,
|
||||
FileCount: 1,
|
||||
SubjSlug: subjSlug,
|
||||
SubjName: subjName,
|
||||
SubjType: subjType,
|
||||
SubjSrc: subjSrc,
|
||||
FileCount: 1,
|
||||
}
|
||||
|
||||
return result
|
||||
|
@ -101,10 +101,12 @@ func (m *Subject) Delete() error {
|
|||
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() {
|
||||
event.EntitiesDeleted("people", []string{m.SubjectUID})
|
||||
event.EntitiesDeleted("people", []string{m.SubjUID})
|
||||
event.Publish("count.people", event.Data{
|
||||
"count": -1,
|
||||
})
|
||||
|
@ -123,11 +125,12 @@ func (m *Subject) Restore() error {
|
|||
if m.Deleted() {
|
||||
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() {
|
||||
event.EntitiesCreated("people", []*Person{m.Person()})
|
||||
|
||||
event.Publish("count.people", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
|
@ -153,14 +156,16 @@ func (m *Subject) Updates(values interface{}) error {
|
|||
func FirstOrCreateSubject(m *Subject) *Subject {
|
||||
if m == nil {
|
||||
return nil
|
||||
} else if m.SubjectName == "" {
|
||||
} else if m.SubjName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if found := FindSubjectByName(m.SubjectName); found != nil {
|
||||
if found := FindSubjectByName(m.SubjName); found != nil {
|
||||
return found
|
||||
} 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() {
|
||||
event.EntitiesCreated("people", []*Person{m.Person()})
|
||||
|
@ -170,10 +175,10 @@ func FirstOrCreateSubject(m *Subject) *Subject {
|
|||
}
|
||||
|
||||
return m
|
||||
} else if found = FindSubjectByName(m.SubjectName); found != nil {
|
||||
} else if found = FindSubjectByName(m.SubjName); found != nil {
|
||||
return found
|
||||
} 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
|
||||
|
@ -187,7 +192,7 @@ func FindSubject(s string) *Subject {
|
|||
|
||||
result := Subject{}
|
||||
|
||||
db := Db().Where("subject_uid = ?", s)
|
||||
db := Db().Where("subj_uid = ?", s)
|
||||
|
||||
if err := db.First(&result).Error; err != nil {
|
||||
return nil
|
||||
|
@ -205,7 +210,7 @@ func FindSubjectByName(s string) *Subject {
|
|||
result := Subject{}
|
||||
|
||||
// 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 {
|
||||
return nil
|
||||
|
@ -213,9 +218,9 @@ func FindSubjectByName(s string) *Subject {
|
|||
|
||||
// Restore if currently deleted.
|
||||
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 {
|
||||
log.Debugf("subject: %s restored", result.SubjectUID)
|
||||
log.Debugf("subject: %s restored", result.SubjUID)
|
||||
}
|
||||
|
||||
return &result
|
||||
|
@ -223,7 +228,7 @@ func FindSubjectByName(s string) *Subject {
|
|||
|
||||
// IsPerson tests if the subject is a person.
|
||||
func (m *Subject) IsPerson() bool {
|
||||
return m.SubjectType == SubjectPerson
|
||||
return m.SubjType == SubjPerson
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
m.SubjectName = txt.Title(newName)
|
||||
m.SubjectSlug = slug.Make(txt.Clip(name, txt.ClipSlug))
|
||||
m.SubjName = txt.Title(newName)
|
||||
m.SubjSlug = slug.Make(txt.Clip(name, txt.ClipSlug))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -249,15 +254,17 @@ func (m *Subject) SetName(name string) error {
|
|||
func (m *Subject) UpdateName(name string) (*Subject, error) {
|
||||
if err := m.SetName(name); err != nil {
|
||||
return m, err
|
||||
} else if err := m.Updates(Values{"SubjectName": m.SubjectName, "SubjectSlug": m.SubjectSlug}); err == nil {
|
||||
log.Infof("subject: renamed %s %s", m.SubjectType, txt.Quote(m.SubjectName))
|
||||
} else if err := m.Updates(Values{"SubjName": m.SubjName, "SubjSlug": m.SubjSlug}); err == nil {
|
||||
log.Infof("subject: renamed %s %s", m.SubjType, txt.Quote(m.SubjName))
|
||||
|
||||
event.EntitiesUpdated("subjects", []*Subject{m})
|
||||
|
||||
if m.IsPerson() {
|
||||
event.EntitiesUpdated("people", []*Person{m.Person()})
|
||||
}
|
||||
|
||||
return m, m.UpdateMarkerNames()
|
||||
} else if existing := FindSubjectByName(m.SubjectName); existing == nil {
|
||||
} else if existing := FindSubjectByName(m.SubjName); existing == nil {
|
||||
return m, err
|
||||
} else {
|
||||
return existing, m.MergeWith(existing)
|
||||
|
@ -266,16 +273,16 @@ func (m *Subject) UpdateName(name string) (*Subject, error) {
|
|||
|
||||
// UpdateMarkerNames updates related marker names.
|
||||
func (m *Subject) UpdateMarkerNames() error {
|
||||
if m.SubjectName == "" {
|
||||
if m.SubjName == "" {
|
||||
return fmt.Errorf("subject name is empty")
|
||||
} else if m.SubjectUID == "" {
|
||||
} else if m.SubjUID == "" {
|
||||
return fmt.Errorf("subject uid is empty")
|
||||
}
|
||||
|
||||
if err := Db().Model(&Marker{}).
|
||||
Where("subject_uid = ? AND subject_src <> ?", m.SubjectUID, SrcAuto).
|
||||
Where("marker_name <> ?", m.SubjectName).
|
||||
Update(Values{"MarkerName": m.SubjectName}).Error; err != nil {
|
||||
Where("subj_uid = ? AND subj_src <> ?", m.SubjUID, SrcAuto).
|
||||
Where("marker_name <> ?", m.SubjName).
|
||||
Update(Values{"MarkerName": m.SubjName}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -284,33 +291,33 @@ func (m *Subject) UpdateMarkerNames() error {
|
|||
|
||||
// RefreshPhotos flags related photos for metadata maintenance.
|
||||
func (m *Subject) RefreshPhotos() error {
|
||||
if m.SubjectUID == "" {
|
||||
if m.SubjUID == "" {
|
||||
return fmt.Errorf("empty subject uid")
|
||||
}
|
||||
|
||||
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)`,
|
||||
gorm.Expr(Marker{}.TableName()), m.SubjectUID).Error
|
||||
(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.SubjUID).Error
|
||||
}
|
||||
|
||||
// MergeWith merges this subject with another subject and then deletes it.
|
||||
func (m *Subject) MergeWith(other *Subject) error {
|
||||
if other == 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")
|
||||
} else if m.SubjectUID == "" {
|
||||
} else if m.SubjUID == "" {
|
||||
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{}).
|
||||
Where("subject_uid = ?", m.SubjectUID).
|
||||
Update(Values{"SubjectUID": other.SubjectUID}).Error; err != nil {
|
||||
Where("subj_uid = ?", m.SubjUID).
|
||||
Update(Values{"SubjUID": other.SubjUID}).Error; err != nil {
|
||||
return err
|
||||
} else if err := Db().Model(&Face{}).
|
||||
Where("subject_uid = ?", m.SubjectUID).
|
||||
Update(Values{"SubjectUID": other.SubjectUID}).Error; err != nil {
|
||||
Where("subj_uid = ?", m.SubjUID).
|
||||
Update(Values{"SubjUID": other.SubjUID}).Error; err != nil {
|
||||
return err
|
||||
} else if err := other.UpdateMarkerNames(); err != nil {
|
||||
return err
|
||||
|
@ -321,5 +328,5 @@ func (m *Subject) MergeWith(other *Subject) error {
|
|||
|
||||
// Links returns all share links for this entity.
|
||||
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{
|
||||
"john-doe": Subject{
|
||||
SubjectUID: "jqu0xs11qekk9jx8",
|
||||
SubjectSlug: "john-doe",
|
||||
SubjectName: "John Doe",
|
||||
SubjectType: SubjectPerson,
|
||||
SubjectSrc: SrcManual,
|
||||
Favorite: true,
|
||||
Private: false,
|
||||
Excluded: false,
|
||||
SubjectBio: "Subject Description",
|
||||
SubjectNotes: "Short Note",
|
||||
SubjUID: "jqu0xs11qekk9jx8",
|
||||
SubjSlug: "john-doe",
|
||||
SubjName: "John Doe",
|
||||
SubjType: SubjPerson,
|
||||
SubjSrc: SrcManual,
|
||||
SubjFavorite: true,
|
||||
SubjPrivate: false,
|
||||
SubjExcluded: false,
|
||||
SubjBio: "Subject Description",
|
||||
SubjNotes: "Short Note",
|
||||
MetadataJSON: []byte(""),
|
||||
FileCount: 1,
|
||||
CreatedAt: TimeStamp(),
|
||||
|
@ -37,16 +37,16 @@ var SubjectFixtures = SubjectMap{
|
|||
DeletedAt: nil,
|
||||
},
|
||||
"joe-biden": Subject{
|
||||
SubjectUID: "jqy3y652h8njw0sx",
|
||||
SubjectSlug: "joe-biden",
|
||||
SubjectName: "Joe Biden",
|
||||
SubjectType: SubjectPerson,
|
||||
SubjectSrc: SrcMarker,
|
||||
Favorite: false,
|
||||
Private: false,
|
||||
Excluded: false,
|
||||
SubjectBio: "",
|
||||
SubjectNotes: "",
|
||||
SubjUID: "jqy3y652h8njw0sx",
|
||||
SubjSlug: "joe-biden",
|
||||
SubjName: "Joe Biden",
|
||||
SubjType: SubjPerson,
|
||||
SubjSrc: SrcMarker,
|
||||
SubjFavorite: false,
|
||||
SubjPrivate: false,
|
||||
SubjExcluded: false,
|
||||
SubjBio: "",
|
||||
SubjNotes: "",
|
||||
MetadataJSON: []byte(""),
|
||||
FileCount: 1,
|
||||
CreatedAt: TimeStamp(),
|
||||
|
@ -54,17 +54,17 @@ var SubjectFixtures = SubjectMap{
|
|||
DeletedAt: nil,
|
||||
},
|
||||
"dangling": Subject{
|
||||
SubjectUID: "jqy1y111h1njaaaa",
|
||||
SubjectSlug: "dangling-subject",
|
||||
SubjectName: "Dangling Subject",
|
||||
SubjectAlias: "Powell",
|
||||
SubjectType: SubjectPerson,
|
||||
SubjectSrc: SrcMarker,
|
||||
Favorite: false,
|
||||
Private: false,
|
||||
Excluded: false,
|
||||
SubjectBio: "",
|
||||
SubjectNotes: "",
|
||||
SubjUID: "jqy1y111h1njaaaa",
|
||||
SubjSlug: "dangling-subject",
|
||||
SubjName: "Dangling Subject",
|
||||
SubjAlias: "Powell",
|
||||
SubjType: SubjPerson,
|
||||
SubjSrc: SrcMarker,
|
||||
SubjFavorite: false,
|
||||
SubjPrivate: false,
|
||||
SubjExcluded: false,
|
||||
SubjBio: "",
|
||||
SubjNotes: "",
|
||||
MetadataJSON: []byte(""),
|
||||
FileCount: 0,
|
||||
CreatedAt: TimeStamp(),
|
||||
|
@ -72,16 +72,16 @@ var SubjectFixtures = SubjectMap{
|
|||
DeletedAt: nil,
|
||||
},
|
||||
"jane-doe": Subject{
|
||||
SubjectUID: "jqy1y111h1njaaab",
|
||||
SubjectSlug: "jane-doe",
|
||||
SubjectName: "Jane Doe",
|
||||
SubjectType: SubjectPerson,
|
||||
SubjectSrc: SrcMarker,
|
||||
Favorite: false,
|
||||
Private: false,
|
||||
Excluded: false,
|
||||
SubjectBio: "",
|
||||
SubjectNotes: "",
|
||||
SubjUID: "jqy1y111h1njaaab",
|
||||
SubjSlug: "jane-doe",
|
||||
SubjName: "Jane Doe",
|
||||
SubjType: SubjPerson,
|
||||
SubjSrc: SrcMarker,
|
||||
SubjFavorite: false,
|
||||
SubjPrivate: false,
|
||||
SubjExcluded: false,
|
||||
SubjBio: "",
|
||||
SubjNotes: "",
|
||||
MetadataJSON: []byte(""),
|
||||
FileCount: 3,
|
||||
CreatedAt: TimeStamp(),
|
||||
|
@ -89,28 +89,28 @@ var SubjectFixtures = SubjectMap{
|
|||
DeletedAt: nil,
|
||||
},
|
||||
"actress-1": Subject{
|
||||
SubjectUID: "jqy1y111h1njaaac",
|
||||
SubjectSlug: "actress-a",
|
||||
SubjectName: "Actress A",
|
||||
SubjectType: SubjectPerson,
|
||||
SubjectSrc: SrcMarker,
|
||||
Favorite: false,
|
||||
Private: false,
|
||||
SubjectNotes: "",
|
||||
SubjUID: "jqy1y111h1njaaac",
|
||||
SubjSlug: "actress-a",
|
||||
SubjName: "Actress A",
|
||||
SubjType: SubjPerson,
|
||||
SubjSrc: SrcMarker,
|
||||
SubjFavorite: false,
|
||||
SubjPrivate: false,
|
||||
SubjNotes: "",
|
||||
MetadataJSON: []byte(""),
|
||||
CreatedAt: TimeStamp(),
|
||||
UpdatedAt: TimeStamp(),
|
||||
DeletedAt: nil,
|
||||
},
|
||||
"actor-1": Subject{
|
||||
SubjectUID: "jqy1y111h1njaaad",
|
||||
SubjectSlug: "actor-a",
|
||||
SubjectName: "Actor A",
|
||||
SubjectType: SubjectPerson,
|
||||
SubjectSrc: SrcMarker,
|
||||
Favorite: false,
|
||||
Private: false,
|
||||
SubjectNotes: "",
|
||||
SubjUID: "jqy1y111h1njaaad",
|
||||
SubjSlug: "actor-a",
|
||||
SubjName: "Actor A",
|
||||
SubjType: SubjPerson,
|
||||
SubjSrc: SrcMarker,
|
||||
SubjFavorite: false,
|
||||
SubjPrivate: false,
|
||||
SubjNotes: "",
|
||||
MetadataJSON: []byte(""),
|
||||
CreatedAt: TimeStamp(),
|
||||
UpdatedAt: TimeStamp(),
|
||||
|
|
|
@ -9,12 +9,12 @@ import (
|
|||
func TestSubjectMap_Get(t *testing.T) {
|
||||
t.Run("get existing subject", func(t *testing.T) {
|
||||
r := SubjectFixtures.Get("joe-biden")
|
||||
assert.Equal(t, "Joe Biden", r.SubjectName)
|
||||
assert.Equal(t, "Joe Biden", r.SubjName)
|
||||
assert.IsType(t, Subject{}, r)
|
||||
})
|
||||
t.Run("get not existing subject", func(t *testing.T) {
|
||||
r := SubjectFixtures.Get("monstera")
|
||||
assert.Equal(t, "", r.SubjectName)
|
||||
assert.Equal(t, "", r.SubjName)
|
||||
assert.IsType(t, Subject{}, r)
|
||||
})
|
||||
}
|
||||
|
@ -22,12 +22,12 @@ func TestSubjectMap_Get(t *testing.T) {
|
|||
func TestSubjectMap_Pointer(t *testing.T) {
|
||||
t.Run("get existing subject", func(t *testing.T) {
|
||||
r := SubjectFixtures.Pointer("joe-biden")
|
||||
assert.Equal(t, "Joe Biden", r.SubjectName)
|
||||
assert.Equal(t, "Joe Biden", r.SubjName)
|
||||
assert.IsType(t, &Subject{}, r)
|
||||
})
|
||||
t.Run("get not existing subject", func(t *testing.T) {
|
||||
r := SubjectFixtures.Pointer("monstera")
|
||||
assert.Equal(t, "", r.SubjectName)
|
||||
assert.Equal(t, "", r.SubjName)
|
||||
assert.IsType(t, &Subject{}, r)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
SubjectPerson = "person"
|
||||
SubjPerson = "person"
|
||||
)
|
||||
|
||||
// People represents a list of people.
|
||||
|
@ -15,20 +15,20 @@ type People []Person
|
|||
|
||||
// Person represents a subject with type person.
|
||||
type Person struct {
|
||||
SubjectUID string `json:"UID"`
|
||||
SubjectName string `json:"Name"`
|
||||
SubjectAlias string `json:"Alias,omitempty"`
|
||||
Favorite bool `json:"Favorite,omitempty"`
|
||||
Thumb string `json:",omitempty"`
|
||||
SubjUID string `json:"UID"`
|
||||
SubjName string `json:"Name"`
|
||||
SubjAlias string `json:"Alias"`
|
||||
SubjFavorite bool `json:"Favorite"`
|
||||
Thumb string `json:"Thumb"`
|
||||
}
|
||||
|
||||
// NewPerson returns a new entity.
|
||||
func NewPerson(subj Subject) *Person {
|
||||
result := &Person{
|
||||
SubjectUID: subj.SubjectUID,
|
||||
SubjectName: subj.SubjectName,
|
||||
SubjectAlias: subj.SubjectAlias,
|
||||
Favorite: subj.Favorite,
|
||||
SubjUID: subj.SubjUID,
|
||||
SubjName: subj.SubjName,
|
||||
SubjAlias: subj.SubjAlias,
|
||||
SubjFavorite: subj.SubjFavorite,
|
||||
Thumb: subj.Thumb,
|
||||
}
|
||||
|
||||
|
@ -44,10 +44,10 @@ func (m *Person) MarshalJSON() ([]byte, error) {
|
|||
Favorite bool `json:",omitempty"`
|
||||
Thumb string `json:",omitempty"`
|
||||
}{
|
||||
UID: m.SubjectUID,
|
||||
Name: m.SubjectName,
|
||||
Keywords: txt.NameKeywords(m.SubjectName, m.SubjectAlias),
|
||||
Favorite: m.Favorite,
|
||||
UID: m.SubjUID,
|
||||
Name: m.SubjName,
|
||||
Keywords: txt.NameKeywords(m.SubjName, m.SubjAlias),
|
||||
Favorite: m.SubjFavorite,
|
||||
Thumb: m.Thumb,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -9,19 +9,19 @@ import (
|
|||
func TestNewPerson(t *testing.T) {
|
||||
t.Run("BillGates", func(t *testing.T) {
|
||||
subj := Subject{
|
||||
SubjectUID: "jqytw12v8jjeu3e6",
|
||||
SubjectName: "William Henry Gates III",
|
||||
SubjectAlias: "Windows Guru",
|
||||
Favorite: true,
|
||||
SubjUID: "jqytw12v8jjeu3e6",
|
||||
SubjName: "William Henry Gates III",
|
||||
SubjAlias: "Windows Guru",
|
||||
SubjFavorite: true,
|
||||
Thumb: "622c7287967f2800e873fbc55f0328973056ce1d",
|
||||
}
|
||||
|
||||
m := NewPerson(subj)
|
||||
|
||||
assert.Equal(t, "jqytw12v8jjeu3e6", m.SubjectUID)
|
||||
assert.Equal(t, "William Henry Gates III", m.SubjectName)
|
||||
assert.Equal(t, "Windows Guru", m.SubjectAlias)
|
||||
assert.Equal(t, true, m.Favorite)
|
||||
assert.Equal(t, "jqytw12v8jjeu3e6", m.SubjUID)
|
||||
assert.Equal(t, "William Henry Gates III", m.SubjName)
|
||||
assert.Equal(t, "Windows Guru", m.SubjAlias)
|
||||
assert.Equal(t, true, m.SubjFavorite)
|
||||
assert.Equal(t, "622c7287967f2800e873fbc55f0328973056ce1d", m.Thumb)
|
||||
|
||||
if j, err := m.MarshalJSON(); err != nil {
|
||||
|
|
|
@ -14,16 +14,16 @@ func TestSubject_TableName(t *testing.T) {
|
|||
|
||||
func TestNewSubject(t *testing.T) {
|
||||
t.Run("Jens_Mander", func(t *testing.T) {
|
||||
m := NewSubject("Jens Mander", SubjectPerson, SrcAuto)
|
||||
assert.Equal(t, "Jens Mander", m.SubjectName)
|
||||
assert.Equal(t, "jens-mander", m.SubjectSlug)
|
||||
assert.Equal(t, "person", m.SubjectType)
|
||||
m := NewSubject("Jens Mander", SubjPerson, SrcAuto)
|
||||
assert.Equal(t, "Jens Mander", m.SubjName)
|
||||
assert.Equal(t, "jens-mander", m.SubjSlug)
|
||||
assert.Equal(t, "person", m.SubjType)
|
||||
})
|
||||
t.Run("subject Type empty", func(t *testing.T) {
|
||||
m := NewSubject("Anna Mander", "", SrcAuto)
|
||||
assert.Equal(t, "Anna Mander", m.SubjectName)
|
||||
assert.Equal(t, "anna-mander", m.SubjectSlug)
|
||||
assert.Equal(t, "person", m.SubjectType)
|
||||
assert.Equal(t, "Anna Mander", m.SubjName)
|
||||
assert.Equal(t, "anna-mander", m.SubjSlug)
|
||||
assert.Equal(t, "person", m.SubjType)
|
||||
})
|
||||
t.Run("subject name empty", func(t *testing.T) {
|
||||
m := NewSubject("", "", SrcAuto)
|
||||
|
@ -33,44 +33,44 @@ func TestNewSubject(t *testing.T) {
|
|||
|
||||
func TestSubject_SetName(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.SubjectSlug)
|
||||
assert.Equal(t, "Jens Mander", m.SubjName)
|
||||
assert.Equal(t, "jens-mander", m.SubjSlug)
|
||||
|
||||
if err := m.SetName("Foo McBar"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Foo McBar", m.SubjectName)
|
||||
assert.Equal(t, "foo-mcbar", m.SubjectSlug)
|
||||
assert.Equal(t, "Foo McBar", m.SubjName)
|
||||
assert.Equal(t, "foo-mcbar", m.SubjSlug)
|
||||
})
|
||||
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.SubjectSlug)
|
||||
assert.Equal(t, "Jens Mander", m.SubjName)
|
||||
assert.Equal(t, "jens-mander", m.SubjSlug)
|
||||
|
||||
err := m.SetName("")
|
||||
if err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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) {
|
||||
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)
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
|
||||
assert.Equal(t, "Create Me", m.SubjectName)
|
||||
assert.Equal(t, "create-me", m.SubjectSlug)
|
||||
assert.Equal(t, "Create Me", m.SubjName)
|
||||
assert.Equal(t, "create-me", m.SubjSlug)
|
||||
})
|
||||
t.Run("existing person", func(t *testing.T) {
|
||||
m := SubjectFixtures.Pointer("john-doe")
|
||||
|
@ -80,15 +80,15 @@ func TestFirstOrCreatePerson(t *testing.T) {
|
|||
t.Fatal("result should not be nil")
|
||||
}
|
||||
|
||||
assert.Equal(t, "John Doe", m.SubjectName)
|
||||
assert.Equal(t, "john-doe", m.SubjectSlug)
|
||||
assert.Equal(t, "Short Note", m.SubjectNotes)
|
||||
assert.Equal(t, "John Doe", m.SubjName)
|
||||
assert.Equal(t, "john-doe", m.SubjSlug)
|
||||
assert.Equal(t, "Short Note", m.SubjNotes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSubject_Save(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
|
||||
err := m.Save()
|
||||
|
||||
|
@ -105,13 +105,13 @@ func TestSubject_Save(t *testing.T) {
|
|||
|
||||
func TestSubject_Delete(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()
|
||||
assert.False(t, m.Deleted())
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ func TestSubject_Delete(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
|
||||
|
@ -134,7 +134,7 @@ func TestSubject_Restore(t *testing.T) {
|
|||
t.Run("success", func(t *testing.T) {
|
||||
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()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -148,7 +148,7 @@ func TestSubject_Restore(t *testing.T) {
|
|||
assert.False(t, m.Deleted())
|
||||
})
|
||||
t.Run("subject not deleted", func(t *testing.T) {
|
||||
m := &Subject{DeletedAt: nil, SubjectName: "NotDeleted1234"}
|
||||
m := &Subject{DeletedAt: nil, SubjName: "NotDeleted1234"}
|
||||
err := m.Restore()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -159,18 +159,18 @@ func TestSubject_Restore(t *testing.T) {
|
|||
|
||||
func TestFindSubject(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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if s := FindSubject(m.SubjectName); s != nil {
|
||||
if s := FindSubject(m.SubjName); s != nil {
|
||||
t.Fatal("result must be nil")
|
||||
}
|
||||
|
||||
if s := FindSubject(m.SubjectUID); s != nil {
|
||||
assert.Equal(t, "Find Me", s.SubjectName)
|
||||
if s := FindSubject(m.SubjUID); s != nil {
|
||||
assert.Equal(t, "Find Me", s.SubjName)
|
||||
} else {
|
||||
t.Fatal("result must not be nil")
|
||||
}
|
||||
|
@ -195,33 +195,33 @@ func TestSubject_Links(t *testing.T) {
|
|||
|
||||
func TestSubject_Update(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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := m.Update("SubjectName", "Updated Name"); err != nil {
|
||||
if err := m.Update("SubjName", "Updated Name"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.Equal(t, "Updated Name", m.SubjectName)
|
||||
assert.Equal(t, "Updated Name", m.SubjName)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
func TestSubject_Updates(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 {
|
||||
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)
|
||||
} else {
|
||||
assert.Equal(t, "UpdatedName", m.SubjectName)
|
||||
assert.Equal(t, "UpdatedType", m.SubjectType)
|
||||
assert.Equal(t, "UpdatedName", m.SubjName)
|
||||
assert.Equal(t, "UpdatedType", m.SubjType)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -229,45 +229,45 @@ func TestSubject_Updates(t *testing.T) {
|
|||
|
||||
func TestSubject_UpdateName(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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Test Person", m.SubjectName)
|
||||
assert.Equal(t, "test-person", m.SubjectSlug)
|
||||
assert.Equal(t, "Test Person", m.SubjName)
|
||||
assert.Equal(t, "test-person", m.SubjSlug)
|
||||
|
||||
if s, err := m.UpdateName("New New"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if s == nil {
|
||||
t.Fatal("subject is nil")
|
||||
} else {
|
||||
assert.Equal(t, "New New", m.SubjectName)
|
||||
assert.Equal(t, "new-new", m.SubjectSlug)
|
||||
assert.Equal(t, "New New", s.SubjectName)
|
||||
assert.Equal(t, "new-new", s.SubjectSlug)
|
||||
assert.Equal(t, "New New", m.SubjName)
|
||||
assert.Equal(t, "new-new", m.SubjSlug)
|
||||
assert.Equal(t, "New New", s.SubjName)
|
||||
assert.Equal(t, "new-new", s.SubjSlug)
|
||||
}
|
||||
})
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "Test Person2", m.SubjectName)
|
||||
assert.Equal(t, "test-person2", m.SubjectSlug)
|
||||
assert.Equal(t, "Test Person2", m.SubjName)
|
||||
assert.Equal(t, "test-person2", m.SubjSlug)
|
||||
|
||||
if s, err := m.UpdateName(""); err == nil {
|
||||
t.Error("error expected")
|
||||
} else if s == nil {
|
||||
t.Fatal("subject is nil")
|
||||
} else {
|
||||
assert.Equal(t, "Test Person2", m.SubjectName)
|
||||
assert.Equal(t, "test-person2", m.SubjectSlug)
|
||||
assert.Equal(t, "Test Person2", s.SubjectName)
|
||||
assert.Equal(t, "test-person2", s.SubjectSlug)
|
||||
assert.Equal(t, "Test Person2", m.SubjName)
|
||||
assert.Equal(t, "test-person2", m.SubjSlug)
|
||||
assert.Equal(t, "Test Person2", s.SubjName)
|
||||
assert.Equal(t, "test-person2", s.SubjSlug)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ import "github.com/ulule/deepcopier"
|
|||
|
||||
// Marker represents an image marker edit form.
|
||||
type Marker struct {
|
||||
SubjectSrc string `json:"SubjectSrc"`
|
||||
SubjSrc string `json:"SubjSrc"`
|
||||
MarkerName string `json:"Name"`
|
||||
Review bool `json:"Review"`
|
||||
MarkerReview bool `json:"MarkerReview"`
|
||||
MarkerInvalid bool `json:"Invalid"`
|
||||
}
|
||||
|
||||
|
|
|
@ -9,14 +9,14 @@ import (
|
|||
func TestNewMarker(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
var m = struct {
|
||||
SubjectSrc string
|
||||
SubjSrc string
|
||||
MarkerName string
|
||||
Review bool
|
||||
MarkerReview bool
|
||||
MarkerInvalid bool
|
||||
}{
|
||||
SubjectSrc: "manual",
|
||||
SubjSrc: "manual",
|
||||
MarkerName: "Foo",
|
||||
Review: true,
|
||||
MarkerReview: true,
|
||||
MarkerInvalid: true,
|
||||
}
|
||||
|
||||
|
@ -26,9 +26,9 @@ func TestNewMarker(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "manual", f.SubjectSrc)
|
||||
assert.Equal(t, "manual", f.SubjSrc)
|
||||
assert.Equal(t, "Foo", f.MarkerName)
|
||||
assert.Equal(t, true, f.Review)
|
||||
assert.Equal(t, true, f.MarkerReview)
|
||||
assert.Equal(t, true, f.MarkerInvalid)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,11 +3,12 @@ package form
|
|||
import "strings"
|
||||
|
||||
type Selection struct {
|
||||
Files []string `json:"files"`
|
||||
Photos []string `json:"photos"`
|
||||
Albums []string `json:"albums"`
|
||||
Labels []string `json:"labels"`
|
||||
Places []string `json:"places"`
|
||||
Files []string `json:"files"`
|
||||
Photos []string `json:"photos"`
|
||||
Albums []string `json:"albums"`
|
||||
Labels []string `json:"labels"`
|
||||
Places []string `json:"places"`
|
||||
Subjects []string `json:"subjects"`
|
||||
}
|
||||
|
||||
func (f Selection) Empty() bool {
|
||||
|
@ -22,6 +23,8 @@ func (f Selection) Empty() bool {
|
|||
return false
|
||||
case len(f.Places) > 0:
|
||||
return false
|
||||
case len(f.Subjects) > 0:
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
@ -36,6 +39,7 @@ func (f Selection) All() []string {
|
|||
all = append(all, f.Albums...)
|
||||
all = append(all, f.Labels...)
|
||||
all = append(all, f.Places...)
|
||||
all = append(all, f.Subjects...)
|
||||
|
||||
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"}}
|
||||
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) {
|
||||
sel := Selection{Photos: []string{}, Albums: []string{}, Labels: []string{}}
|
||||
assert.Equal(t, true, sel.Empty())
|
||||
|
@ -35,8 +40,8 @@ func TestSelection_Empty(t *testing.T) {
|
|||
|
||||
func TestSelection_All(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"}}
|
||||
assert.Equal(t, []string{"p123", "p456", "a123", "l123", "l456", "l789", "p568"}, sel.All())
|
||||
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", "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
|
||||
MsgLabelsDeleted
|
||||
MsgLabelSaved
|
||||
MsgSubjectSaved
|
||||
MsgSubjectDeleted
|
||||
MsgFilesUploadedIn
|
||||
MsgSelectionApproved
|
||||
MsgSelectionArchived
|
||||
|
@ -132,6 +134,8 @@ var Messages = MessageMap{
|
|||
MsgCopyingFilesFrom: gettext("Copying files from %s"),
|
||||
MsgLabelsDeleted: gettext("Labels deleted"),
|
||||
MsgLabelSaved: gettext("Label saved"),
|
||||
MsgSubjectSaved: gettext("Subject saved"),
|
||||
MsgSubjectDeleted: gettext("Subject deleted"),
|
||||
MsgFilesUploadedIn: gettext("%d files uploaded in %d s"),
|
||||
MsgSelectionApproved: gettext("Selection approved"),
|
||||
MsgSelectionArchived: gettext("Selection archived"),
|
||||
|
|
|
@ -65,7 +65,7 @@ func (w *Faces) Audit(fix bool) (err error) {
|
|||
|
||||
for _, f2 := range faces {
|
||||
if matched, dist := f1.Match(entity.Embeddings{f2.Embedding()}); matched {
|
||||
if f1.SubjectUID == f2.SubjectUID {
|
||||
if f1.SubjUID == f2.SubjUID {
|
||||
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)
|
||||
|
||||
if f1.SubjectUID != "" {
|
||||
log.Infof("face %s: subject %s (%s %s)", f1.ID, txt.Quote(subj[f1.SubjectUID].SubjectName), f1.SubjectUID, entity.SrcString(f1.FaceSrc))
|
||||
if f1.SubjUID != "" {
|
||||
log.Infof("face %s: subject %s (%s %s)", f1.ID, txt.Quote(subj[f1.SubjUID].SubjName), f1.SubjUID, entity.SrcString(f1.FaceSrc))
|
||||
} else {
|
||||
log.Infof("face %s: no subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc))
|
||||
}
|
||||
|
||||
if f2.SubjectUID != "" {
|
||||
log.Infof("face %s: subject %s (%s %s)", f2.ID, txt.Quote(subj[f2.SubjectUID].SubjectName), f2.SubjectUID, entity.SrcString(f2.FaceSrc))
|
||||
if f2.SubjUID != "" {
|
||||
log.Infof("face %s: subject %s (%s %s)", f2.ID, txt.Quote(subj[f2.SubjUID].SubjName), f2.SubjUID, entity.SrcString(f2.FaceSrc))
|
||||
} else {
|
||||
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)
|
||||
} else {
|
||||
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++
|
||||
}
|
||||
|
||||
if marker.SubjectUID != "" {
|
||||
if marker.SubjUID != "" {
|
||||
result.Recognized++
|
||||
} else {
|
||||
result.Unknown++
|
||||
|
|
|
@ -37,7 +37,7 @@ func (w *Faces) Optimize() (result FacesOptimizeResult, err error) {
|
|||
for j := 0; j <= n; j++ {
|
||||
if len(merge) == 0 {
|
||||
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 {
|
||||
// Nothing to merge.
|
||||
} else if _, err := query.MergeFaces(merge); err != nil {
|
||||
|
@ -48,7 +48,7 @@ func (w *Faces) Optimize() (result FacesOptimizeResult, err error) {
|
|||
|
||||
merge = nil
|
||||
} 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])
|
||||
} else if len(merge) == 1 {
|
||||
merge = nil
|
||||
|
|
|
@ -69,7 +69,7 @@ func (w *Faces) Stats() (err error) {
|
|||
min := -1.0
|
||||
max := -1.0
|
||||
|
||||
if k, ok := dist[f1.SubjectUID]; ok {
|
||||
if k, ok := dist[f1.SubjUID]; ok {
|
||||
min = k[0]
|
||||
max = k[1]
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ func (w *Faces) Stats() (err error) {
|
|||
|
||||
f2 := faces[j]
|
||||
|
||||
if f1.SubjectUID != f2.SubjectUID {
|
||||
if f1.SubjUID != f2.SubjUID {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ func (w *Faces) Stats() (err error) {
|
|||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ func Faces(knownOnly, unmatched bool) (result entity.Faces, err error) {
|
|||
func ManuallyAddedFaces() (result entity.Faces, err error) {
|
||||
err = Db().
|
||||
Where("face_src = ?", entity.SrcManual).
|
||||
Where("subject_uid <> ''").Order("subject_uid, samples DESC").
|
||||
Where("subj_uid <> ''").Order("subj_uid, samples DESC").
|
||||
Find(&result).Error
|
||||
|
||||
return result, err
|
||||
|
@ -48,9 +48,9 @@ func MatchFaceMarkers() (affected int64, err error) {
|
|||
for _, f := range faces {
|
||||
if res := Db().Model(&entity.Marker{}).
|
||||
Where("face_id = ?", f.ID).
|
||||
Where("subject_src = ?", entity.SrcAuto).
|
||||
Where("subject_uid <> ?", f.SubjectUID).
|
||||
Updates(entity.Values{"SubjectUID": f.SubjectUID, "Review": false}); res.Error != nil {
|
||||
Where("subj_src = ?", entity.SrcAuto).
|
||||
Where("subj_uid <> ?", f.SubjUID).
|
||||
Updates(entity.Values{"SubjUID": f.SubjUID, "MarkerReview": false}); res.Error != nil {
|
||||
return affected, err
|
||||
} else if res.RowsAffected > 0 {
|
||||
affected += res.RowsAffected
|
||||
|
@ -64,7 +64,7 @@ func MatchFaceMarkers() (affected int64, err error) {
|
|||
func RemoveAnonymousFaceClusters() (removed int64, err error) {
|
||||
res := UnscopedDb().Delete(
|
||||
entity.Face{},
|
||||
"face_src = ? AND subject_uid = ''", entity.SrcAuto)
|
||||
"face_src = ? AND subj_uid = ''", entity.SrcAuto)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
subjectUID := merge[0].SubjectUID
|
||||
subjUID := merge[0].SubjUID
|
||||
|
||||
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",
|
||||
txt.Quote(subjectUID), txt.Quote(merge[i].SubjectUID))
|
||||
txt.Quote(subjUID), txt.Quote(merge[i].SubjUID))
|
||||
}
|
||||
}
|
||||
|
||||
// Find or create merged face cluster.
|
||||
if merged = entity.NewFace(merge[0].SubjectUID, merge[0].FaceSrc, merge.Embeddings()); merged == nil {
|
||||
return merged, fmt.Errorf("faces: new cluster is nil for subject %s", txt.Quote(subjectUID))
|
||||
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(subjUID))
|
||||
} 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 {
|
||||
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 {
|
||||
return merged, err
|
||||
} 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 {
|
||||
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
|
||||
|
@ -172,7 +172,7 @@ func ResolveFaceCollisions() (conflicts, resolved int, err error) {
|
|||
for _, f1 := range faces {
|
||||
for _, f2 := range faces {
|
||||
if matched, dist := f1.Match(entity.Embeddings{f2.Embedding()}); matched {
|
||||
if f1.SubjectUID == f2.SubjectUID {
|
||||
if f1.SubjUID == f2.SubjUID {
|
||||
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)
|
||||
|
||||
if f1.SubjectUID != "" {
|
||||
log.Debugf("face %s: subject %s (%s %s)", f1.ID, txt.Quote(f1.SubjectUID), f1.SubjectUID, entity.SrcString(f1.FaceSrc))
|
||||
if f1.SubjUID != "" {
|
||||
log.Debugf("face %s: subject %s (%s %s)", f1.ID, txt.Quote(f1.SubjUID), f1.SubjUID, entity.SrcString(f1.FaceSrc))
|
||||
} else {
|
||||
log.Debugf("face %s: no subject (%s)", f1.ID, entity.SrcString(f1.FaceSrc))
|
||||
}
|
||||
|
||||
if f2.SubjectUID != "" {
|
||||
log.Debugf("face %s: subject %s (%s %s)", f2.ID, txt.Quote(f2.SubjectUID), f2.SubjectUID, entity.SrcString(f2.FaceSrc))
|
||||
if f2.SubjUID != "" {
|
||||
log.Debugf("face %s: subject %s (%s %s)", f2.ID, txt.Quote(f2.SubjUID), f2.SubjUID, entity.SrcString(f2.FaceSrc))
|
||||
} else {
|
||||
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 {
|
||||
t.Fatal("marker is nil")
|
||||
} else {
|
||||
assert.Empty(t, m.SubjectUID)
|
||||
assert.Empty(t, m.SubjUID)
|
||||
}
|
||||
|
||||
// Reset subject_uid.
|
||||
// Reset subj_uid.
|
||||
if err := Db().Model(&entity.Marker{}).
|
||||
Where("subject_src = ?", entity.SrcAuto).
|
||||
Where("subject_uid = ?", "jqu0xs11qekk9jx8").
|
||||
Updates(entity.Values{"SubjectUID": ""}).Error; err != nil {
|
||||
Where("subj_src = ?", entity.SrcAuto).
|
||||
Where("subj_uid = ?", "jqu0xs11qekk9jx8").
|
||||
Updates(entity.Values{"SubjUID": ""}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -86,7 +86,7 @@ func TestMatchFaceMarkers(t *testing.T) {
|
|||
} else if m == nil {
|
||||
t.Fatal("marker is nil")
|
||||
} 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, 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, 0.03948165743305488, result.SampleRadius)
|
||||
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.
|
||||
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)
|
||||
log.Debugf("search: subject %s", txt.Quote(strings.Join(names, ", ")))
|
||||
f.Query = remaining
|
||||
|
@ -115,12 +115,12 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
|||
// Filter for one or more subjects?
|
||||
if f.Subject != "" {
|
||||
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))
|
||||
}
|
||||
} else if f.Subjects != "" {
|
||||
for _, where := range LikeAnyWord("s.subject_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 (?))",
|
||||
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.subj_uid = m.subj_uid 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 {
|
||||
db = db.Where("subject_uid <> ''")
|
||||
db = db.Where("subj_uid <> ''")
|
||||
}
|
||||
|
||||
if !matchedBefore.IsZero() {
|
||||
|
@ -119,8 +119,8 @@ func Embeddings(single, unclustered bool, size, score int) (result entity.Embedd
|
|||
func RemoveInvalidMarkerReferences() (removed int64, err error) {
|
||||
res := Db().
|
||||
Model(&entity.Marker{}).
|
||||
Where("marker_invalid = 1 AND (subject_uid <> '' OR face_id <> '')").
|
||||
UpdateColumns(entity.Values{"subject_uid": "", "face_id": "", "face_dist": -1.0, "matched_at": nil})
|
||||
Where("marker_invalid = 1 AND (subj_uid <> '' OR face_id <> '')").
|
||||
UpdateColumns(entity.Values{"subj_uid": "", "face_id": "", "face_dist": -1.0, "matched_at": nil})
|
||||
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
@ -141,8 +141,8 @@ func RemoveNonExistentMarkerFaces() (removed int64, err error) {
|
|||
func RemoveNonExistentMarkerSubjects() (removed int64, err error) {
|
||||
res := Db().
|
||||
Model(&entity.Marker{}).
|
||||
Where(fmt.Sprintf("subject_uid <> '' AND subject_uid NOT IN (SELECT subject_uid FROM %s)", entity.Subject{}.TableName())).
|
||||
UpdateColumns(entity.Values{"subject_uid": "", "matched_at": nil})
|
||||
Where(fmt.Sprintf("subj_uid <> '' AND subj_uid NOT IN (SELECT subj_uid FROM %s)", entity.Subject{}.TableName())).
|
||||
UpdateColumns(entity.Values{"subj_uid": "", "matched_at": nil})
|
||||
|
||||
return res.RowsAffected, res.Error
|
||||
}
|
||||
|
@ -182,7 +182,7 @@ func MarkersWithNonExistentReferences() (faces entity.Markers, subjects entity.M
|
|||
|
||||
// Find markers with invalid subject UIDs.
|
||||
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 {
|
||||
err = res.Error
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ func MarkersWithNonExistentReferences() (faces entity.Markers, subjects entity.M
|
|||
// MarkersWithSubjectConflict finds markers with conflicting subjects.
|
||||
func MarkersWithSubjectConflict() (results entity.Markers, err error) {
|
||||
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").
|
||||
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.
|
||||
func ResetFaceMarkerMatches() (removed int64, err error) {
|
||||
res := Db().Model(&entity.Marker{}).
|
||||
Where("subject_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})
|
||||
Where("subj_src = ? AND marker_type = ?", entity.SrcAuto, entity.MarkerFace).
|
||||
UpdateColumns(entity.Values{"marker_name": "", "subj_uid": "", "subj_src": "", "face_id": "", "face_dist": -1.0, "matched_at": nil})
|
||||
|
||||
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.
|
||||
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)
|
||||
log.Debugf("people: searching for %s", txt.Quote(txt.JoinNames(names)))
|
||||
f.Query = remaining
|
||||
|
@ -222,12 +222,12 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
|||
// Filter for one or more subjects?
|
||||
if f.Subject != "" {
|
||||
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))
|
||||
}
|
||||
} else if f.Subjects != "" {
|
||||
for _, where := range LikeAnyWord("s.subject_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 (?))",
|
||||
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.subj_uid = m.subj_uid WHERE (?))",
|
||||
entity.Marker{}.TableName(), entity.Subject{}.TableName()), gorm.Expr(where))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ func UpdateSubjectPreviews() (err error) {
|
|||
return Db().Table(entity.Subject{}.TableName()).
|
||||
UpdateColumn("thumb", gorm.Expr("(SELECT f.file_hash FROM files f "+
|
||||
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.Subject{}.TableName())+
|
||||
` JOIN photos p ON f.photo_id = p.id
|
||||
|
@ -172,7 +172,7 @@ func UpdateSubjectPreviews() (err error) {
|
|||
err = Db().Table(entity.Subject{}.TableName()).
|
||||
UpdateColumn("thumb", gorm.Expr("(SELECT m.file_hash FROM "+
|
||||
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.Subject{}.TableName())+
|
||||
` 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 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.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 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").
|
||||
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 {
|
||||
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 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 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 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").
|
||||
Select("files.*").
|
||||
Joins("JOIN photos ON photos.id = files.photo_id").
|
||||
Where("photos.deleted_at IS NULL").
|
||||
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")
|
||||
|
||||
if result := s.Scan(&results); result.Error != nil {
|
||||
|
|
|
@ -13,16 +13,16 @@ import (
|
|||
|
||||
// SubjectResult represents a subject search result.
|
||||
type SubjectResult struct {
|
||||
SubjectUID string `json:"UID"`
|
||||
SubjectType string `json:"Type"`
|
||||
SubjectSlug string `json:"Slug"`
|
||||
SubjectName string `json:"Name"`
|
||||
SubjectAlias string `json:"Alias,omitempty"`
|
||||
Thumb string `json:"Thumb,omitempty"`
|
||||
Favorite bool `json:"Favorite,omitempty"`
|
||||
Private bool `json:"Private,omitempty"`
|
||||
Excluded bool `json:"Excluded,omitempty"`
|
||||
FileCount int `json:"Files,omitempty"`
|
||||
SubjUID string `json:"UID"`
|
||||
SubjType string `json:"Type"`
|
||||
SubjSlug string `json:"Slug"`
|
||||
SubjName string `json:"Name"`
|
||||
SubjAlias string `json:"Alias"`
|
||||
SubjFavorite bool `json:"Favorite"`
|
||||
SubjPrivate bool `json:"Private"`
|
||||
SubjExcluded bool `json:"Excluded"`
|
||||
FileCount int `json:"Files"`
|
||||
Thumb string `json:"Thumb"`
|
||||
}
|
||||
|
||||
// SubjectResults represents subject search results.
|
||||
|
@ -38,7 +38,7 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
|
|||
|
||||
// Base query.
|
||||
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.
|
||||
if f.Count > 0 && f.Count <= MaxResults {
|
||||
|
@ -49,14 +49,20 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
|
|||
|
||||
// Set sort order.
|
||||
switch f.Order {
|
||||
case "name":
|
||||
s = s.Order("subj_name")
|
||||
case "count":
|
||||
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:
|
||||
s = s.Order("subject_name")
|
||||
s = s.Order("subj_favorite DESC, subj_name")
|
||||
}
|
||||
|
||||
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 {
|
||||
return results, result.Error
|
||||
|
@ -66,27 +72,28 @@ func SubjectSearch(f form.SubjectSearch) (results SubjectResults, err error) {
|
|||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
s = s.Where("favorite = 1")
|
||||
s = s.Where("subj_favorite = 1")
|
||||
}
|
||||
|
||||
if f.Private {
|
||||
s = s.Where("private = 1")
|
||||
s = s.Where("subj_private = 1")
|
||||
}
|
||||
|
||||
if f.Excluded {
|
||||
s = s.Where("excluded = 1")
|
||||
s = s.Where("subj_excluded = 1")
|
||||
}
|
||||
|
||||
// Omit deleted rows.
|
||||
s = s.Where("deleted_at IS NULL")
|
||||
|
||||
if result := s.Scan(&results); result.Error != nil {
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
func TestSubjectSearch(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.LessOrEqual(t, 3, len(results))
|
||||
})
|
||||
|
|
|
@ -14,9 +14,9 @@ import (
|
|||
func People() (people entity.People, err error) {
|
||||
err = UnscopedDb().
|
||||
Table(entity.Subject{}.TableName()).
|
||||
Select("subject_uid, subject_name, subject_alias, favorite ").
|
||||
Where("deleted_at IS NULL AND subject_type = ?", entity.SubjectPerson).
|
||||
Order("favorite, subject_name").
|
||||
Select("subj_uid, subj_name, subj_alias, subj_favorite ").
|
||||
Where("deleted_at IS NULL AND subj_type = ?", entity.SubjPerson).
|
||||
Order("subj_favorite, subj_name").
|
||||
Limit(2000).Offset(0).
|
||||
Scan(&people).Error
|
||||
|
||||
|
@ -28,7 +28,7 @@ func PeopleCount() (count int, err error) {
|
|||
err = Db().
|
||||
Table(entity.Subject{}.TableName()).
|
||||
Where("deleted_at IS NULL").
|
||||
Where("subject_type = ?", entity.SubjectPerson).
|
||||
Where("subj_type = ?", entity.SubjPerson).
|
||||
Count(&count).Error
|
||||
|
||||
return count, err
|
||||
|
@ -38,7 +38,7 @@ func PeopleCount() (count int, err error) {
|
|||
func Subjects(limit, offset int) (result entity.Subjects, err error) {
|
||||
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
|
||||
|
||||
return result, err
|
||||
|
@ -57,7 +57,7 @@ func SubjectMap() (result map[string]entity.Subject, err error) {
|
|||
}
|
||||
|
||||
for _, s := range subj {
|
||||
result[s.SubjectUID] = s
|
||||
result[s.SubjUID] = s
|
||||
}
|
||||
|
||||
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.
|
||||
func RemoveDanglingMarkerSubjects() (removed int64, err error) {
|
||||
res := UnscopedDb().
|
||||
Where("subject_src = ?", entity.SrcMarker).
|
||||
Where(fmt.Sprintf("subject_uid NOT IN (SELECT subject_uid FROM %s)", entity.Face{}.TableName())).
|
||||
Where(fmt.Sprintf("subject_uid NOT IN (SELECT subject_uid FROM %s)", entity.Marker{}.TableName())).
|
||||
Where("subj_src = ?", entity.SrcMarker).
|
||||
Where(fmt.Sprintf("subj_uid NOT IN (SELECT subj_uid FROM %s)", entity.Face{}.TableName())).
|
||||
Where(fmt.Sprintf("subj_uid NOT IN (SELECT subj_uid FROM %s)", entity.Marker{}.TableName())).
|
||||
Delete(&entity.Subject{})
|
||||
|
||||
return res.RowsAffected, res.Error
|
||||
|
@ -79,7 +79,7 @@ func CreateMarkerSubjects() (affected int64, err error) {
|
|||
var markers entity.Markers
|
||||
|
||||
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).
|
||||
Order("marker_name").
|
||||
Find(&markers).Error; err != nil {
|
||||
|
@ -94,7 +94,7 @@ func CreateMarkerSubjects() (affected int64, err error) {
|
|||
for _, m := range markers {
|
||||
if name == m.MarkerName && subj != nil {
|
||||
// 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?")
|
||||
continue
|
||||
} else if subj = entity.FirstOrCreateSubject(subj); subj == nil {
|
||||
|
@ -106,13 +106,13 @@ func CreateMarkerSubjects() (affected int64, err error) {
|
|||
|
||||
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
|
||||
}
|
||||
|
||||
if m.FaceID == "" {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -120,21 +120,21 @@ func CreateMarkerSubjects() (affected int64, err error) {
|
|||
return affected, err
|
||||
}
|
||||
|
||||
// SearchSubjectUIDs finds subject UIDs matching the search string, and removes names from the remaining query.
|
||||
func SearchSubjectUIDs(s string) (result []string, names []string, remaining string) {
|
||||
// SearchSubjUIDs finds subject UIDs matching the search string, and removes names from the remaining query.
|
||||
func SearchSubjUIDs(s string) (result []string, names []string, remaining string) {
|
||||
if s == "" {
|
||||
return result, names, s
|
||||
}
|
||||
|
||||
type Matches struct {
|
||||
SubjectUID string
|
||||
SubjectName string
|
||||
SubjectAlias string
|
||||
SubjUID string
|
||||
SubjName string
|
||||
SubjAlias string
|
||||
}
|
||||
|
||||
var matches []Matches
|
||||
|
||||
wheres := LikeAllNames(Cols{"subject_name", "subject_alias"}, s)
|
||||
wheres := LikeAllNames(Cols{"subj_name", "subj_alias"}, s)
|
||||
|
||||
if len(wheres) == 0 {
|
||||
return result, names, s
|
||||
|
@ -155,16 +155,16 @@ func SearchSubjectUIDs(s string) (result []string, names []string, remaining str
|
|||
}
|
||||
|
||||
for _, m := range matches {
|
||||
subj = append(subj, m.SubjectUID)
|
||||
names = append(names, m.SubjectName)
|
||||
subj = append(subj, m.SubjUID)
|
||||
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 {
|
||||
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 {
|
||||
remaining = strings.ReplaceAll(remaining, r, "")
|
||||
}
|
||||
|
|
|
@ -72,9 +72,9 @@ func TestCreateMarkerSubjects(t *testing.T) {
|
|||
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) {
|
||||
result, names, remaining := SearchSubjectUIDs("john & his | cats")
|
||||
result, names, remaining := SearchSubjUIDs("john & his | cats")
|
||||
|
||||
if len(result) != 1 {
|
||||
t.Fatal("expected one result")
|
||||
|
@ -85,11 +85,11 @@ func TestSearchSubjectUIDs(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("xxx", func(t *testing.T) {
|
||||
result, _, _ := SearchSubjectUIDs("xxx")
|
||||
result, _, _ := SearchSubjUIDs("xxx")
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
t.Run("empty string", func(t *testing.T) {
|
||||
result, _, _ := SearchSubjectUIDs("")
|
||||
result, _, _ := SearchSubjUIDs("")
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -103,6 +103,9 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||
|
||||
api.GetSubjects(v1)
|
||||
api.GetSubject(v1)
|
||||
api.UpdateSubject(v1)
|
||||
api.LikeSubject(v1)
|
||||
api.DislikeSubject(v1)
|
||||
|
||||
api.LabelCover(v1)
|
||||
api.GetLabels(v1)
|
||||
|
|
Loading…
Reference in a new issue