Added additional photo meta data like aperture, lens and camera make; smaller perceptive hash; implemented stub for single photo view

This commit is contained in:
Michael Mayer 2018-09-24 19:07:43 +02:00
parent 13426caba2
commit bbab05f9db
15 changed files with 456 additions and 62 deletions

View file

@ -33,6 +33,7 @@ Vue.use(Vuetify, {
success: '#00BFA5', success: '#00BFA5',
warning: '#FFD600', warning: '#FFD600',
delete: '#E57373', delete: '#E57373',
love: '#EF5350',
}, },
}); });

View file

@ -175,12 +175,17 @@
:key="photo.ID" :key="photo.ID"
xs12 sm6 md4 lg3 d-flex xs12 sm6 md4 lg3 d-flex
> >
<v-card tile class="ma-2"> <v-hover>
<v-card tile class="ma-2" slot-scope="{ hover }"
:class="photo.selected ? 'elevation-14' : 'elevation-2'">
<v-img <v-img
:src="photo.getThumbnailUrl('square', 500)" :src="photo.getThumbnailUrl('square', 500)"
aspect-ratio="1" aspect-ratio="1"
v-bind:class="{ selected: photo.selected }" v-bind:class="{ selected: photo.selected }"
@click="selectPhoto(photo)" style="cursor: pointer"
class="grey lighten-2"
@click="openPhoto(photo)"
> >
<v-layout <v-layout
slot="placeholder" slot="placeholder"
@ -191,26 +196,42 @@
> >
<v-progress-circular indeterminate color="grey lighten-5"></v-progress-circular> <v-progress-circular indeterminate color="grey lighten-5"></v-progress-circular>
</v-layout> </v-layout>
<v-btn v-if="hover || photo.selected" :flat="!hover" icon large absolute
:ripple="false" style="right: 4px; bottom: 4px;"
@click.stop.prevent="selectPhoto(photo)">
<v-icon v-if="photo.selected" color="white">check_box</v-icon>
<v-icon v-else color="white">check_box_outline_blank</v-icon>
</v-btn>
<v-btn v-if="hover || photo.PhotoFavorite" :flat="!hover" icon large absolute
:ripple="false" style="top: 4px; left: 4px"
@click.stop.prevent="likePhoto(photo)">
<v-icon v-if="photo.PhotoFavorite" color="white">favorite
</v-icon>
<v-icon v-else color="white">favorite_border</v-icon>
</v-btn>
</v-img> </v-img>
<v-card-title primary-title class="pa-3"> <v-card-title primary-title class="pa-3">
<div> <div>
<h3 class="subheading mb-0">{{ photo.PhotoTitle | truncate(50)}}</h3> <h3 class="subheading mb-2" :title="photo.PhotoTitle">{{ photo.PhotoTitle |
<div><v-icon small>date_range</v-icon> {{ photo.TakenAt | moment('DD/MM/YYYY hh:mm:ss') }} truncate(80) }}</h3>
<v-spacer></v-spacer> <div class="caption">
<v-icon small>photo_camera</v-icon> {{ photo.CameraModel }}<v-spacer></v-spacer> <v-icon size="14">date_range</v-icon>
<template v-if="photo.LocationID"><v-icon small>location_on</v-icon> {{ photo.LocName ? photo.LocName + ', ' : ''}}{{ photo.LocCity ? photo.LocCity + ', ' : ''}}{{ photo.LocCounty ? photo.LocCounty + ', ' : ''}}{{ photo.LocCountry }}</template> {{ photo.TakenAt | moment('DD/MM/YYYY hh:mm:ss') }}
<template v-if="photo.CountryName"><v-icon small>location_on</v-icon> {{ photo.CountryName }}</template> <br/>
<v-icon size="14">photo_camera</v-icon>
{{ photo.getCamera() }}
<br/>
<v-icon size="14">location_on</v-icon>
<span :title="photo.getFullLocation()">{{ photo.getLocation() }}</span>
</div> </div>
</div> </div>
</v-card-title> </v-card-title>
<!-- v-card-actions>
<v-btn flat color="orange">Like</v-btn>
<v-btn flat color="orange">Edit</v-btn>
</v-card-actions -->
</v-card> </v-card>
</v-hover>
</v-flex> </v-flex>
</v-layout> </v-layout>
</v-container> </v-container>
@ -231,14 +252,15 @@
:key="photo.ID" :key="photo.ID"
xs12 sm6 md3 lg2 d-flex xs12 sm6 md3 lg2 d-flex
v-bind:class="{ selected: photo.selected }" v-bind:class="{ selected: photo.selected }"
class="photo-tile"
> >
<v-tooltip bottom> <v-hover>
<v-card-actions flat tile class="d-flex" slot="activator" @click="selectPhoto(photo)" <v-card tile class="ma-2" slot-scope="{ hover }"
@mouseover="overPhoto(photo)" @mouseleave="leavePhoto(photo)"> :class="photo.selected ? 'elevation-14' : hover ? 'elevation-6' : 'elevation-2'">
<v-img :src="photo.getThumbnailUrl('square', 500)" <v-img :src="photo.getThumbnailUrl('square', 500)"
aspect-ratio="1" aspect-ratio="1"
class="grey lighten-2" class="grey lighten-2"
style="cursor: pointer"
@click="openPhoto(photo)"
> >
<v-layout <v-layout
slot="placeholder" slot="placeholder"
@ -247,12 +269,27 @@
justify-center justify-center
ma-0 ma-0
> >
<v-progress-circular indeterminate color="grey lighten-5"></v-progress-circular> <v-progress-circular indeterminate
color="grey lighten-5"></v-progress-circular>
</v-layout> </v-layout>
<v-btn v-if="hover || photo.selected" :flat="!hover" icon large absolute
:ripple="false" style="right: 4px; bottom: 4px;"
@click.stop.prevent="selectPhoto(photo)">
<v-icon v-if="photo.selected" color="white">check_box</v-icon>
<v-icon v-else color="white">check_box_outline_blank</v-icon>
</v-btn>
<v-btn v-if="hover || photo.PhotoFavorite" :flat="!hover" icon large absolute
:ripple="false" style="top: 4px; left: 4px"
@click.stop.prevent="likePhoto(photo)">
<v-icon v-if="photo.PhotoFavorite" color="white">favorite</v-icon>
<v-icon v-else color="white">favorite_border</v-icon>
</v-btn>
</v-img> </v-img>
</v-card-actions>
<span>{{ photo.PhotoTitle }}<br/>{{ photo.TakenAt | moment('DD/MM/YYYY') }} / {{ photo.CameraModel }}</span> </v-card>
</v-tooltip> </v-hover>
</v-flex> </v-flex>
</v-layout> </v-layout>
</v-container> </v-container>
@ -273,6 +310,35 @@
</v-btn> </v-btn>
</v-snackbar> </v-snackbar>
</v-container> </v-container>
<v-dialog v-model="viewDialog" fullscreen hide-overlay transition="dialog-bottom-transition">
<v-card v-if="viewDialogPhoto">
<v-img :src="viewDialogPhoto.getThumbnailUrl('fit', 500)"
:aspect-ratio="viewDialogPhoto.FileAspectRatio"
contain
class="black"
@click="closePhoto()"
style="cursor: pointer"
:max-width="window.width"
:max-height="window.height"
:srcset="viewDialogPhoto.getThumbnailSrcset()"
:sizes="viewDialogPhoto.getThumbnailSizes()"
>
<v-layout
slot="placeholder"
fill-height
align-center
justify-center
ma-0
>
<v-progress-circular indeterminate
color="grey lighten-5"></v-progress-circular>
</v-layout>
</v-img>
</v-card>
</v-dialog>
</div> </div>
</template> </template>
@ -299,6 +365,12 @@
'snackbarVisible': false, 'snackbarVisible': false,
'snackbarText': '', 'snackbarText': '',
'advandedSearch': false, 'advandedSearch': false,
'viewDialog': false,
'viewDialogPhoto': null,
'window': {
width: 0,
height: 0
},
'results': [], 'results': [],
'query': { 'query': {
view: view, view: view,
@ -347,7 +419,14 @@
'selected': [], 'selected': [],
}; };
}, },
destroyed() {
window.removeEventListener('resize', this.handleResize)
},
methods: { methods: {
handleResize() {
this.window.width = window.innerWidth;
this.window.height = window.innerHeight;
},
overPhoto(photo) { overPhoto(photo) {
}, },
@ -359,10 +438,22 @@
this.selected[i].selected = false; this.selected[i].selected = false;
} }
this.selected = []; this.selected = [];
this.snackbarText = ''; this.updateSnackbar();
},
updateSnackbar(text) {
if(!text) text = "";
this.snackbarText = text;
this.snackbarVisible = this.snackbarText !== "";
},
showSnackbar() {
this.snackbarVisible = this.snackbarText !== "";
},
hideSnackbar() {
this.snackbarVisible = false; this.snackbarVisible = false;
}, },
selectPhoto(photo) { selectPhoto(photo, ev) {
if (photo.selected) { if (photo.selected) {
for (let i = 0; i < this.selected.length; i++) { for (let i = 0; i < this.selected.length; i++) {
if (this.selected[i].id === photo.id) { if (this.selected[i].id === photo.id) {
@ -389,8 +480,19 @@
this.snackbarVisible = false; this.snackbarVisible = false;
} }
}, },
openPhoto(photo) {
this.$alert.success('Open photo' + photo.PhotoTitle);
this.viewDialogPhoto = photo;
this.viewDialog = true;
this.hideSnackbar();
},
closePhoto() {
this.viewDialogPhoto = null;
this.viewDialog = false;
this.showSnackbar();
},
likePhoto(photo) { likePhoto(photo) {
photo.Favorite = !photo.Favorite; photo.PhotoFavorite = !photo.PhotoFavorite;
}, },
deletePhoto(photo) { deletePhoto(photo) {
this.$alert.success('Photo deleted'); this.$alert.success('Photo deleted');
@ -459,7 +561,17 @@
}); });
} }
}, },
beforeRouteLeave(to, from, next) {
if (this.viewDialog) {
this.closePhoto()
next(false)
} else {
next()
}
},
created() { created() {
window.addEventListener('resize', this.handleResize);
this.handleResize();
this.refreshList(); this.refreshList();
}, },
}; };

View file

@ -17,6 +17,106 @@ class Photo extends Abstract {
return '/api/v1/thumbnails/' + type + '/' + size + '/' + this.FileHash; return '/api/v1/thumbnails/' + type + '/' + size + '/' + this.FileHash;
} }
getThumbnailSrcset() {
const result = [];
result.push(this.getThumbnailUrl('fit', 320) + ' 320w');
result.push(this.getThumbnailUrl('fit', 500) + ' 500w');
result.push(this.getThumbnailUrl('fit', 720) + ' 720w');
result.push(this.getThumbnailUrl('fit', 1280) + ' 1280w');
result.push(this.getThumbnailUrl('fit', 1920) + ' 1920w');
result.push(this.getThumbnailUrl('fit', 2560) + ' 2560w');
result.push(this.getThumbnailUrl('fit', 3840) + ' 3840w');
return result.join(', ');
}
getThumbnailSizes() {
const result = [];
result.push('(max-width: 320px) or (max-height: 320px) 320px');
result.push('(max-width: 500px) or (max-height: 500px) 500px');
result.push('(max-width: 720px) or (max-height: 720px) 720px');
result.push('(max-width: 1280px) or (max-height: 1280px) 1280px');
result.push('(max-width: 1920px) or (max-height: 1920px) 1920px');
result.push('(max-width: 2560px) or (max-height: 2560px) 2560px');
result.push('(min-width: 1920px) or (min-height: 1920px) 3840px');
return result.join(', ');
}
getLocation() {
const location = [];
if (this.LocationID) {
if (this.LocName && !this.LocCity && !this.LocCounty) {
location.push(this.LocName)
} else if (this.LocCity) {
location.push(this.LocCity)
} else if (this.LocCounty) {
location.push(this.LocCounty)
}
if (this.LocState && LocState !== LocCity) {
location.push(this.LocState)
}
if (this.LocCountry) {
location.push(this.LocCountry)
}
} else if (this.CountryName) {
location.push(this.CountryName)
} else {
location.push('Unknown')
}
return location.join(', ');
}
getFullLocation() {
const location = [];
if (this.LocationID) {
if (this.LocName) {
location.push(this.LocName)
}
if (this.LocCity) {
location.push(this.LocCity)
}
if (this.LocPostcode) {
location.push(this.LocPostcode)
}
if (this.LocCounty) {
location.push(this.LocCounty)
}
if (this.LocState) {
location.push(this.LocState)
}
if (this.LocCountry) {
location.push(this.LocCountry)
}
} else if (this.CountryName) {
location.push(this.CountryName)
} else {
location.push('Unknown')
}
return location.join(', ');
}
getCamera() {
if (this.CameraModel) {
return this.CameraModel
}
return 'Unknown'
}
static getCollectionResource() { static getCollectionResource() {
return 'photos'; return 'photos';
} }

View file

@ -9,13 +9,14 @@ type Camera struct {
gorm.Model gorm.Model
CameraSlug string CameraSlug string
CameraModel string CameraModel string
CameraMake string
CameraType string CameraType string
CameraOwner string CameraOwner string
CameraDescription string `gorm:"type:text;"` CameraDescription string `gorm:"type:text;"`
CameraNotes string `gorm:"type:text;"` CameraNotes string `gorm:"type:text;"`
} }
func NewCamera(modelName string) *Camera { func NewCamera(modelName string, makeName string) *Camera {
if modelName == "" { if modelName == "" {
modelName = "Unknown" modelName = "Unknown"
} }
@ -24,6 +25,7 @@ func NewCamera(modelName string) *Camera {
result := &Camera{ result := &Camera{
CameraModel: modelName, CameraModel: modelName,
CameraMake: makeName,
CameraSlug: cameraSlug, CameraSlug: cameraSlug,
} }
@ -31,7 +33,7 @@ func NewCamera(modelName string) *Camera {
} }
func (c *Camera) FirstOrCreate(db *gorm.DB) *Camera { func (c *Camera) FirstOrCreate(db *gorm.DB) *Camera {
db.FirstOrCreate(c, "camera_model = ?", c.CameraModel) db.FirstOrCreate(c, "camera_model = ? AND camera_make = ?", c.CameraModel, c.CameraMake)
return c return c
} }

43
internal/models/lens.go Normal file
View file

@ -0,0 +1,43 @@
package models
import (
"github.com/gosimple/slug"
"github.com/jinzhu/gorm"
)
type Lens struct {
gorm.Model
LensSlug string
LensModel string
LensMake string
LensType string
LensOwner string
LensDescription string `gorm:"type:text;"`
LensNotes string `gorm:"type:text;"`
}
func (Lens) TableName() string {
return "lenses"
}
func NewLens(modelName string, makeName string) *Lens {
if modelName == "" {
modelName = "Unknown"
}
lensSlug := slug.MakeLang(modelName, "en")
result := &Lens{
LensModel: modelName,
LensMake: makeName,
LensSlug: lensSlug,
}
return result
}
func (c *Lens) FirstOrCreate(db *gorm.DB) *Lens {
db.FirstOrCreate(c, "lens_model = ? AND lens_make = ?", c.LensModel, c.LensMake)
return c
}

View file

@ -9,6 +9,7 @@ type Photo struct {
gorm.Model gorm.Model
TakenAt time.Time TakenAt time.Time
PhotoTitle string PhotoTitle string
PhotoTitleChanged bool
PhotoDescription string `gorm:"type:text;"` PhotoDescription string `gorm:"type:text;"`
PhotoNotes string `gorm:"type:text;"` PhotoNotes string `gorm:"type:text;"`
PhotoArtist string PhotoArtist string
@ -20,13 +21,19 @@ type Photo struct {
PhotoFavorite bool PhotoFavorite bool
PhotoLat float64 PhotoLat float64
PhotoLong float64 PhotoLong float64
PhotoFocalLength float64
PhotoAperture float64
Camera *Camera
CameraID uint
Lens *Lens
LensID uint
Country *Country Country *Country
CountryID string CountryID string
CountryChanged bool
Location *Location Location *Location
LocationID uint LocationID uint
LocationChanged bool
Tags []*Tag `gorm:"many2many:photo_tags;"` Tags []*Tag `gorm:"many2many:photo_tags;"`
Files []*File Files []*File
Albums []*Album `gorm:"many2many:album_photos;"` Albums []*Album `gorm:"many2many:album_photos;"`
Camera *Camera
CameraID uint
} }

View file

@ -220,7 +220,7 @@ func (c *Config) GetDb() *gorm.DB {
func (c *Config) MigrateDb() { func (c *Config) MigrateDb() {
db := c.GetDb() db := c.GetDb()
db.AutoMigrate(&File{}, &Photo{}, &Tag{}, &Album{}, &Location{}, &Camera{}, &Country{}) db.AutoMigrate(&File{}, &Photo{}, &Tag{}, &Album{}, &Location{}, &Camera{}, &Lens{}, &Country{})
if !db.Dialect().HasIndex("photos", "photos_fulltext") { if !db.Dialect().HasIndex("photos", "photos_fulltext") {
db.Exec("CREATE FULLTEXT INDEX photos_fulltext ON photos (photo_title, photo_description, photo_artist, photo_colors)") db.Exec("CREATE FULLTEXT INDEX photos_fulltext ON photos (photo_title, photo_description, photo_artist, photo_colors)")

View file

@ -0,0 +1,3 @@
package photoprism
const PerceptualHashSize = 4

View file

@ -4,6 +4,7 @@ import (
"errors" "errors"
"github.com/rwcarlsen/goexif/exif" "github.com/rwcarlsen/goexif/exif"
"github.com/rwcarlsen/goexif/mknote" "github.com/rwcarlsen/goexif/mknote"
"math"
"strings" "strings"
"time" "time"
) )
@ -11,7 +12,12 @@ import (
type ExifData struct { type ExifData struct {
DateTime time.Time DateTime time.Time
Artist string Artist string
CameraMake string
CameraModel string CameraModel string
LensMake string
LensModel string
Aperture float64
FocalLength float64
UniqueID string UniqueID string
Lat float64 Lat float64
Long float64 Long float64
@ -60,6 +66,42 @@ func (m *MediaFile) GetExifData() (*ExifData, error) {
m.exifData.CameraModel = strings.Replace(camModel.String(), "\"", "", -1) m.exifData.CameraModel = strings.Replace(camModel.String(), "\"", "", -1)
} }
if camMake, err := x.Get(exif.Make); err == nil {
m.exifData.CameraMake = strings.Replace(camMake.String(), "\"", "", -1)
}
if lensMake, err := x.Get(exif.LensMake); err == nil {
m.exifData.LensMake = strings.Replace(lensMake.String(), "\"", "", -1)
}
if lensModel, err := x.Get(exif.LensModel); err == nil {
m.exifData.LensModel = strings.Replace(lensModel.String(), "\"", "", -1)
}
if aperture, err := x.Get(exif.ApertureValue); err == nil {
number, denom, _ := aperture.Rat2(0)
if denom == 0 {
denom = 1
}
value := float64(number) / float64(denom)
m.exifData.Aperture = math.Round(value * 1000) / 1000
}
if focal, err := x.Get(exif.FocalLength); err == nil {
number, denom, _ := focal.Rat2(0)
if denom == 0 {
denom = 1
}
value := float64(number) / float64(denom)
m.exifData.FocalLength = math.Round(value * 1000) / 1000
}
if tm, err := x.DateTime(); err == nil { if tm, err := x.DateTime(); err == nil {
m.exifData.DateTime = tm m.exifData.DateTime = tm
} }

View file

@ -55,5 +55,5 @@ func TestImporter_GetDestinationFilename(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, conf.OriginalsPath+"/2018/02/20180204_170813_B0770443A5F7.cr2", filename) assert.Equal(t, conf.OriginalsPath+"/2018/02/20180204_170813_863A6248DCCA.cr2", filename)
} }

View file

@ -115,7 +115,11 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
tags = i.appendTag(tags, location.LocType) tags = i.appendTag(tags, location.LocType)
if photo.PhotoTitle == "" && location.LocName != "" { // TODO: User defined title format if photo.PhotoTitle == "" && location.LocName != "" { // TODO: User defined title format
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocName, location.LocCountry, mediaFile.GetDateCreated().Format("2006")) if len(location.LocName) > 40 {
photo.PhotoTitle = fmt.Sprintf("%s / %s", strings.Title(location.LocName), mediaFile.GetDateCreated().Format("2006"))
} else {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", strings.Title(location.LocName), location.LocCity, mediaFile.GetDateCreated().Format("2006"))
}
} else if photo.PhotoTitle == "" && location.LocCity != "" { } else if photo.PhotoTitle == "" && location.LocCity != "" {
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCity, location.LocCountry, mediaFile.GetDateCreated().Format("2006")) photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCity, location.LocCountry, mediaFile.GetDateCreated().Format("2006"))
} else if photo.PhotoTitle == "" && location.LocCounty != "" { } else if photo.PhotoTitle == "" && location.LocCounty != "" {
@ -131,17 +135,23 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
} }
} }
photo.Tags = tags
if photo.PhotoTitle == "" { if photo.PhotoTitle == "" {
if len(photo.Tags) > 0 { // TODO: User defined title format if len(photo.Tags) > 0 { // TODO: User defined title format
photo.PhotoTitle = fmt.Sprintf("%s / %s", strings.Title(photo.Tags[0].TagLabel), mediaFile.GetDateCreated().Format("2006")) photo.PhotoTitle = fmt.Sprintf("%s / %s", strings.Title(photo.Tags[0].TagLabel), mediaFile.GetDateCreated().Format("2006"))
} else if photo.Country != nil && photo.Country.CountryName != "" {
photo.PhotoTitle = fmt.Sprintf("%s / %s", strings.Title(photo.Country.CountryName), mediaFile.GetDateCreated().Format("2006"))
} else { } else {
photo.PhotoTitle = fmt.Sprintf("Unknown / %s", mediaFile.GetDateCreated().Format("2006")) photo.PhotoTitle = fmt.Sprintf("Unknown / %s", mediaFile.GetDateCreated().Format("2006"))
} }
} }
photo.Tags = tags photo.Camera = NewCamera(mediaFile.GetCameraModel(), mediaFile.GetCameraMake()).FirstOrCreate(i.db)
photo.Lens = NewLens(mediaFile.GetLensModel(), mediaFile.GetLensMake()).FirstOrCreate(i.db)
photo.PhotoFocalLength = mediaFile.GetFocalLength()
photo.PhotoAperture = mediaFile.GetAperture()
photo.Camera = NewCamera(mediaFile.GetCameraModel()).FirstOrCreate(i.db)
photo.TakenAt = mediaFile.GetDateCreated() photo.TakenAt = mediaFile.GetDateCreated()
photo.PhotoCanonicalName = canonicalName photo.PhotoCanonicalName = canonicalName
photo.PhotoFavorite = false photo.PhotoFavorite = false
@ -160,9 +170,14 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
colorNames, photo.PhotoVibrantColor, photo.PhotoMutedColor = jpeg.GetColors() colorNames, photo.PhotoVibrantColor, photo.PhotoMutedColor = jpeg.GetColors()
photo.PhotoColors = strings.Join(colorNames, ", ") photo.PhotoColors = strings.Join(colorNames, ", ")
photo.Camera = NewCamera(mediaFile.GetCameraModel(), mediaFile.GetCameraMake()).FirstOrCreate(i.db)
photo.Lens = NewLens(mediaFile.GetLensModel(), mediaFile.GetLensMake()).FirstOrCreate(i.db)
photo.PhotoFocalLength = mediaFile.GetFocalLength()
photo.PhotoAperture = mediaFile.GetAperture()
} }
if photo.CountryID == "" { if photo.LocationID == 0 {
var recentPhoto Photo var recentPhoto Photo
if result := i.db.Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", photo.TakenAt)).Preload("Country").First(&recentPhoto); result.Error == nil { if result := i.db.Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", photo.TakenAt)).Preload("Country").First(&recentPhoto); result.Error == nil {

View file

@ -163,6 +163,66 @@ func (m *MediaFile) GetCameraModel() string {
return result return result
} }
func (m *MediaFile) GetCameraMake() string {
info, err := m.GetExifData()
var result string
if err == nil {
result = info.CameraMake
}
return result
}
func (m *MediaFile) GetLensModel() string {
info, err := m.GetExifData()
var result string
if err == nil {
result = info.LensModel
}
return result
}
func (m *MediaFile) GetLensMake() string {
info, err := m.GetExifData()
var result string
if err == nil {
result = info.LensMake
}
return result
}
func (m *MediaFile) GetFocalLength() float64 {
info, err := m.GetExifData()
var result float64
if err == nil {
result = info.FocalLength
}
return result
}
func (m *MediaFile) GetAperture() float64 {
info, err := m.GetExifData()
var result float64
if err == nil {
result = info.Aperture
}
return result
}
func (m *MediaFile) GetCanonicalName() string { func (m *MediaFile) GetCanonicalName() string {
var postfix string var postfix string
@ -198,7 +258,7 @@ func (m *MediaFile) GetPerceptualHash() (string, error) {
return m.perceptualHash, nil return m.perceptualHash, nil
} }
hasher := ish.NewDifferenceHash(8, 8) hasher := ish.NewDifferenceHash(PerceptualHashSize, PerceptualHashSize)
img, _, err := ish.LoadFile(m.GetFilename()) img, _, err := ish.LoadFile(m.GetFilename())
if err != nil { if err != nil {

View file

@ -85,27 +85,27 @@ func TestMediaFile_GetPerceptiveHash(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
hash1, _ := mediaFile1.GetPerceptualHash() hash1, _ := mediaFile1.GetPerceptualHash()
assert.Equal(t, "66debc383325d3bd", hash1) assert.Equal(t, "ef95", hash1)
mediaFile2, err := NewMediaFile(conf.ImportPath + "/20130203_193332_0AE340D280_V2.jpg") mediaFile2, err := NewMediaFile(conf.ImportPath + "/20130203_193332_0AE340D280_V2.jpg")
assert.Nil(t, err) assert.Nil(t, err)
hash2, _ := mediaFile2.GetPerceptualHash() hash2, _ := mediaFile2.GetPerceptualHash()
assert.Equal(t, "e6debc393325c3b9", hash2) assert.Equal(t, "6f95", hash2)
distance, _ := mediaFile1.GetPerceptualDistance(hash2) distance, _ := mediaFile1.GetPerceptualDistance(hash2)
assert.Equal(t, 4, distance) assert.Equal(t, 1, distance)
mediaFile3, err := NewMediaFile(conf.ImportPath + "/iphone/IMG_6788.JPG") mediaFile3, err := NewMediaFile(conf.ImportPath + "/iphone/IMG_6788.JPG")
assert.Nil(t, err) assert.Nil(t, err)
hash3, _ := mediaFile3.GetPerceptualHash() hash3, _ := mediaFile3.GetPerceptualHash()
assert.Equal(t, "f1e2858b171d3e78", hash3) assert.Equal(t, "ad73", hash3)
distance, _ = mediaFile1.GetPerceptualDistance(hash3) distance, _ = mediaFile1.GetPerceptualDistance(hash3)
assert.Equal(t, 33, distance) assert.Equal(t, 7, distance)
} }
func TestMediaFile_GetMimeType(t *testing.T) { func TestMediaFile_GetMimeType(t *testing.T) {

View file

@ -7,6 +7,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"net/http" "net/http"
"strconv" "strconv"
"strings"
) )
type OpenstreetmapAddress struct { type OpenstreetmapAddress struct {
@ -74,7 +75,7 @@ func (m *MediaFile) GetLocation() (*Location, error) {
location.LocLong = lon location.LocLong = lon
} }
location.LocName = openstreetmapLocation.Name location.LocName = strings.Title(openstreetmapLocation.Name)
location.LocPostcode = openstreetmapLocation.Address.Postcode location.LocPostcode = openstreetmapLocation.Address.Postcode
location.LocCounty = openstreetmapLocation.Address.County location.LocCounty = openstreetmapLocation.Address.County
location.LocState = openstreetmapLocation.Address.State location.LocState = openstreetmapLocation.Address.State

View file

@ -40,6 +40,12 @@ type PhotoSearchResult struct {
// Camera // Camera
CameraID uint CameraID uint
CameraModel string CameraModel string
CameraMake string
// Lens
LensID uint
LensModel string
LensMake string
// Country // Country
CountryID string CountryID string
@ -87,12 +93,14 @@ func (s *Search) Photos(form PhotoSearchForm) ([]PhotoSearchResult, error) {
q = q.Table("photos"). q = q.Table("photos").
Select(`SQL_CALC_FOUND_ROWS photos.*, Select(`SQL_CALC_FOUND_ROWS photos.*,
files.id AS file_id, files.file_name, files.file_hash, files.file_type, files.file_mime, files.file_width, files.file_height, files.file_aspect_ratio, files.file_orientation, files.id AS file_id, files.file_name, files.file_hash, files.file_type, files.file_mime, files.file_width, files.file_height, files.file_aspect_ratio, files.file_orientation,
cameras.camera_model, cameras.camera_make, cameras.camera_model,
lenses.lens_make, lenses.lens_model,
countries.country_name, countries.country_name,
locations.loc_display_name, locations.loc_name, locations.loc_city, locations.loc_postcode, locations.loc_country, locations.loc_country_code, locations.loc_category, locations.loc_type, locations.loc_display_name, locations.loc_name, locations.loc_city, locations.loc_postcode, locations.loc_county, locations.loc_state, locations.loc_country, locations.loc_country_code, locations.loc_category, locations.loc_type,
GROUP_CONCAT(tags.tag_label) AS tags`). GROUP_CONCAT(tags.tag_label) AS tags`).
Joins("JOIN files ON files.photo_id = photos.id AND files.file_primary AND files.deleted_at IS NULL"). Joins("JOIN files ON files.photo_id = photos.id AND files.file_primary AND files.deleted_at IS NULL").
Joins("JOIN cameras ON cameras.id = photos.camera_id"). Joins("JOIN cameras ON cameras.id = photos.camera_id").
Joins("JOIN lenses ON lenses.id = photos.lens_id").
Joins("LEFT JOIN countries ON countries.id = photos.country_id"). Joins("LEFT JOIN countries ON countries.id = photos.country_id").
Joins("LEFT JOIN locations ON locations.id = photos.location_id"). Joins("LEFT JOIN locations ON locations.id = photos.location_id").
Joins("LEFT JOIN photo_tags ON photo_tags.photo_id = photos.id"). Joins("LEFT JOIN photo_tags ON photo_tags.photo_id = photos.id").