Indexing bug fixes and UX improvements
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
73c4891cde
commit
ca8a8466d4
27 changed files with 404 additions and 263 deletions
|
@ -7,7 +7,7 @@ ENV TF_CPP_MIN_LOG_LEVEL 2
|
||||||
COPY /docker/demo/index.tmpl /photoprism/assets/templates
|
COPY /docker/demo/index.tmpl /photoprism/assets/templates
|
||||||
|
|
||||||
# Download example photos
|
# Download example photos
|
||||||
RUN wget -qO- https://dl.photoprism.org/fixtures/demo.tar.gz | tar xvz -C /photoprism/import
|
RUN wget -qO- https://dl.photoprism.org/fixtures/demo.tar.gz | tar xvz -C /photoprism/originals
|
||||||
|
|
||||||
# Configure PhotoPrism
|
# Configure PhotoPrism
|
||||||
ENV PHOTOPRISM_STORAGE_PATH /photoprism/storage
|
ENV PHOTOPRISM_STORAGE_PATH /photoprism/storage
|
||||||
|
@ -27,9 +27,11 @@ ENV PHOTOPRISM_THUMB_SIZE 3840
|
||||||
ENV PHOTOPRISM_THUMB_LIMIT 3840
|
ENV PHOTOPRISM_THUMB_LIMIT 3840
|
||||||
ENV PHOTOPRISM_JPEG_QUALITY 95
|
ENV PHOTOPRISM_JPEG_QUALITY 95
|
||||||
ENV PHOTOPRISM_JPEG_HIDDEN false
|
ENV PHOTOPRISM_JPEG_HIDDEN false
|
||||||
|
ENV PHOTOPRISM_SITE_CAPTION "Try our demo"
|
||||||
|
|
||||||
# Import example photos
|
# Import example photos
|
||||||
RUN photoprism import
|
RUN photoprism index
|
||||||
|
RUN photoprism moments
|
||||||
|
|
||||||
# Start PhotoPrism server
|
# Start PhotoPrism server
|
||||||
CMD photoprism --public start
|
CMD photoprism --public start
|
||||||
|
|
|
@ -296,7 +296,7 @@
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
<v-list-tile to="/files" @click="" class="p-navigation-files" v-show="$config.feature('files')">
|
<v-list-tile to="/library/files" @click="" class="p-navigation-files" v-show="$config.feature('files')">
|
||||||
<v-list-tile-content>
|
<v-list-tile-content>
|
||||||
<v-list-tile-title>
|
<v-list-tile-title>
|
||||||
<translate key="Files">Files</translate>
|
<translate key="Files">Files</translate>
|
||||||
|
@ -304,6 +304,15 @@
|
||||||
</v-list-tile-title>
|
</v-list-tile-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
|
|
||||||
|
<v-list-tile to="/library/hidden" @click="" class="p-navigation-hidden">
|
||||||
|
<v-list-tile-content>
|
||||||
|
<v-list-tile-title>
|
||||||
|
<translate key="Hidden">Hidden</translate>
|
||||||
|
<span v-show="config.count.hidden > 0" class="p-navigation-count">{{ config.count.hidden }}</span>
|
||||||
|
</v-list-tile-title>
|
||||||
|
</v-list-tile-content>
|
||||||
|
</v-list-tile>
|
||||||
</v-list-group>
|
</v-list-group>
|
||||||
|
|
||||||
<v-list-tile to="/settings" @click="" class="p-navigation-settings" v-show="!config.disableSettings">
|
<v-list-tile to="/settings" @click="" class="p-navigation-settings" v-show="!config.disableSettings">
|
||||||
|
|
|
@ -16,7 +16,38 @@
|
||||||
|
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
<p class="subheading">
|
<p class="subheading">
|
||||||
<span v-if="total === 0">Select files to start upload...</span>
|
<v-combobox v-if="total === 0" flat solo hide-details chips deletable-chips
|
||||||
|
multiple color="secondary-dark" class="my-0"
|
||||||
|
v-model="selectedAlbums"
|
||||||
|
:items="albums"
|
||||||
|
item-text="Title"
|
||||||
|
item-value="UID"
|
||||||
|
:allow-overflow="false"
|
||||||
|
label="Select albums or create a new one"
|
||||||
|
return-object
|
||||||
|
>
|
||||||
|
<template v-slot:no-data>
|
||||||
|
<v-list-tile>
|
||||||
|
<v-list-tile-content>
|
||||||
|
<v-list-tile-title>
|
||||||
|
Press <kbd>enter</kbd> to create a new album.
|
||||||
|
</v-list-tile-title>
|
||||||
|
</v-list-tile-content>
|
||||||
|
</v-list-tile>
|
||||||
|
</template>
|
||||||
|
<template v-slot:selection="data">
|
||||||
|
<v-chip
|
||||||
|
:key="JSON.stringify(data.item)"
|
||||||
|
:selected="data.selected"
|
||||||
|
:disabled="data.disabled"
|
||||||
|
class="v-chip--select-multi"
|
||||||
|
@input="data.parent.selectItem(data.item)"
|
||||||
|
>
|
||||||
|
<v-icon class="pr-1">folder</v-icon>
|
||||||
|
{{ data.item.Title ? data.item.Title : data.item | truncate(40) }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-combobox>
|
||||||
<span v-else-if="failed">Upload failed</span>
|
<span v-else-if="failed">Upload failed</span>
|
||||||
<span v-else-if="total > 0 && completed < 100">
|
<span v-else-if="total > 0 && completed < 100">
|
||||||
Uploading {{current}} of {{total}}...
|
Uploading {{current}} of {{total}}...
|
||||||
|
@ -25,38 +56,6 @@
|
||||||
<span v-else-if="completed === 100">Done.</span>
|
<span v-else-if="completed === 100">Done.</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<v-combobox flat solo hide-details chips deletable-chips
|
|
||||||
multiple color="secondary-dark" class="my-3"
|
|
||||||
v-model="selectedAlbums"
|
|
||||||
:items="albums"
|
|
||||||
item-text="Title"
|
|
||||||
item-value="UID"
|
|
||||||
:allow-overflow="false"
|
|
||||||
label="Add to existing albums or create a new one."
|
|
||||||
return-object
|
|
||||||
>
|
|
||||||
<template v-slot:no-data>
|
|
||||||
<v-list-tile>
|
|
||||||
<v-list-tile-content>
|
|
||||||
<v-list-tile-title>
|
|
||||||
Press <kbd>enter</kbd> to create a new album.
|
|
||||||
</v-list-tile-title>
|
|
||||||
</v-list-tile-content>
|
|
||||||
</v-list-tile>
|
|
||||||
</template>
|
|
||||||
<template v-slot:selection="data">
|
|
||||||
<v-chip
|
|
||||||
:key="JSON.stringify(data.item)"
|
|
||||||
:selected="data.selected"
|
|
||||||
:disabled="data.disabled"
|
|
||||||
class="v-chip--select-multi"
|
|
||||||
@input="data.parent.selectItem(data.item)"
|
|
||||||
>
|
|
||||||
<v-icon class="pr-1">folder</v-icon>
|
|
||||||
{{ data.item.Title ? data.item.Title : data.item | truncate(40) }}
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
|
||||||
</v-combobox>
|
|
||||||
|
|
||||||
<v-progress-linear color="secondary-dark" v-model="completed"
|
<v-progress-linear color="secondary-dark" v-model="completed"
|
||||||
:indeterminate="indexing"></v-progress-linear>
|
:indeterminate="indexing"></v-progress-linear>
|
||||||
|
|
|
@ -369,6 +369,10 @@ export class Photo extends RestModel {
|
||||||
info.push(Util.duration(file.Duration));
|
info.push(Util.duration(file.Duration));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (file.Codec) {
|
||||||
|
info.push(file.Codec.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
this.addSizeInfo(file, info);
|
this.addSizeInfo(file, info);
|
||||||
|
|
||||||
if (!info) {
|
if (!info) {
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<v-toolbar flat color="secondary">
|
|
||||||
<v-toolbar-title>Events</v-toolbar-title>
|
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-toolbar>
|
|
||||||
|
|
||||||
<v-container>
|
|
||||||
<v-layout wrap>
|
|
||||||
<v-flex
|
|
||||||
xs12
|
|
||||||
class="mb-3"
|
|
||||||
>
|
|
||||||
<v-sheet height="500">
|
|
||||||
<v-calendar
|
|
||||||
ref="calendar"
|
|
||||||
v-model="start"
|
|
||||||
:type="type"
|
|
||||||
:end="end"
|
|
||||||
color="primary"
|
|
||||||
></v-calendar>
|
|
||||||
</v-sheet>
|
|
||||||
</v-flex>
|
|
||||||
|
|
||||||
<v-flex
|
|
||||||
sm4
|
|
||||||
xs12
|
|
||||||
class="text-sm-left text-xs-center"
|
|
||||||
>
|
|
||||||
<v-btn @click="$refs.calendar.prev()">
|
|
||||||
<v-icon
|
|
||||||
dark
|
|
||||||
left
|
|
||||||
>
|
|
||||||
keyboard_arrow_left
|
|
||||||
</v-icon>
|
|
||||||
<translate>Prev</translate>
|
|
||||||
</v-btn>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex
|
|
||||||
sm4
|
|
||||||
xs12
|
|
||||||
class="text-xs-center"
|
|
||||||
>
|
|
||||||
<v-select
|
|
||||||
v-model="type"
|
|
||||||
:items="typeOptions"
|
|
||||||
:label="labels.type"
|
|
||||||
></v-select>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex
|
|
||||||
sm4
|
|
||||||
xs12
|
|
||||||
class="text-sm-right text-xs-center"
|
|
||||||
>
|
|
||||||
<v-btn @click="$refs.calendar.next()">
|
|
||||||
<translate>Next</translate>
|
|
||||||
<v-icon
|
|
||||||
right
|
|
||||||
dark
|
|
||||||
>
|
|
||||||
keyboard_arrow_right
|
|
||||||
</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>
|
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'calendar',
|
|
||||||
data: () => ({
|
|
||||||
type: 'month',
|
|
||||||
start: '2019-01-01',
|
|
||||||
end: '2019-01-06',
|
|
||||||
typeOptions: [
|
|
||||||
{text: this.$gettext('Day'), value: 'day'},
|
|
||||||
{text: this.$gettext('4 Day'), value: '4day'},
|
|
||||||
{text: this.$gettext('Week'), value: 'week'},
|
|
||||||
{text: this.$gettext('Month'), value: 'month'},
|
|
||||||
{text: this.$gettext('Custom Daily'), value: 'custom-daily'},
|
|
||||||
{text: this.$gettext('Custom Weekly'), value: 'custom-weekly'}
|
|
||||||
],
|
|
||||||
labels: {
|
|
||||||
type: this.$gettext("Type"),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
methods: {}
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -40,6 +40,19 @@
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const s = this.$config.values.settings.maps;
|
const s = this.$config.values.settings.maps;
|
||||||
|
const filter = {
|
||||||
|
q: this.query(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings = this.$config.settings();
|
||||||
|
|
||||||
|
if (settings && settings.features.private) {
|
||||||
|
filter.public = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings && settings.features.review && (!this.staticFilter || !("quality" in this.staticFilter))) {
|
||||||
|
filter.quality = 3;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
|
@ -58,9 +71,7 @@
|
||||||
},
|
},
|
||||||
photos: [],
|
photos: [],
|
||||||
result: {},
|
result: {},
|
||||||
filter: {
|
filter: filter,
|
||||||
q: this.query(),
|
|
||||||
},
|
|
||||||
lastFilter: {},
|
lastFilter: {},
|
||||||
labels: {
|
labels: {
|
||||||
search: this.$gettext("Search"),
|
search: this.$gettext("Search"),
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<v-toolbar flat color="secondary">
|
|
||||||
<v-toolbar-title>Not implemented yet</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: 'todo',
|
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
methods: {}
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -1,29 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="p-page p-page-todo">
|
|
||||||
<v-toolbar flat color="secondary">
|
|
||||||
<v-toolbar-title>Not implemented yet</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: 'p-page-todo',
|
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
methods: {}
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -2,15 +2,13 @@ import Photos from "pages/photos.vue";
|
||||||
import Albums from "pages/albums.vue";
|
import Albums from "pages/albums.vue";
|
||||||
import AlbumPhotos from "pages/album/photos.vue";
|
import AlbumPhotos from "pages/album/photos.vue";
|
||||||
import Places from "pages/places.vue";
|
import Places from "pages/places.vue";
|
||||||
import Files from "pages/files.vue";
|
import Files from "pages/library/files.vue";
|
||||||
import Labels from "pages/labels.vue";
|
import Labels from "pages/labels.vue";
|
||||||
import People from "pages/people.vue";
|
import People from "pages/people.vue";
|
||||||
import Library from "pages/library.vue";
|
import Library from "pages/library.vue";
|
||||||
import Share from "pages/share.vue";
|
|
||||||
import Settings from "pages/settings.vue";
|
import Settings from "pages/settings.vue";
|
||||||
import Login from "pages/login.vue";
|
import Login from "pages/login.vue";
|
||||||
import Discover from "pages/discover.vue";
|
import Discover from "pages/discover.vue";
|
||||||
import Todo from "pages/todo.vue";
|
|
||||||
|
|
||||||
const c = window.__CONFIG__;
|
const c = window.__CONFIG__;
|
||||||
|
|
||||||
|
@ -134,10 +132,16 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "files",
|
name: "files",
|
||||||
path: "/files*",
|
path: "/library/files*",
|
||||||
component: Files,
|
component: Files,
|
||||||
meta: {title: "File Browser", auth: true},
|
meta: {title: "File Browser", auth: true},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "hidden",
|
||||||
|
path: "/library/hidden",
|
||||||
|
component: Photos,
|
||||||
|
props: {staticFilter: {hidden: true}},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "labels",
|
name: "labels",
|
||||||
path: "/labels",
|
path: "/labels",
|
||||||
|
@ -157,12 +161,6 @@ export default [
|
||||||
component: People,
|
component: People,
|
||||||
meta: {title: "People", auth: true},
|
meta: {title: "People", auth: true},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "filters",
|
|
||||||
path: "/filters",
|
|
||||||
component: Todo,
|
|
||||||
meta: {title: "Filters", auth: true},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "library_logs",
|
name: "library_logs",
|
||||||
path: "/library/logs",
|
path: "/library/logs",
|
||||||
|
@ -184,12 +182,6 @@ export default [
|
||||||
meta: {title: "Originals", auth: true, background: "application-light"},
|
meta: {title: "Originals", auth: true, background: "application-light"},
|
||||||
props: {tab: 0},
|
props: {tab: 0},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "share",
|
|
||||||
path: "/share",
|
|
||||||
component: Share,
|
|
||||||
meta: {title: "Share with friends", auth: true},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "settings",
|
name: "settings",
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
|
|
|
@ -33,14 +33,45 @@ var UnknownCamera = Camera{
|
||||||
|
|
||||||
var CameraMakes = map[string]string{
|
var CameraMakes = map[string]string{
|
||||||
"OLYMPUS OPTICAL CO.,LTD": "Olympus",
|
"OLYMPUS OPTICAL CO.,LTD": "Olympus",
|
||||||
|
"samsung": "Samsung",
|
||||||
}
|
}
|
||||||
|
|
||||||
var CameraModels = map[string]string{
|
var CameraModels = map[string]string{
|
||||||
"ELE-L29": "P30",
|
"ELE-L29": "P30",
|
||||||
"ELE-AL00": "P30",
|
"ELE-AL00": "P30",
|
||||||
"ELE-L04": "P30",
|
"ELE-L04": "P30",
|
||||||
"ELE-L09": "P30",
|
"ELE-L09": "P30",
|
||||||
"ELE-TL00": "P30",
|
"ELE-TL00": "P30",
|
||||||
|
"VOG-L29": "P30 Pro",
|
||||||
|
"VOG-L09": "P30 Pro",
|
||||||
|
"VOG-L04": "P30 Pro",
|
||||||
|
"VOG-AL00": "P30 Pro",
|
||||||
|
"VOG-AL10": "P30 Pro",
|
||||||
|
"VOG-TL00": "P30 Pro",
|
||||||
|
"MAR-L01A": "P30 Lite",
|
||||||
|
"MAR-L21A": "P30 Lite",
|
||||||
|
"MAR-LX1A": "P30 Lite",
|
||||||
|
"MAR-LX1M": "P30 Lite",
|
||||||
|
"MAR-LX2": "P30 Lite",
|
||||||
|
"MAR-L21MEA": "P30 Lite",
|
||||||
|
"MAR-L22A": "P30 Lite",
|
||||||
|
"MAR-L22B": "P30 Lite",
|
||||||
|
"MAR-LX3A": "P30 Lite",
|
||||||
|
"ANA-AN00": "P40",
|
||||||
|
"ANA-TN00": "P40",
|
||||||
|
"ELS-AN00": "P40 Pro",
|
||||||
|
"ELS-TN00": "P40 Pro",
|
||||||
|
"ELS-NX9": "P40 Pro",
|
||||||
|
"ELS-N04": "P40 Pro",
|
||||||
|
"JNY-L21A": "P40 Lite",
|
||||||
|
"JNY-L01A": "P40 Lite",
|
||||||
|
"JNY-L21B": "P40 Lite",
|
||||||
|
"JNY-L22A": "P40 Lite",
|
||||||
|
"JNY-L02A": "P40 Lite",
|
||||||
|
"JNY-L22B": "P40 Lite",
|
||||||
|
"STK-LX1": "Honor 9X",
|
||||||
|
"HLK-AL00": "Honor 9X",
|
||||||
|
"HLK-TL00": "Honor 9X",
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUnknownCamera initializes the database with an unknown camera if not exists
|
// CreateUnknownCamera initializes the database with an unknown camera if not exists
|
||||||
|
|
|
@ -419,7 +419,18 @@ func (m *Photo) LoadPlace() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var place Place
|
var place Place
|
||||||
return Db().Set("gorm:auto_preload", true).Model(m).Related(&place, "Place").Error
|
|
||||||
|
err := Db().Set("gorm:auto_preload", true).Model(m).Related(&place, "Place").Error
|
||||||
|
|
||||||
|
if m.Place == nil {
|
||||||
|
m.Place = &place
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Place.Unknown() {
|
||||||
|
m.Place = &UnknownPlace
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasLatLng checks if the photo has a latitude and longitude.
|
// HasLatLng checks if the photo has a latitude and longitude.
|
||||||
|
|
|
@ -102,7 +102,7 @@ func (m *Photo) UpdateLocation(geoApi string) (keywords []string, labels classif
|
||||||
if m.UnknownLocation() {
|
if m.UnknownLocation() {
|
||||||
m.Location = &UnknownLocation
|
m.Location = &UnknownLocation
|
||||||
m.LocationID = UnknownLocation.ID
|
m.LocationID = UnknownLocation.ID
|
||||||
} else if err := m.LoadLocation(); err == nil {
|
} else if err := m.LoadLocation(); err == nil && m.Location != nil && m.Location.Place != nil {
|
||||||
m.Place = m.Location.Place
|
m.Place = m.Location.Place
|
||||||
m.PlaceID = m.Location.PlaceID
|
m.PlaceID = m.Location.PlaceID
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ func (m *Photo) UpdateLocation(geoApi string) (keywords []string, labels classif
|
||||||
if m.UnknownPlace() {
|
if m.UnknownPlace() {
|
||||||
m.Place = &UnknownPlace
|
m.Place = &UnknownPlace
|
||||||
m.PlaceID = UnknownPlace.ID
|
m.PlaceID = UnknownPlace.ID
|
||||||
} else if err := m.LoadPlace(); err == nil {
|
} else if err := m.LoadPlace(); err == nil && m.Place != nil {
|
||||||
m.PhotoCountry = m.Place.CountryCode()
|
m.PhotoCountry = m.Place.CountryCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,13 +14,16 @@ type GeoSearch struct {
|
||||||
Favorite bool `form:"favorite"`
|
Favorite bool `form:"favorite"`
|
||||||
Video bool `form:"video"`
|
Video bool `form:"video"`
|
||||||
Photo bool `form:"photo"`
|
Photo bool `form:"photo"`
|
||||||
|
Archived bool `form:"archived"`
|
||||||
|
Public bool `form:"public"`
|
||||||
|
Private bool `form:"private"`
|
||||||
|
Review bool `form:"review"`
|
||||||
|
Quality int `form:"quality"`
|
||||||
Lat float32 `form:"lat"`
|
Lat float32 `form:"lat"`
|
||||||
Lng float32 `form:"lng"`
|
Lng float32 `form:"lng"`
|
||||||
S2 string `form:"s2"`
|
S2 string `form:"s2"`
|
||||||
Olc string `form:"olc"`
|
Olc string `form:"olc"`
|
||||||
Dist uint `form:"dist"`
|
Dist uint `form:"dist"`
|
||||||
Quality int `form:"quality"`
|
|
||||||
Review bool `form:"review"`
|
|
||||||
Album string `form:"album"`
|
Album string `form:"album"`
|
||||||
Country string `form:"country"`
|
Country string `form:"country"`
|
||||||
Year int `form:"year"`
|
Year int `form:"year"`
|
||||||
|
|
|
@ -19,8 +19,13 @@ type PhotoSearch struct {
|
||||||
Video bool `form:"video"`
|
Video bool `form:"video"`
|
||||||
Photo bool `form:"photo"`
|
Photo bool `form:"photo"`
|
||||||
Duplicate bool `form:"duplicate"`
|
Duplicate bool `form:"duplicate"`
|
||||||
Archived bool `form:"archived"`
|
|
||||||
Error bool `form:"error"`
|
Error bool `form:"error"`
|
||||||
|
Hidden bool `form:"hidden"`
|
||||||
|
Archived bool `form:"archived"`
|
||||||
|
Public bool `form:"public"`
|
||||||
|
Private bool `form:"private"`
|
||||||
|
Favorite bool `form:"favorite"`
|
||||||
|
Safe bool `form:"safe"`
|
||||||
Lat float32 `form:"lat"`
|
Lat float32 `form:"lat"`
|
||||||
Lng float32 `form:"lng"`
|
Lng float32 `form:"lng"`
|
||||||
Dist uint `form:"dist"`
|
Dist uint `form:"dist"`
|
||||||
|
@ -45,10 +50,6 @@ type PhotoSearch struct {
|
||||||
Lens int `form:"lens"`
|
Lens int `form:"lens"`
|
||||||
Before time.Time `form:"before" time_format:"2006-01-02"`
|
Before time.Time `form:"before" time_format:"2006-01-02"`
|
||||||
After time.Time `form:"after" time_format:"2006-01-02"`
|
After time.Time `form:"after" time_format:"2006-01-02"`
|
||||||
Favorite bool `form:"favorite"`
|
|
||||||
Public bool `form:"public"`
|
|
||||||
Private bool `form:"private"`
|
|
||||||
Safe bool `form:"safe"`
|
|
||||||
Count int `form:"count" binding:"required" serialize:"-"`
|
Count int `form:"count" binding:"required" serialize:"-"`
|
||||||
Offset int `form:"offset" serialize:"-"`
|
Offset int `form:"offset" serialize:"-"`
|
||||||
Order string `form:"order" serialize:"-"`
|
Order string `form:"order" serialize:"-"`
|
||||||
|
|
|
@ -49,8 +49,8 @@ type Data struct {
|
||||||
|
|
||||||
// AspectRatio returns the aspect ratio based on width and height.
|
// AspectRatio returns the aspect ratio based on width and height.
|
||||||
func (data Data) AspectRatio() float32 {
|
func (data Data) AspectRatio() float32 {
|
||||||
width := float64(data.Width)
|
width := float64(data.ActualWidth())
|
||||||
height := float64(data.Height)
|
height := float64(data.ActualHeight())
|
||||||
|
|
||||||
if width <= 0 || height <= 0 {
|
if width <= 0 || height <= 0 {
|
||||||
return 0
|
return 0
|
||||||
|
@ -85,3 +85,21 @@ func (data Data) HasInstanceID() bool {
|
||||||
func (data Data) HasTimeAndPlace() bool {
|
func (data Data) HasTimeAndPlace() bool {
|
||||||
return !data.TakenAt.IsZero() && data.Lat != 0 && data.Lng != 0
|
return !data.TakenAt.IsZero() && data.Lat != 0 && data.Lng != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActualWidth is the width after rotating the media file if needed.
|
||||||
|
func (data Data) ActualWidth() int {
|
||||||
|
if data.Orientation > 4 {
|
||||||
|
return data.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActualHeight is the height after rotating the media file if needed.
|
||||||
|
func (data Data) ActualHeight() int {
|
||||||
|
if data.Orientation > 4 {
|
||||||
|
return data.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Height
|
||||||
|
}
|
||||||
|
|
|
@ -122,7 +122,7 @@ func TestExif(t *testing.T) {
|
||||||
assert.Equal(t, "Apple", data.CameraMake)
|
assert.Equal(t, "Apple", data.CameraMake)
|
||||||
assert.Equal(t, "iPhone 7", data.CameraModel)
|
assert.Equal(t, "iPhone 7", data.CameraModel)
|
||||||
assert.Equal(t, 74, data.FocalLength)
|
assert.Equal(t, 74, data.FocalLength)
|
||||||
assert.Equal(t, 6, int(data.Orientation))
|
assert.Equal(t, 6, data.Orientation)
|
||||||
assert.Equal(t, "Apple", data.LensMake)
|
assert.Equal(t, "Apple", data.LensMake)
|
||||||
assert.Equal(t, "iPhone 7 back camera 3.99mm f/1.8", data.LensModel)
|
assert.Equal(t, "iPhone 7 back camera 3.99mm f/1.8", data.LensModel)
|
||||||
|
|
||||||
|
@ -232,4 +232,38 @@ func TestExif(t *testing.T) {
|
||||||
assert.Equal(t, "721", data.All["PixelXDimension"])
|
assert.Equal(t, "721", data.All["PixelXDimension"])
|
||||||
assert.Equal(t, "332", data.All["PixelYDimension"])
|
assert.Equal(t, "332", data.All["PixelYDimension"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("orientation.jpg", func(t *testing.T) {
|
||||||
|
data, err := Exif("testdata/orientation.jpg")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "3264", data.All["PixelXDimension"])
|
||||||
|
assert.Equal(t, "1836", data.All["PixelYDimension"])
|
||||||
|
assert.Equal(t, 3264, data.Width)
|
||||||
|
assert.Equal(t, 1836, data.Height)
|
||||||
|
assert.Equal(t, 6, data.Orientation) // TODO: Should be 1
|
||||||
|
|
||||||
|
if err := data.JSON("testdata/orientation.json", "orientation.jpg"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, 326, data.Width)
|
||||||
|
assert.Equal(t, 184, data.Height)
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
|
|
||||||
|
if err := data.JSON("testdata/orientation.json", "foo.jpg"); err != nil {
|
||||||
|
assert.EqualError(t, err, "meta: original name foo.jpg does not match orientation.jpg (json)")
|
||||||
|
} else {
|
||||||
|
t.Error("error expected when providing wrong orginal name")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gopher-preview.jpg", func(t *testing.T) {
|
||||||
|
_, err := Exif("testdata/gopher-preview.jpg")
|
||||||
|
|
||||||
|
assert.EqualError(t, err, "no exif data in gopher-preview.jpg")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,14 +14,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// JSON parses a json sidecar file (as used by Exiftool) and returns a Data struct.
|
// JSON parses a json sidecar file (as used by Exiftool) and returns a Data struct.
|
||||||
func JSON(fileName string) (data Data, err error) {
|
func JSON(jsonName, originalName string) (data Data, err error) {
|
||||||
err = data.JSON(fileName)
|
err = data.JSON(jsonName, originalName)
|
||||||
|
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON parses a json sidecar file (as used by Exiftool) and returns a Data struct.
|
// JSON parses a json sidecar file (as used by Exiftool) and returns a Data struct.
|
||||||
func (data *Data) JSON(fileName string) (err error) {
|
func (data *Data) JSON(jsonName, originalName string) (err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if e := recover(); e != nil {
|
if e := recover(); e != nil {
|
||||||
err = fmt.Errorf("%s (json metadata)", e)
|
err = fmt.Errorf("%s (json metadata)", e)
|
||||||
|
@ -32,17 +32,17 @@ func (data *Data) JSON(fileName string) (err error) {
|
||||||
data.All = make(map[string]string)
|
data.All = make(map[string]string)
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonString, err := ioutil.ReadFile(fileName)
|
jsonString, err := ioutil.ReadFile(jsonName)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("meta: %s", err.Error())
|
log.Warnf("meta: %s", err.Error())
|
||||||
return fmt.Errorf("can't read %s (json)", txt.Quote(filepath.Base(fileName)))
|
return fmt.Errorf("can't read %s (json)", txt.Quote(filepath.Base(jsonName)))
|
||||||
}
|
}
|
||||||
|
|
||||||
j := gjson.GetBytes(jsonString, "@flatten|@join")
|
j := gjson.GetBytes(jsonString, "@flatten|@join")
|
||||||
|
|
||||||
if !j.IsObject() {
|
if !j.IsObject() {
|
||||||
return fmt.Errorf("data is not an object in %s (json)", txt.Quote(filepath.Base(fileName)))
|
return fmt.Errorf("data is not an object in %s (json)", txt.Quote(filepath.Base(jsonName)))
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonValues := j.Map()
|
jsonValues := j.Map()
|
||||||
|
@ -51,6 +51,10 @@ func (data *Data) JSON(fileName string) (err error) {
|
||||||
data.All[key] = val.String()
|
data.All[key] = val.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if fileName, ok := data.All["FileName"]; ok && fileName != "" && originalName != "" && fileName != originalName {
|
||||||
|
return fmt.Errorf("meta: original name %s does not match %s (json)", txt.Quote(originalName), txt.Quote(fileName))
|
||||||
|
}
|
||||||
|
|
||||||
v := reflect.ValueOf(data).Elem()
|
v := reflect.ValueOf(data).Elem()
|
||||||
|
|
||||||
// Iterate through all config fields
|
// Iterate through all config fields
|
||||||
|
@ -132,10 +136,39 @@ func (data *Data) JSON(fileName string) (err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix rotation.
|
if orientation, ok := data.All["Orientation"]; ok && orientation != "" {
|
||||||
if data.Rotation == 90 || data.Rotation == 270 || data.Rotation == -90 {
|
switch orientation {
|
||||||
data.Width, data.Height = data.Height, data.Width
|
case "1", "Horizontal (normal)":
|
||||||
data.Rotation = 0
|
data.Orientation = 1
|
||||||
|
case "2":
|
||||||
|
data.Orientation = 2
|
||||||
|
case "3", "Rotate 180 CW":
|
||||||
|
data.Orientation = 3
|
||||||
|
case "4":
|
||||||
|
data.Orientation = 4
|
||||||
|
case "5":
|
||||||
|
data.Orientation = 5
|
||||||
|
case "6", "Rotate 90 CW":
|
||||||
|
data.Orientation = 6
|
||||||
|
case "7":
|
||||||
|
data.Orientation = 7
|
||||||
|
case "8", "Rotate 270 CW":
|
||||||
|
data.Orientation = 8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Orientation == 0 {
|
||||||
|
// Set orientation based on rotation.
|
||||||
|
switch data.Rotation {
|
||||||
|
case 0:
|
||||||
|
data.Orientation = 1
|
||||||
|
case -180, 180:
|
||||||
|
data.Orientation = 3
|
||||||
|
case 90:
|
||||||
|
data.Orientation = 6
|
||||||
|
case -90, 270:
|
||||||
|
data.Orientation = 8
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize compression information.
|
// Normalize compression information.
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
func TestJSON(t *testing.T) {
|
func TestJSON(t *testing.T) {
|
||||||
t.Run("iphone-mov.json", func(t *testing.T) {
|
t.Run("iphone-mov.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/iphone-mov.json")
|
data, err := JSON("testdata/iphone-mov.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -21,8 +21,11 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "2018-09-08 17:20:14 +0000 UTC", data.TakenAtLocal.String())
|
assert.Equal(t, "2018-09-08 17:20:14 +0000 UTC", data.TakenAtLocal.String())
|
||||||
assert.Equal(t, "2018-09-08 15:20:14 +0000 UTC", data.TakenAt.String())
|
assert.Equal(t, "2018-09-08 15:20:14 +0000 UTC", data.TakenAt.String())
|
||||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||||
assert.Equal(t, 1080, data.Width)
|
assert.Equal(t, 1920, data.Width)
|
||||||
assert.Equal(t, 1920, data.Height)
|
assert.Equal(t, 1080, data.Height)
|
||||||
|
assert.Equal(t, 1080, data.ActualWidth())
|
||||||
|
assert.Equal(t, 1920, data.ActualHeight())
|
||||||
|
assert.Equal(t, 6, data.Orientation)
|
||||||
assert.Equal(t, float32(52.4587), data.Lat)
|
assert.Equal(t, float32(52.4587), data.Lat)
|
||||||
assert.Equal(t, float32(13.4593), data.Lng)
|
assert.Equal(t, float32(13.4593), data.Lng)
|
||||||
assert.Equal(t, "Apple", data.CameraMake)
|
assert.Equal(t, "Apple", data.CameraMake)
|
||||||
|
@ -31,7 +34,7 @@ func TestJSON(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("gopher-telegram.json", func(t *testing.T) {
|
t.Run("gopher-telegram.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/gopher-telegram.json")
|
data, err := JSON("testdata/gopher-telegram.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -46,6 +49,9 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "", data.TimeZone)
|
assert.Equal(t, "", data.TimeZone)
|
||||||
assert.Equal(t, 270, data.Width)
|
assert.Equal(t, 270, data.Width)
|
||||||
assert.Equal(t, 480, data.Height)
|
assert.Equal(t, 480, data.Height)
|
||||||
|
assert.Equal(t, 270, data.ActualWidth())
|
||||||
|
assert.Equal(t, 480, data.ActualHeight())
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
assert.Equal(t, float32(0), data.Lat)
|
assert.Equal(t, float32(0), data.Lat)
|
||||||
assert.Equal(t, float32(0), data.Lng)
|
assert.Equal(t, float32(0), data.Lng)
|
||||||
assert.Equal(t, "", data.CameraMake)
|
assert.Equal(t, "", data.CameraMake)
|
||||||
|
@ -54,7 +60,7 @@ func TestJSON(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("gopher-original.json", func(t *testing.T) {
|
t.Run("gopher-original.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/gopher-original.json")
|
data, err := JSON("testdata/gopher-original.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -67,8 +73,12 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "2020-05-11 14:16:48 +0000 UTC", data.TakenAtLocal.String())
|
assert.Equal(t, "2020-05-11 14:16:48 +0000 UTC", data.TakenAtLocal.String())
|
||||||
assert.Equal(t, "2020-05-11 12:16:48 +0000 UTC", data.TakenAt.String())
|
assert.Equal(t, "2020-05-11 12:16:48 +0000 UTC", data.TakenAt.String())
|
||||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||||
assert.Equal(t, 1080, data.Width)
|
assert.Equal(t, 1920, data.Width)
|
||||||
assert.Equal(t, 1920, data.Height)
|
assert.Equal(t, 1080, data.Height)
|
||||||
|
assert.Equal(t, 1080, data.ActualWidth())
|
||||||
|
assert.Equal(t, 1920, data.ActualHeight())
|
||||||
|
assert.Equal(t, float32(0.5625), data.AspectRatio())
|
||||||
|
assert.Equal(t, 6, data.Orientation)
|
||||||
assert.Equal(t, float32(52.4596), data.Lat)
|
assert.Equal(t, float32(52.4596), data.Lat)
|
||||||
assert.Equal(t, float32(13.3218), data.Lng)
|
assert.Equal(t, float32(13.3218), data.Lng)
|
||||||
assert.Equal(t, "", data.CameraMake)
|
assert.Equal(t, "", data.CameraMake)
|
||||||
|
@ -77,7 +87,7 @@ func TestJSON(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("berlin-landscape.json", func(t *testing.T) {
|
t.Run("berlin-landscape.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/berlin-landscape.json")
|
data, err := JSON("testdata/berlin-landscape.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -92,6 +102,7 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||||
assert.Equal(t, 1920, data.Width)
|
assert.Equal(t, 1920, data.Width)
|
||||||
assert.Equal(t, 1080, data.Height)
|
assert.Equal(t, 1080, data.Height)
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
assert.Equal(t, float32(52.4649), data.Lat)
|
assert.Equal(t, float32(52.4649), data.Lat)
|
||||||
assert.Equal(t, float32(13.3148), data.Lng)
|
assert.Equal(t, float32(13.3148), data.Lng)
|
||||||
assert.Equal(t, "", data.CameraMake)
|
assert.Equal(t, "", data.CameraMake)
|
||||||
|
@ -100,7 +111,7 @@ func TestJSON(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("mp4.json", func(t *testing.T) {
|
t.Run("mp4.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/mp4.json")
|
data, err := JSON("testdata/mp4.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -113,6 +124,7 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "2019-11-23 13:51:49 +0000 UTC", data.TakenAtLocal.String())
|
assert.Equal(t, "2019-11-23 13:51:49 +0000 UTC", data.TakenAtLocal.String())
|
||||||
assert.Equal(t, 848, data.Width)
|
assert.Equal(t, 848, data.Width)
|
||||||
assert.Equal(t, 480, data.Height)
|
assert.Equal(t, 480, data.Height)
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
assert.Equal(t, "", data.Copyright)
|
assert.Equal(t, "", data.Copyright)
|
||||||
assert.Equal(t, "", data.CameraMake)
|
assert.Equal(t, "", data.CameraMake)
|
||||||
assert.Equal(t, "", data.CameraModel)
|
assert.Equal(t, "", data.CameraModel)
|
||||||
|
@ -120,7 +132,7 @@ func TestJSON(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("photoshop.json", func(t *testing.T) {
|
t.Run("photoshop.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/photoshop.json")
|
data, err := JSON("testdata/photoshop.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -142,10 +154,11 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "HUAWEI", data.CameraMake)
|
assert.Equal(t, "HUAWEI", data.CameraMake)
|
||||||
assert.Equal(t, "ELE-L29", data.CameraModel)
|
assert.Equal(t, "ELE-L29", data.CameraModel)
|
||||||
assert.Equal(t, "HUAWEI P30 Rear Main Camera", data.LensModel)
|
assert.Equal(t, "HUAWEI P30 Rear Main Camera", data.LensModel)
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("canon_eos_6d.json", func(t *testing.T) {
|
t.Run("canon_eos_6d.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/canon_eos_6d.json")
|
data, err := JSON("testdata/canon_eos_6d.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -161,10 +174,11 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "Canon", data.CameraMake)
|
assert.Equal(t, "Canon", data.CameraMake)
|
||||||
assert.Equal(t, "Canon EOS 6D", data.CameraModel)
|
assert.Equal(t, "Canon EOS 6D", data.CameraModel)
|
||||||
assert.Equal(t, "EF24-105mm f/4L IS USM", data.LensModel)
|
assert.Equal(t, "EF24-105mm f/4L IS USM", data.LensModel)
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("gps-2000.json", func(t *testing.T) {
|
t.Run("gps-2000.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/gps-2000.json")
|
data, err := JSON("testdata/gps-2000.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -181,10 +195,11 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "", data.CameraModel)
|
assert.Equal(t, "", data.CameraModel)
|
||||||
assert.Equal(t, "", data.LensMake)
|
assert.Equal(t, "", data.LensMake)
|
||||||
assert.Equal(t, "", data.LensModel)
|
assert.Equal(t, "", data.LensModel)
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ladybug.json", func(t *testing.T) {
|
t.Run("ladybug.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/ladybug.json")
|
data, err := JSON("testdata/ladybug.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -201,10 +216,11 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "", data.CameraModel)
|
assert.Equal(t, "", data.CameraModel)
|
||||||
assert.Equal(t, "", data.LensMake)
|
assert.Equal(t, "", data.LensMake)
|
||||||
assert.Equal(t, "", data.LensModel)
|
assert.Equal(t, "", data.LensModel)
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("iphone_7.json", func(t *testing.T) {
|
t.Run("iphone_7.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/iphone_7.json")
|
data, err := JSON("testdata/iphone_7.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -217,6 +233,7 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "", data.Artist)
|
assert.Equal(t, "", data.Artist)
|
||||||
assert.Equal(t, "", data.Description)
|
assert.Equal(t, "", data.Description)
|
||||||
assert.Equal(t, "", data.Copyright)
|
assert.Equal(t, "", data.Copyright)
|
||||||
|
assert.Equal(t, 6, data.Orientation)
|
||||||
assert.Equal(t, "Apple", data.CameraMake)
|
assert.Equal(t, "Apple", data.CameraMake)
|
||||||
assert.Equal(t, "iPhone 7", data.CameraModel)
|
assert.Equal(t, "iPhone 7", data.CameraModel)
|
||||||
assert.Equal(t, "Apple", data.LensMake)
|
assert.Equal(t, "Apple", data.LensMake)
|
||||||
|
@ -224,7 +241,7 @@ func TestJSON(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("uuid-original.json", func(t *testing.T) {
|
t.Run("uuid-original.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/uuid-original.json")
|
data, err := JSON("testdata/uuid-original.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -241,6 +258,7 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||||
assert.Equal(t, 3024, data.Width)
|
assert.Equal(t, 3024, data.Width)
|
||||||
assert.Equal(t, 4032, data.Height)
|
assert.Equal(t, 4032, data.Height)
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
assert.Equal(t, float32(48.300003), data.Lat)
|
assert.Equal(t, float32(48.300003), data.Lat)
|
||||||
assert.Equal(t, float32(8.929067), data.Lng)
|
assert.Equal(t, float32(8.929067), data.Lng)
|
||||||
assert.Equal(t, "Apple", data.CameraMake)
|
assert.Equal(t, "Apple", data.CameraMake)
|
||||||
|
@ -249,7 +267,7 @@ func TestJSON(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("uuid-copy.json", func(t *testing.T) {
|
t.Run("uuid-copy.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/uuid-copy.json")
|
data, err := JSON("testdata/uuid-copy.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -266,6 +284,7 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||||
assert.Equal(t, 1024, data.Width)
|
assert.Equal(t, 1024, data.Width)
|
||||||
assert.Equal(t, 1365, data.Height)
|
assert.Equal(t, 1365, data.Height)
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
assert.Equal(t, float32(48.300003), data.Lat)
|
assert.Equal(t, float32(48.300003), data.Lat)
|
||||||
assert.Equal(t, float32(8.929067), data.Lng)
|
assert.Equal(t, float32(8.929067), data.Lng)
|
||||||
assert.Equal(t, "Apple", data.CameraMake)
|
assert.Equal(t, "Apple", data.CameraMake)
|
||||||
|
@ -274,7 +293,7 @@ func TestJSON(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("uuid-imagemagick.json", func(t *testing.T) {
|
t.Run("uuid-imagemagick.json", func(t *testing.T) {
|
||||||
data, err := JSON("testdata/uuid-imagemagick.json")
|
data, err := JSON("testdata/uuid-imagemagick.json", "")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -291,10 +310,23 @@ func TestJSON(t *testing.T) {
|
||||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||||
assert.Equal(t, 1125, data.Width)
|
assert.Equal(t, 1125, data.Width)
|
||||||
assert.Equal(t, 1500, data.Height)
|
assert.Equal(t, 1500, data.Height)
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
assert.Equal(t, float32(48.300003), data.Lat)
|
assert.Equal(t, float32(48.300003), data.Lat)
|
||||||
assert.Equal(t, float32(8.929067), data.Lng)
|
assert.Equal(t, float32(8.929067), data.Lng)
|
||||||
assert.Equal(t, "Apple", data.CameraMake)
|
assert.Equal(t, "Apple", data.CameraMake)
|
||||||
assert.Equal(t, "iPhone SE", data.CameraModel)
|
assert.Equal(t, "iPhone SE", data.CameraModel)
|
||||||
assert.Equal(t, "iPhone SE back camera 4.15mm f/2.2", data.LensModel)
|
assert.Equal(t, "iPhone SE back camera 4.15mm f/2.2", data.LensModel)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("orientation.json", func(t *testing.T) {
|
||||||
|
data, err := JSON("testdata/orientation.json", "")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, 326, data.Width)
|
||||||
|
assert.Equal(t, 184, data.Height)
|
||||||
|
assert.Equal(t, 1, data.Orientation)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
BIN
internal/meta/testdata/gopher-preview.jpg
vendored
Normal file
BIN
internal/meta/testdata/gopher-preview.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
BIN
internal/meta/testdata/orientation.jpg
vendored
Normal file
BIN
internal/meta/testdata/orientation.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
91
internal/meta/testdata/orientation.json
vendored
Normal file
91
internal/meta/testdata/orientation.json
vendored
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
[{
|
||||||
|
"SourceFile": "orientation.jpg",
|
||||||
|
"ExifToolVersion": 10.80,
|
||||||
|
"FileName": "orientation.jpg",
|
||||||
|
"Directory": ".",
|
||||||
|
"FileSize": "45 kB",
|
||||||
|
"FileModifyDate": "2020:06:04 09:20:43+00:00",
|
||||||
|
"FileAccessDate": "2020:06:04 09:20:45+00:00",
|
||||||
|
"FileInodeChangeDate": "2020:06:04 09:20:43+00:00",
|
||||||
|
"FilePermissions": "rw-r--r--",
|
||||||
|
"FileType": "JPEG",
|
||||||
|
"FileTypeExtension": "jpg",
|
||||||
|
"MIMEType": "image/jpeg",
|
||||||
|
"JFIFVersion": 1.01,
|
||||||
|
"ExifByteOrder": "Little-endian (Intel, II)",
|
||||||
|
"Make": "samsung",
|
||||||
|
"Model": "SM-G900F",
|
||||||
|
"Orientation": "Horizontal (normal)",
|
||||||
|
"XResolution": 72,
|
||||||
|
"YResolution": 72,
|
||||||
|
"ResolutionUnit": "inches",
|
||||||
|
"Software": "G900FXXS1CQD1",
|
||||||
|
"ModifyDate": "2018:10:03 16:56:19",
|
||||||
|
"YCbCrPositioning": "Centered",
|
||||||
|
"ExposureTime": "1/2376",
|
||||||
|
"FNumber": 2.2,
|
||||||
|
"ExposureProgram": "Program AE",
|
||||||
|
"ISO": 40,
|
||||||
|
"ExifVersion": "0220",
|
||||||
|
"DateTimeOriginal": "2018:10:03 16:56:19",
|
||||||
|
"CreateDate": "2018:10:03 16:56:19",
|
||||||
|
"ComponentsConfiguration": "Y, Cb, Cr, -",
|
||||||
|
"ShutterSpeedValue": "1/2369",
|
||||||
|
"ApertureValue": 2.2,
|
||||||
|
"BrightnessValue": 9.92,
|
||||||
|
"ExposureCompensation": 0,
|
||||||
|
"MaxApertureValue": 2.2,
|
||||||
|
"MeteringMode": "Center-weighted average",
|
||||||
|
"LightSource": "Unknown",
|
||||||
|
"Flash": "Fired",
|
||||||
|
"FocalLength": "4.8 mm",
|
||||||
|
"UserComment": "\n",
|
||||||
|
"SubSecTime": 414,
|
||||||
|
"SubSecTimeOriginal": 414,
|
||||||
|
"SubSecTimeDigitized": 414,
|
||||||
|
"FlashpixVersion": "0100",
|
||||||
|
"ColorSpace": "sRGB",
|
||||||
|
"ExifImageWidth": 3264,
|
||||||
|
"ExifImageHeight": 1836,
|
||||||
|
"InteropIndex": "R98 - DCF basic file (sRGB)",
|
||||||
|
"InteropVersion": "0100",
|
||||||
|
"SensingMethod": "One-chip color area",
|
||||||
|
"SceneType": "Directly photographed",
|
||||||
|
"ExposureMode": "Auto",
|
||||||
|
"WhiteBalance": "Auto",
|
||||||
|
"FocalLengthIn35mmFormat": "31 mm",
|
||||||
|
"SceneCaptureType": "Standard",
|
||||||
|
"ImageUniqueID": "F16QLHF01VB",
|
||||||
|
"GPSVersionID": "2.2.0.0",
|
||||||
|
"GPSLatitudeRef": "North",
|
||||||
|
"GPSLongitudeRef": "East",
|
||||||
|
"Compression": "JPEG (old-style)",
|
||||||
|
"ThumbnailOffset": 3426,
|
||||||
|
"ThumbnailLength": 12320,
|
||||||
|
"XMPToolkit": "Image::ExifTool 10.80",
|
||||||
|
"Subject": "Nice",
|
||||||
|
"Title": "Côte d'Azur 3-9 octobre 2018 - Nice",
|
||||||
|
"ImageWidth": 326,
|
||||||
|
"ImageHeight": 184,
|
||||||
|
"EncodingProcess": "Baseline DCT, Huffman coding",
|
||||||
|
"BitsPerSample": 8,
|
||||||
|
"ColorComponents": 3,
|
||||||
|
"YCbCrSubSampling": "YCbCr4:2:0 (2 2)",
|
||||||
|
"Aperture": 2.2,
|
||||||
|
"GPSLatitude": "43 deg 41' 45.00\" N",
|
||||||
|
"GPSLongitude": "7 deg 16' 17.00\" E",
|
||||||
|
"GPSPosition": "43 deg 41' 45.00\" N, 7 deg 16' 17.00\" E",
|
||||||
|
"ImageSize": "326x184",
|
||||||
|
"Megapixels": 0.060,
|
||||||
|
"ScaleFactor35efl": 6.5,
|
||||||
|
"ShutterSpeed": "1/2376",
|
||||||
|
"SubSecCreateDate": "2018:10:03 16:56:19.414",
|
||||||
|
"SubSecDateTimeOriginal": "2018:10:03 16:56:19.414",
|
||||||
|
"SubSecModifyDate": "2018:10:03 16:56:19.414",
|
||||||
|
"ThumbnailImage": "(Binary data 12320 bytes, use -b option to extract)",
|
||||||
|
"CircleOfConfusion": "0.005 mm",
|
||||||
|
"FOV": "60.3 deg",
|
||||||
|
"FocalLength35efl": "4.8 mm (35 mm equivalent: 31.0 mm)",
|
||||||
|
"HyperfocalDistance": "2.25 m",
|
||||||
|
"LightValue": 14.8
|
||||||
|
}]
|
|
@ -226,8 +226,8 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
||||||
metaData = m.MetaData()
|
metaData = m.MetaData()
|
||||||
|
|
||||||
file.FileCodec = metaData.Codec
|
file.FileCodec = metaData.Codec
|
||||||
file.FileWidth = metaData.Width
|
file.FileWidth = metaData.ActualWidth()
|
||||||
file.FileHeight = metaData.Height
|
file.FileHeight = metaData.ActualHeight()
|
||||||
file.FileDuration = metaData.Duration
|
file.FileDuration = metaData.Duration
|
||||||
file.FileAspectRatio = metaData.AspectRatio()
|
file.FileAspectRatio = metaData.AspectRatio()
|
||||||
file.FilePortrait = metaData.Portrait()
|
file.FilePortrait = metaData.Portrait()
|
||||||
|
|
|
@ -309,6 +309,11 @@ func (m *MediaFile) FileName() string {
|
||||||
return m.fileName
|
return m.fileName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BaseName returns the filename without path.
|
||||||
|
func (m *MediaFile) BaseName() string {
|
||||||
|
return filepath.Base(m.fileName)
|
||||||
|
}
|
||||||
|
|
||||||
// SetFileName sets the filename to the given string.
|
// SetFileName sets the filename to the given string.
|
||||||
func (m *MediaFile) SetFileName(fileName string) {
|
func (m *MediaFile) SetFileName(fileName string) {
|
||||||
m.fileName = fileName
|
m.fileName = fileName
|
||||||
|
@ -620,7 +625,7 @@ func (m *MediaFile) HasJson() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MediaFile) decodeDimensions() error {
|
func (m *MediaFile) decodeDimensions() error {
|
||||||
if !m.IsPhoto() {
|
if !m.IsMedia() {
|
||||||
return fmt.Errorf("not a photo: %s", m.FileName())
|
return fmt.Errorf("not a photo: %s", m.FileName())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -665,7 +670,7 @@ func (m *MediaFile) decodeDimensions() error {
|
||||||
|
|
||||||
// Width return the width dimension of a MediaFile.
|
// Width return the width dimension of a MediaFile.
|
||||||
func (m *MediaFile) Width() int {
|
func (m *MediaFile) Width() int {
|
||||||
if !m.IsPhoto() {
|
if !m.IsMedia() {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -680,7 +685,7 @@ func (m *MediaFile) Width() int {
|
||||||
|
|
||||||
// Height returns the height dimension of a MediaFile.
|
// Height returns the height dimension of a MediaFile.
|
||||||
func (m *MediaFile) Height() int {
|
func (m *MediaFile) Height() int {
|
||||||
if !m.IsPhoto() {
|
if !m.IsMedia() {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,8 +22,8 @@ func (m *MediaFile) MetaData() (result meta.Data) {
|
||||||
|
|
||||||
if jsonFile := fs.TypeJson.FindSub(m.FileName(), fs.HiddenPath, false); jsonFile == "" {
|
if jsonFile := fs.TypeJson.FindSub(m.FileName(), fs.HiddenPath, false); jsonFile == "" {
|
||||||
log.Debugf("mediafile: no json sidecar file found for %s", txt.Quote(filepath.Base(m.FileName())))
|
log.Debugf("mediafile: no json sidecar file found for %s", txt.Quote(filepath.Base(m.FileName())))
|
||||||
} else if jsonErr := m.metaData.JSON(jsonFile); jsonErr != nil {
|
} else if jsonErr := m.metaData.JSON(jsonFile, m.BaseName()); jsonErr != nil {
|
||||||
log.Warn(jsonErr)
|
log.Debug(jsonErr)
|
||||||
} else {
|
} else {
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,10 +138,23 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
|
||||||
s = s.Where("photos.photo_name LIKE ?", strings.ReplaceAll(f.Name, "*", "%"))
|
s = s.Where("photos.photo_name LIKE ?", strings.ReplaceAll(f.Name, "*", "%"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Review {
|
// Filter by status.
|
||||||
s = s.Where("photos.photo_quality < 3")
|
if f.Archived {
|
||||||
} else if f.Quality != 0 {
|
s = s.Where("photos.deleted_at IS NOT NULL")
|
||||||
s = s.Where("photos.photo_quality >= ?", f.Quality)
|
} else {
|
||||||
|
s = s.Where("photos.deleted_at IS NULL")
|
||||||
|
|
||||||
|
if f.Private {
|
||||||
|
s = s.Where("photos.photo_private = 1")
|
||||||
|
} else if f.Public {
|
||||||
|
s = s.Where("photos.photo_private = 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.Review {
|
||||||
|
s = s.Where("photos.photo_quality < 3")
|
||||||
|
} else if f.Quality != 0 && f.Private == false {
|
||||||
|
s = s.Where("photos.photo_quality >= ?", f.Quality)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Favorite {
|
if f.Favorite {
|
||||||
|
|
|
@ -126,7 +126,10 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by status.
|
// Filter by status.
|
||||||
if f.Archived {
|
if f.Hidden {
|
||||||
|
s = s.Where("photos.photo_quality = -1")
|
||||||
|
s = s.Where("photos.deleted_at IS NULL")
|
||||||
|
} else if f.Archived {
|
||||||
s = s.Where("photos.deleted_at IS NOT NULL")
|
s = s.Where("photos.deleted_at IS NOT NULL")
|
||||||
} else {
|
} else {
|
||||||
s = s.Where("photos.deleted_at IS NULL")
|
s = s.Where("photos.deleted_at IS NULL")
|
||||||
|
|
Loading…
Reference in a new issue