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:
parent
13426caba2
commit
bbab05f9db
|
@ -33,6 +33,7 @@ Vue.use(Vuetify, {
|
|||
success: '#00BFA5',
|
||||
warning: '#FFD600',
|
||||
delete: '#E57373',
|
||||
love: '#EF5350',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -175,42 +175,63 @@
|
|||
:key="photo.ID"
|
||||
xs12 sm6 md4 lg3 d-flex
|
||||
>
|
||||
<v-card tile class="ma-2">
|
||||
<v-img
|
||||
:src="photo.getThumbnailUrl('square', 500)"
|
||||
aspect-ratio="1"
|
||||
v-bind:class="{ selected: photo.selected }"
|
||||
@click="selectPhoto(photo)"
|
||||
>
|
||||
<v-layout
|
||||
slot="placeholder"
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
ma-0
|
||||
<v-hover>
|
||||
<v-card tile class="ma-2" slot-scope="{ hover }"
|
||||
:class="photo.selected ? 'elevation-14' : 'elevation-2'">
|
||||
<v-img
|
||||
:src="photo.getThumbnailUrl('square', 500)"
|
||||
aspect-ratio="1"
|
||||
v-bind:class="{ selected: photo.selected }"
|
||||
style="cursor: pointer"
|
||||
class="grey lighten-2"
|
||||
@click="openPhoto(photo)"
|
||||
|
||||
>
|
||||
<v-progress-circular indeterminate color="grey lighten-5"></v-progress-circular>
|
||||
</v-layout>
|
||||
</v-img>
|
||||
<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-card-title primary-title class="pa-3">
|
||||
<div>
|
||||
<h3 class="subheading mb-0">{{ photo.PhotoTitle | truncate(50)}}</h3>
|
||||
<div><v-icon small>date_range</v-icon> {{ photo.TakenAt | moment('DD/MM/YYYY hh:mm:ss') }}
|
||||
<v-spacer></v-spacer>
|
||||
<v-icon small>photo_camera</v-icon> {{ photo.CameraModel }}<v-spacer></v-spacer>
|
||||
<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>
|
||||
<template v-if="photo.CountryName"><v-icon small>location_on</v-icon> {{ photo.CountryName }}</template>
|
||||
<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-card-title primary-title class="pa-3">
|
||||
<div>
|
||||
<h3 class="subheading mb-2" :title="photo.PhotoTitle">{{ photo.PhotoTitle |
|
||||
truncate(80) }}</h3>
|
||||
<div class="caption">
|
||||
<v-icon size="14">date_range</v-icon>
|
||||
{{ photo.TakenAt | moment('DD/MM/YYYY hh:mm:ss') }}
|
||||
<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>
|
||||
</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-title>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
|
@ -231,14 +252,15 @@
|
|||
:key="photo.ID"
|
||||
xs12 sm6 md3 lg2 d-flex
|
||||
v-bind:class="{ selected: photo.selected }"
|
||||
class="photo-tile"
|
||||
>
|
||||
<v-tooltip bottom>
|
||||
<v-card-actions flat tile class="d-flex" slot="activator" @click="selectPhoto(photo)"
|
||||
@mouseover="overPhoto(photo)" @mouseleave="leavePhoto(photo)">
|
||||
<v-hover>
|
||||
<v-card tile class="ma-2" slot-scope="{ hover }"
|
||||
:class="photo.selected ? 'elevation-14' : hover ? 'elevation-6' : 'elevation-2'">
|
||||
<v-img :src="photo.getThumbnailUrl('square', 500)"
|
||||
aspect-ratio="1"
|
||||
class="grey lighten-2"
|
||||
style="cursor: pointer"
|
||||
@click="openPhoto(photo)"
|
||||
>
|
||||
<v-layout
|
||||
slot="placeholder"
|
||||
|
@ -247,12 +269,27 @@
|
|||
justify-center
|
||||
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-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-card-actions>
|
||||
<span>{{ photo.PhotoTitle }}<br/>{{ photo.TakenAt | moment('DD/MM/YYYY') }} / {{ photo.CameraModel }}</span>
|
||||
</v-tooltip>
|
||||
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
|
@ -273,6 +310,35 @@
|
|||
</v-btn>
|
||||
</v-snackbar>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
@ -299,6 +365,12 @@
|
|||
'snackbarVisible': false,
|
||||
'snackbarText': '',
|
||||
'advandedSearch': false,
|
||||
'viewDialog': false,
|
||||
'viewDialogPhoto': null,
|
||||
'window': {
|
||||
width: 0,
|
||||
height: 0
|
||||
},
|
||||
'results': [],
|
||||
'query': {
|
||||
view: view,
|
||||
|
@ -347,7 +419,14 @@
|
|||
'selected': [],
|
||||
};
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
},
|
||||
methods: {
|
||||
handleResize() {
|
||||
this.window.width = window.innerWidth;
|
||||
this.window.height = window.innerHeight;
|
||||
},
|
||||
overPhoto(photo) {
|
||||
|
||||
},
|
||||
|
@ -359,10 +438,22 @@
|
|||
this.selected[i].selected = false;
|
||||
}
|
||||
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;
|
||||
},
|
||||
selectPhoto(photo) {
|
||||
selectPhoto(photo, ev) {
|
||||
if (photo.selected) {
|
||||
for (let i = 0; i < this.selected.length; i++) {
|
||||
if (this.selected[i].id === photo.id) {
|
||||
|
@ -389,8 +480,19 @@
|
|||
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) {
|
||||
photo.Favorite = !photo.Favorite;
|
||||
photo.PhotoFavorite = !photo.PhotoFavorite;
|
||||
},
|
||||
deletePhoto(photo) {
|
||||
this.$alert.success('Photo deleted');
|
||||
|
@ -459,7 +561,17 @@
|
|||
});
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (this.viewDialog) {
|
||||
this.closePhoto()
|
||||
next(false)
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
this.refreshList();
|
||||
},
|
||||
};
|
||||
|
|
|
@ -17,6 +17,106 @@ class Photo extends Abstract {
|
|||
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() {
|
||||
return 'photos';
|
||||
}
|
||||
|
|
|
@ -9,13 +9,14 @@ type Camera struct {
|
|||
gorm.Model
|
||||
CameraSlug string
|
||||
CameraModel string
|
||||
CameraMake string
|
||||
CameraType string
|
||||
CameraOwner string
|
||||
CameraDescription string `gorm:"type:text;"`
|
||||
CameraNotes string `gorm:"type:text;"`
|
||||
}
|
||||
|
||||
func NewCamera(modelName string) *Camera {
|
||||
func NewCamera(modelName string, makeName string) *Camera {
|
||||
if modelName == "" {
|
||||
modelName = "Unknown"
|
||||
}
|
||||
|
@ -24,6 +25,7 @@ func NewCamera(modelName string) *Camera {
|
|||
|
||||
result := &Camera{
|
||||
CameraModel: modelName,
|
||||
CameraMake: makeName,
|
||||
CameraSlug: cameraSlug,
|
||||
}
|
||||
|
||||
|
@ -31,7 +33,7 @@ func NewCamera(modelName string) *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
|
||||
}
|
||||
|
|
43
internal/models/lens.go
Normal file
43
internal/models/lens.go
Normal 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
|
||||
}
|
|
@ -9,6 +9,7 @@ type Photo struct {
|
|||
gorm.Model
|
||||
TakenAt time.Time
|
||||
PhotoTitle string
|
||||
PhotoTitleChanged bool
|
||||
PhotoDescription string `gorm:"type:text;"`
|
||||
PhotoNotes string `gorm:"type:text;"`
|
||||
PhotoArtist string
|
||||
|
@ -20,13 +21,19 @@ type Photo struct {
|
|||
PhotoFavorite bool
|
||||
PhotoLat float64
|
||||
PhotoLong float64
|
||||
PhotoFocalLength float64
|
||||
PhotoAperture float64
|
||||
Camera *Camera
|
||||
CameraID uint
|
||||
Lens *Lens
|
||||
LensID uint
|
||||
Country *Country
|
||||
CountryID string
|
||||
CountryChanged bool
|
||||
Location *Location
|
||||
LocationID uint
|
||||
LocationChanged bool
|
||||
Tags []*Tag `gorm:"many2many:photo_tags;"`
|
||||
Files []*File
|
||||
Albums []*Album `gorm:"many2many:album_photos;"`
|
||||
Camera *Camera
|
||||
CameraID uint
|
||||
}
|
||||
|
|
|
@ -220,7 +220,7 @@ func (c *Config) GetDb() *gorm.DB {
|
|||
func (c *Config) MigrateDb() {
|
||||
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") {
|
||||
db.Exec("CREATE FULLTEXT INDEX photos_fulltext ON photos (photo_title, photo_description, photo_artist, photo_colors)")
|
||||
|
|
3
internal/photoprism/const.go
Normal file
3
internal/photoprism/const.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package photoprism
|
||||
|
||||
const PerceptualHashSize = 4
|
|
@ -4,6 +4,7 @@ import (
|
|||
"errors"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
"github.com/rwcarlsen/goexif/mknote"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
@ -11,7 +12,12 @@ import (
|
|||
type ExifData struct {
|
||||
DateTime time.Time
|
||||
Artist string
|
||||
CameraMake string
|
||||
CameraModel string
|
||||
LensMake string
|
||||
LensModel string
|
||||
Aperture float64
|
||||
FocalLength float64
|
||||
UniqueID string
|
||||
Lat float64
|
||||
Long float64
|
||||
|
@ -60,6 +66,42 @@ func (m *MediaFile) GetExifData() (*ExifData, error) {
|
|||
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 {
|
||||
m.exifData.DateTime = tm
|
||||
}
|
||||
|
|
|
@ -55,5 +55,5 @@ func TestImporter_GetDestinationFilename(t *testing.T) {
|
|||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -115,7 +115,11 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
|
|||
tags = i.appendTag(tags, location.LocType)
|
||||
|
||||
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 != "" {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCity, location.LocCountry, mediaFile.GetDateCreated().Format("2006"))
|
||||
} else if photo.PhotoTitle == "" && location.LocCounty != "" {
|
||||
|
@ -131,17 +135,23 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
|
|||
}
|
||||
}
|
||||
|
||||
photo.Tags = tags
|
||||
|
||||
if photo.PhotoTitle == "" {
|
||||
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"))
|
||||
} else if photo.Country != nil && photo.Country.CountryName != "" {
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s", strings.Title(photo.Country.CountryName), mediaFile.GetDateCreated().Format("2006"))
|
||||
} else {
|
||||
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.PhotoCanonicalName = canonicalName
|
||||
photo.PhotoFavorite = false
|
||||
|
@ -160,9 +170,14 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
|
|||
colorNames, photo.PhotoVibrantColor, photo.PhotoMutedColor = jpeg.GetColors()
|
||||
|
||||
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
|
||||
|
||||
if result := i.db.Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", photo.TakenAt)).Preload("Country").First(&recentPhoto); result.Error == nil {
|
||||
|
|
|
@ -163,6 +163,66 @@ func (m *MediaFile) GetCameraModel() string {
|
|||
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 {
|
||||
var postfix string
|
||||
|
||||
|
@ -198,7 +258,7 @@ func (m *MediaFile) GetPerceptualHash() (string, error) {
|
|||
return m.perceptualHash, nil
|
||||
}
|
||||
|
||||
hasher := ish.NewDifferenceHash(8, 8)
|
||||
hasher := ish.NewDifferenceHash(PerceptualHashSize, PerceptualHashSize)
|
||||
img, _, err := ish.LoadFile(m.GetFilename())
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -85,27 +85,27 @@ func TestMediaFile_GetPerceptiveHash(t *testing.T) {
|
|||
assert.Nil(t, err)
|
||||
hash1, _ := mediaFile1.GetPerceptualHash()
|
||||
|
||||
assert.Equal(t, "66debc383325d3bd", hash1)
|
||||
assert.Equal(t, "ef95", hash1)
|
||||
|
||||
mediaFile2, err := NewMediaFile(conf.ImportPath + "/20130203_193332_0AE340D280_V2.jpg")
|
||||
assert.Nil(t, err)
|
||||
hash2, _ := mediaFile2.GetPerceptualHash()
|
||||
|
||||
assert.Equal(t, "e6debc393325c3b9", hash2)
|
||||
assert.Equal(t, "6f95", hash2)
|
||||
|
||||
distance, _ := mediaFile1.GetPerceptualDistance(hash2)
|
||||
|
||||
assert.Equal(t, 4, distance)
|
||||
assert.Equal(t, 1, distance)
|
||||
|
||||
mediaFile3, err := NewMediaFile(conf.ImportPath + "/iphone/IMG_6788.JPG")
|
||||
assert.Nil(t, err)
|
||||
hash3, _ := mediaFile3.GetPerceptualHash()
|
||||
|
||||
assert.Equal(t, "f1e2858b171d3e78", hash3)
|
||||
assert.Equal(t, "ad73", hash3)
|
||||
|
||||
distance, _ = mediaFile1.GetPerceptualDistance(hash3)
|
||||
|
||||
assert.Equal(t, 33, distance)
|
||||
assert.Equal(t, 7, distance)
|
||||
}
|
||||
|
||||
func TestMediaFile_GetMimeType(t *testing.T) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OpenstreetmapAddress struct {
|
||||
|
@ -74,7 +75,7 @@ func (m *MediaFile) GetLocation() (*Location, error) {
|
|||
location.LocLong = lon
|
||||
}
|
||||
|
||||
location.LocName = openstreetmapLocation.Name
|
||||
location.LocName = strings.Title(openstreetmapLocation.Name)
|
||||
location.LocPostcode = openstreetmapLocation.Address.Postcode
|
||||
location.LocCounty = openstreetmapLocation.Address.County
|
||||
location.LocState = openstreetmapLocation.Address.State
|
||||
|
|
|
@ -40,6 +40,12 @@ type PhotoSearchResult struct {
|
|||
// Camera
|
||||
CameraID uint
|
||||
CameraModel string
|
||||
CameraMake string
|
||||
|
||||
// Lens
|
||||
LensID uint
|
||||
LensModel string
|
||||
LensMake string
|
||||
|
||||
// Country
|
||||
CountryID string
|
||||
|
@ -87,12 +93,14 @@ func (s *Search) Photos(form PhotoSearchForm) ([]PhotoSearchResult, error) {
|
|||
q = q.Table("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,
|
||||
cameras.camera_model,
|
||||
cameras.camera_make, cameras.camera_model,
|
||||
lenses.lens_make, lenses.lens_model,
|
||||
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`).
|
||||
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 lenses ON lenses.id = photos.lens_id").
|
||||
Joins("LEFT JOIN countries ON countries.id = photos.country_id").
|
||||
Joins("LEFT JOIN locations ON locations.id = photos.location_id").
|
||||
Joins("LEFT JOIN photo_tags ON photo_tags.photo_id = photos.id").
|
||||
|
|
Loading…
Reference in a new issue