Indexing bug fixes and UX improvements

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-06-04 14:56:27 +02:00
parent 73c4891cde
commit ca8a8466d4
27 changed files with 404 additions and 263 deletions

View file

@ -7,7 +7,7 @@ ENV TF_CPP_MIN_LOG_LEVEL 2
COPY /docker/demo/index.tmpl /photoprism/assets/templates
# 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
ENV PHOTOPRISM_STORAGE_PATH /photoprism/storage
@ -27,9 +27,11 @@ ENV PHOTOPRISM_THUMB_SIZE 3840
ENV PHOTOPRISM_THUMB_LIMIT 3840
ENV PHOTOPRISM_JPEG_QUALITY 95
ENV PHOTOPRISM_JPEG_HIDDEN false
ENV PHOTOPRISM_SITE_CAPTION "Try our demo"
# Import example photos
RUN photoprism import
RUN photoprism index
RUN photoprism moments
# Start PhotoPrism server
CMD photoprism --public start

View file

@ -296,7 +296,7 @@
</v-list-tile-content>
</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-title>
<translate key="Files">Files</translate>
@ -304,6 +304,15 @@
</v-list-tile-title>
</v-list-tile-content>
</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-tile to="/settings" @click="" class="p-navigation-settings" v-show="!config.disableSettings">

View file

@ -16,7 +16,38 @@
<v-container fluid>
<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="total > 0 && completed < 100">
Uploading {{current}} of {{total}}...
@ -25,38 +56,6 @@
<span v-else-if="completed === 100">Done.</span>
</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"
:indeterminate="indexing"></v-progress-linear>

View file

@ -369,6 +369,10 @@ export class Photo extends RestModel {
info.push(Util.duration(file.Duration));
}
if (file.Codec) {
info.push(file.Codec.toUpperCase());
}
this.addSizeInfo(file, info);
if (!info) {

View file

@ -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>

View file

@ -40,6 +40,19 @@
},
data() {
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 {
initialized: false,
@ -58,9 +71,7 @@
},
photos: [],
result: {},
filter: {
q: this.query(),
},
filter: filter,
lastFilter: {},
labels: {
search: this.$gettext("Search"),

View file

@ -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>

View file

@ -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>

View file

@ -2,15 +2,13 @@ import Photos from "pages/photos.vue";
import Albums from "pages/albums.vue";
import AlbumPhotos from "pages/album/photos.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 People from "pages/people.vue";
import Library from "pages/library.vue";
import Share from "pages/share.vue";
import Settings from "pages/settings.vue";
import Login from "pages/login.vue";
import Discover from "pages/discover.vue";
import Todo from "pages/todo.vue";
const c = window.__CONFIG__;
@ -134,10 +132,16 @@ export default [
},
{
name: "files",
path: "/files*",
path: "/library/files*",
component: Files,
meta: {title: "File Browser", auth: true},
},
{
name: "hidden",
path: "/library/hidden",
component: Photos,
props: {staticFilter: {hidden: true}},
},
{
name: "labels",
path: "/labels",
@ -157,12 +161,6 @@ export default [
component: People,
meta: {title: "People", auth: true},
},
{
name: "filters",
path: "/filters",
component: Todo,
meta: {title: "Filters", auth: true},
},
{
name: "library_logs",
path: "/library/logs",
@ -184,12 +182,6 @@ export default [
meta: {title: "Originals", auth: true, background: "application-light"},
props: {tab: 0},
},
{
name: "share",
path: "/share",
component: Share,
meta: {title: "Share with friends", auth: true},
},
{
name: "settings",
path: "/settings",

View file

@ -33,14 +33,45 @@ var UnknownCamera = Camera{
var CameraMakes = map[string]string{
"OLYMPUS OPTICAL CO.,LTD": "Olympus",
"samsung": "Samsung",
}
var CameraModels = map[string]string{
"ELE-L29": "P30",
"ELE-AL00": "P30",
"ELE-L04": "P30",
"ELE-L09": "P30",
"ELE-TL00": "P30",
"ELE-L29": "P30",
"ELE-AL00": "P30",
"ELE-L04": "P30",
"ELE-L09": "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

View file

@ -419,7 +419,18 @@ func (m *Photo) LoadPlace() error {
}
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.

View file

@ -102,7 +102,7 @@ func (m *Photo) UpdateLocation(geoApi string) (keywords []string, labels classif
if m.UnknownLocation() {
m.Location = &UnknownLocation
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.PlaceID = m.Location.PlaceID
}
@ -110,7 +110,7 @@ func (m *Photo) UpdateLocation(geoApi string) (keywords []string, labels classif
if m.UnknownPlace() {
m.Place = &UnknownPlace
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()
}

View file

@ -14,13 +14,16 @@ type GeoSearch struct {
Favorite bool `form:"favorite"`
Video bool `form:"video"`
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"`
Lng float32 `form:"lng"`
S2 string `form:"s2"`
Olc string `form:"olc"`
Dist uint `form:"dist"`
Quality int `form:"quality"`
Review bool `form:"review"`
Album string `form:"album"`
Country string `form:"country"`
Year int `form:"year"`

View file

@ -19,8 +19,13 @@ type PhotoSearch struct {
Video bool `form:"video"`
Photo bool `form:"photo"`
Duplicate bool `form:"duplicate"`
Archived bool `form:"archived"`
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"`
Lng float32 `form:"lng"`
Dist uint `form:"dist"`
@ -45,10 +50,6 @@ type PhotoSearch struct {
Lens int `form:"lens"`
Before time.Time `form:"before" 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:"-"`
Offset int `form:"offset" serialize:"-"`
Order string `form:"order" serialize:"-"`

View file

@ -49,8 +49,8 @@ type Data struct {
// AspectRatio returns the aspect ratio based on width and height.
func (data Data) AspectRatio() float32 {
width := float64(data.Width)
height := float64(data.Height)
width := float64(data.ActualWidth())
height := float64(data.ActualHeight())
if width <= 0 || height <= 0 {
return 0
@ -85,3 +85,21 @@ func (data Data) HasInstanceID() bool {
func (data Data) HasTimeAndPlace() bool {
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
}

View file

@ -122,7 +122,7 @@ func TestExif(t *testing.T) {
assert.Equal(t, "Apple", data.CameraMake)
assert.Equal(t, "iPhone 7", data.CameraModel)
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, "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, "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")
})
}

View file

@ -14,14 +14,14 @@ import (
)
// JSON parses a json sidecar file (as used by Exiftool) and returns a Data struct.
func JSON(fileName string) (data Data, err error) {
err = data.JSON(fileName)
func JSON(jsonName, originalName string) (data Data, err error) {
err = data.JSON(jsonName, originalName)
return data, err
}
// 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() {
if e := recover(); e != nil {
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)
}
jsonString, err := ioutil.ReadFile(fileName)
jsonString, err := ioutil.ReadFile(jsonName)
if err != nil {
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")
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()
@ -51,6 +51,10 @@ func (data *Data) JSON(fileName string) (err error) {
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()
// Iterate through all config fields
@ -132,10 +136,39 @@ func (data *Data) JSON(fileName string) (err error) {
}
}
// Fix rotation.
if data.Rotation == 90 || data.Rotation == 270 || data.Rotation == -90 {
data.Width, data.Height = data.Height, data.Width
data.Rotation = 0
if orientation, ok := data.All["Orientation"]; ok && orientation != "" {
switch orientation {
case "1", "Horizontal (normal)":
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.

View file

@ -8,7 +8,7 @@ import (
func TestJSON(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 {
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 15:20:14 +0000 UTC", data.TakenAt.String())
assert.Equal(t, "Europe/Berlin", data.TimeZone)
assert.Equal(t, 1080, data.Width)
assert.Equal(t, 1920, data.Height)
assert.Equal(t, 1920, data.Width)
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(13.4593), data.Lng)
assert.Equal(t, "Apple", data.CameraMake)
@ -31,7 +34,7 @@ func TestJSON(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 {
t.Fatal(err)
@ -46,6 +49,9 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "", data.TimeZone)
assert.Equal(t, 270, data.Width)
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.Lng)
assert.Equal(t, "", data.CameraMake)
@ -54,7 +60,7 @@ func TestJSON(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 {
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 12:16:48 +0000 UTC", data.TakenAt.String())
assert.Equal(t, "Europe/Berlin", data.TimeZone)
assert.Equal(t, 1080, data.Width)
assert.Equal(t, 1920, data.Height)
assert.Equal(t, 1920, data.Width)
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(13.3218), data.Lng)
assert.Equal(t, "", data.CameraMake)
@ -77,7 +87,7 @@ func TestJSON(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 {
t.Fatal(err)
@ -92,6 +102,7 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "Europe/Berlin", data.TimeZone)
assert.Equal(t, 1920, data.Width)
assert.Equal(t, 1080, data.Height)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, float32(52.4649), data.Lat)
assert.Equal(t, float32(13.3148), data.Lng)
assert.Equal(t, "", data.CameraMake)
@ -100,7 +111,7 @@ func TestJSON(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 {
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, 848, data.Width)
assert.Equal(t, 480, data.Height)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, "", data.Copyright)
assert.Equal(t, "", data.CameraMake)
assert.Equal(t, "", data.CameraModel)
@ -120,7 +132,7 @@ func TestJSON(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 {
t.Fatal(err)
@ -142,10 +154,11 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "HUAWEI", data.CameraMake)
assert.Equal(t, "ELE-L29", data.CameraModel)
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) {
data, err := JSON("testdata/canon_eos_6d.json")
data, err := JSON("testdata/canon_eos_6d.json", "")
if err != nil {
t.Fatal(err)
@ -161,10 +174,11 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "Canon", data.CameraMake)
assert.Equal(t, "Canon EOS 6D", data.CameraModel)
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) {
data, err := JSON("testdata/gps-2000.json")
data, err := JSON("testdata/gps-2000.json", "")
if err != nil {
t.Fatal(err)
@ -181,10 +195,11 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "", data.CameraModel)
assert.Equal(t, "", data.LensMake)
assert.Equal(t, "", data.LensModel)
assert.Equal(t, 1, data.Orientation)
})
t.Run("ladybug.json", func(t *testing.T) {
data, err := JSON("testdata/ladybug.json")
data, err := JSON("testdata/ladybug.json", "")
if err != nil {
t.Fatal(err)
@ -201,10 +216,11 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "", data.CameraModel)
assert.Equal(t, "", data.LensMake)
assert.Equal(t, "", data.LensModel)
assert.Equal(t, 1, data.Orientation)
})
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 {
t.Fatal(err)
@ -217,6 +233,7 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "", data.Artist)
assert.Equal(t, "", data.Description)
assert.Equal(t, "", data.Copyright)
assert.Equal(t, 6, data.Orientation)
assert.Equal(t, "Apple", data.CameraMake)
assert.Equal(t, "iPhone 7", data.CameraModel)
assert.Equal(t, "Apple", data.LensMake)
@ -224,7 +241,7 @@ func TestJSON(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 {
t.Fatal(err)
@ -241,6 +258,7 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "Europe/Berlin", data.TimeZone)
assert.Equal(t, 3024, data.Width)
assert.Equal(t, 4032, data.Height)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, float32(48.300003), data.Lat)
assert.Equal(t, float32(8.929067), data.Lng)
assert.Equal(t, "Apple", data.CameraMake)
@ -249,7 +267,7 @@ func TestJSON(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 {
t.Fatal(err)
@ -266,6 +284,7 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "Europe/Berlin", data.TimeZone)
assert.Equal(t, 1024, data.Width)
assert.Equal(t, 1365, data.Height)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, float32(48.300003), data.Lat)
assert.Equal(t, float32(8.929067), data.Lng)
assert.Equal(t, "Apple", data.CameraMake)
@ -274,7 +293,7 @@ func TestJSON(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 {
t.Fatal(err)
@ -291,10 +310,23 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "Europe/Berlin", data.TimeZone)
assert.Equal(t, 1125, data.Width)
assert.Equal(t, 1500, data.Height)
assert.Equal(t, 1, data.Orientation)
assert.Equal(t, float32(48.300003), data.Lat)
assert.Equal(t, float32(8.929067), data.Lng)
assert.Equal(t, "Apple", data.CameraMake)
assert.Equal(t, "iPhone SE", data.CameraModel)
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)
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

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
View 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
}]

View file

@ -226,8 +226,8 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
metaData = m.MetaData()
file.FileCodec = metaData.Codec
file.FileWidth = metaData.Width
file.FileHeight = metaData.Height
file.FileWidth = metaData.ActualWidth()
file.FileHeight = metaData.ActualHeight()
file.FileDuration = metaData.Duration
file.FileAspectRatio = metaData.AspectRatio()
file.FilePortrait = metaData.Portrait()

View file

@ -309,6 +309,11 @@ func (m *MediaFile) FileName() string {
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.
func (m *MediaFile) SetFileName(fileName string) {
m.fileName = fileName
@ -620,7 +625,7 @@ func (m *MediaFile) HasJson() bool {
}
func (m *MediaFile) decodeDimensions() error {
if !m.IsPhoto() {
if !m.IsMedia() {
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.
func (m *MediaFile) Width() int {
if !m.IsPhoto() {
if !m.IsMedia() {
return 0
}
@ -680,7 +685,7 @@ func (m *MediaFile) Width() int {
// Height returns the height dimension of a MediaFile.
func (m *MediaFile) Height() int {
if !m.IsPhoto() {
if !m.IsMedia() {
return 0
}

View file

@ -22,8 +22,8 @@ func (m *MediaFile) MetaData() (result meta.Data) {
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())))
} else if jsonErr := m.metaData.JSON(jsonFile); jsonErr != nil {
log.Warn(jsonErr)
} else if jsonErr := m.metaData.JSON(jsonFile, m.BaseName()); jsonErr != nil {
log.Debug(jsonErr)
} else {
err = nil
}

View file

@ -138,10 +138,23 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
s = s.Where("photos.photo_name LIKE ?", strings.ReplaceAll(f.Name, "*", "%"))
}
if f.Review {
s = s.Where("photos.photo_quality < 3")
} else if f.Quality != 0 {
s = s.Where("photos.photo_quality >= ?", f.Quality)
// Filter by status.
if f.Archived {
s = s.Where("photos.deleted_at IS NOT NULL")
} 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 {

View file

@ -126,7 +126,10 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error
}
// 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")
} else {
s = s.Where("photos.deleted_at IS NULL")