mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
Improved photo search
This commit is contained in:
parent
6a6017a478
commit
cea3d70835
15 changed files with 303 additions and 85 deletions
|
|
@ -133,5 +133,5 @@ func (c *Config) GetDb() *gorm.DB {
|
|||
func (c *Config) MigrateDb() {
|
||||
db := c.GetDb()
|
||||
|
||||
db.AutoMigrate(&File{}, &Photo{}, &Tag{}, &Album{})
|
||||
db.AutoMigrate(&File{}, &Photo{}, &Tag{}, &Album{}, &Location{})
|
||||
}
|
||||
|
|
|
|||
2
file.go
2
file.go
|
|
@ -6,12 +6,14 @@ import (
|
|||
|
||||
type File struct {
|
||||
gorm.Model
|
||||
Photo *Photo
|
||||
PhotoID uint
|
||||
Filename string
|
||||
FileType string `gorm:"type:varchar(30)"`
|
||||
MimeType string `gorm:"type:varchar(50)"`
|
||||
Width int
|
||||
Height int
|
||||
Orientation int
|
||||
AspectRatio float64
|
||||
Hash string `gorm:"type:varchar(100);unique_index"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
|
||||
#alerts .warning {
|
||||
background-color: #c18200;
|
||||
background-color: #a89a00;
|
||||
}
|
||||
|
||||
#alerts .warning:before {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
body {
|
||||
background: #fefefe;
|
||||
color: #333333;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
footer {
|
||||
|
|
@ -36,12 +37,20 @@ main {
|
|||
|
||||
main div.page {
|
||||
margin: 43px 0 0 0;
|
||||
padding: 2rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
main div.page {
|
||||
border-top-width: 3px;
|
||||
border-top-style: solid;
|
||||
main div.page .page-container {
|
||||
padding: 2rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
main div.page .page-form {
|
||||
padding: 2.4rem 2rem 0 2rem;
|
||||
float: none;
|
||||
}
|
||||
|
||||
main div.page .page-form form {
|
||||
}
|
||||
|
||||
.navbar {
|
||||
|
|
@ -51,6 +60,7 @@ main div.page {
|
|||
right: 0;
|
||||
background: black !important;
|
||||
position: fixed !important;
|
||||
box-shadow: 0px 6px 30px 0px rgba(0,0,0,0.75);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +70,6 @@ main div.page {
|
|||
}
|
||||
|
||||
main div.page-photos {
|
||||
border-top-color: #c8e5ff;
|
||||
}
|
||||
|
||||
.navbar .nav-item-albums .nav-link.active,
|
||||
|
|
@ -69,7 +78,6 @@ main div.page-photos {
|
|||
}
|
||||
|
||||
main div.page-albums {
|
||||
border-top-color: #c4ffcb;
|
||||
}
|
||||
|
||||
.navbar .nav-item-import .nav-link.active,
|
||||
|
|
@ -78,7 +86,6 @@ main div.page-albums {
|
|||
}
|
||||
|
||||
main div.page-import {
|
||||
border-top-color: #feffb8;
|
||||
}
|
||||
|
||||
.navbar .nav-item-export .nav-link.active,
|
||||
|
|
@ -87,7 +94,6 @@ main div.page-import {
|
|||
}
|
||||
|
||||
main div.page-export {
|
||||
border-top-color: #ffedc1;
|
||||
}
|
||||
|
||||
.navbar .nav-item-settings .nav-link.active,
|
||||
|
|
@ -96,7 +102,6 @@ main div.page-export {
|
|||
}
|
||||
|
||||
main div.page-settings {
|
||||
border-top-color: #f5c5c5;
|
||||
}
|
||||
|
||||
.photo-grid .photo {
|
||||
|
|
@ -105,7 +110,7 @@ main div.page-settings {
|
|||
width: 250px;
|
||||
height: 250px;
|
||||
float: left;
|
||||
margin: 2px;
|
||||
margin: 3px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
|
@ -138,6 +143,10 @@ main div.page-settings {
|
|||
float: right;
|
||||
}
|
||||
|
||||
.photo-grid .photo .left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.photo-grid .photo .actions {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
|
@ -149,7 +158,7 @@ main div.page-settings {
|
|||
font-size: 14px;
|
||||
padding: 6px 12px;
|
||||
color: white;
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
|
@ -162,8 +171,25 @@ main div.page-settings {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.photo-grid .photo .location {
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.photo-grid .photo:hover .location {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.photo-grid .photo:hover .actions .action,
|
||||
.photo-grid .photo .actions .action.liked {
|
||||
.photo-grid .photo .actions .action.favorite {
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media(min-width: 576px) {
|
||||
.mb-sm-0, .my-sm-0 {
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,63 @@
|
|||
<template>
|
||||
<div class="page page-photos">
|
||||
<div class="photo-grid">
|
||||
<div class="page-form">
|
||||
<b-form inline @submit="formChange">
|
||||
<b-form-select class="mb-2 mr-sm-2 mb-sm-0"
|
||||
v-b-tooltip.hover title="Category"
|
||||
v-model="form.category"
|
||||
:options="{ 'junction': 'Junction', 'tourism': 'Tourism', 'historic': 'Historic' }"
|
||||
id="inlineFormCustomSelectPref">
|
||||
<option slot="first" :value="null"></option>
|
||||
</b-form-select>
|
||||
|
||||
<b-form-select @change="formChange" class="mb-2 mr-sm-2 mb-sm-0"
|
||||
v-model="form.country"
|
||||
:options="{ '1': 'One', '2': 'Two', '3': 'Three' }"
|
||||
id="inlineFormCustomSelectPref">
|
||||
<option slot="first" :value="null">Country</option>
|
||||
</b-form-select>
|
||||
<b-form-select @change="formChange" class="mb-2 mr-sm-2 mb-sm-0"
|
||||
:v-model="form.camera"
|
||||
:options="{ '1': 'One', '2': 'Two', '3': 'Three' }"
|
||||
id="inlineFormCustomSelectPref">
|
||||
<option slot="first" :value="null">Camera Model</option>
|
||||
</b-form-select>
|
||||
<b-form-select @change="formChange" class="mb-2 mr-sm-2 mb-sm-0"
|
||||
v-model="dir"
|
||||
:options="{ 'asc': 'Ascending', 'desc': 'Descending' }"
|
||||
id="inlineFormCustomSelectPref">
|
||||
<option slot="first" :value="null">Sort Order</option>
|
||||
</b-form-select>
|
||||
|
||||
<b-form-select @change="formChange" class="mb-2 mr-sm-2 mb-sm-0"
|
||||
v-model="form.view"
|
||||
:options="{ 'list': 'List View', 'tile': 'Tile View (small)', 'tilel_large': 'Tile View (large)' }"
|
||||
id="inlineFormCustomSelectPref">
|
||||
</b-form-select>
|
||||
|
||||
<b-form-input class="mb-2 mr-sm-2 mb-sm-0" v-b-tooltip.hover title="Date" type="date"/>
|
||||
<b-form-input class="mb-2 mr-sm-2 mb-sm-0" placeholder="Tags" v-b-tooltip.hover title="Tags" type="text"/>
|
||||
|
||||
<b-form-checkbox class="mb-2 mr-sm-2 mb-sm-0">
|
||||
Favorites only
|
||||
</b-form-checkbox>
|
||||
</b-form>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="page-container photo-grid">
|
||||
<template v-for="photo in rows">
|
||||
<div class="photo">
|
||||
<div class="info">{{ photo.CreatedAt | moment("DD.MM.YYYY hh:mm:ss") }}<span class="right">{{ photo.CameraModel }}</span></div>
|
||||
<div class="info">{{ photo.TakenAt | moment("DD.MM.YYYY hh:mm:ss") }}<span class="right">{{ photo.CameraModel }}</span></div>
|
||||
<div class="actions">
|
||||
<a class="action like" v-bind:class="{ liked: photo.Liked, notliked: !photo.Liked }" v-on:click="likePhoto(photo)">
|
||||
<i v-if="!photo.Liked" class="far fa-heart"></i>
|
||||
<i v-if="photo.Liked" class="fas fa-heart"></i>
|
||||
</a>
|
||||
<span class="left">
|
||||
<a class="action like" v-bind:class="{ favorite: photo.Favorite }" v-on:click="likePhoto(photo)">
|
||||
<i v-if="!photo.Favorite" class="far fa-heart"></i>
|
||||
<i v-if="photo.Favorite" class="fas fa-heart"></i>
|
||||
</a>
|
||||
</span>
|
||||
<span class="center" v-if="photo.Location">
|
||||
<a class="location" target="_blank" :href="photo.getGoogleMapsLink()" v-b-tooltip.hover :title="photo.Location.DisplayName">{{ photo.Location.Country }}</a>
|
||||
</span>
|
||||
<span class="right">
|
||||
<a class="action delete" v-on:click="deletePhoto(photo)">
|
||||
<i class="fas fa-trash"></i>
|
||||
|
|
@ -33,18 +82,22 @@
|
|||
props: {},
|
||||
data() {
|
||||
const query = this.$route.query;
|
||||
const resultCount = query.hasOwnProperty('count') ? parseInt(query['count']) : 15;
|
||||
const resultCount = query.hasOwnProperty('count') ? parseInt(query['count']) : 70;
|
||||
const resultPage = query.hasOwnProperty('page') ? parseInt(query['page']) : 1;
|
||||
const resultOffset = resultCount * (resultPage - 1);
|
||||
const order = query.hasOwnProperty('order') ? query['order'] : '';
|
||||
const dir = query.hasOwnProperty('dir') ? query['dir'] : '';
|
||||
const q = query.hasOwnProperty('q') ? query['q'] : '';
|
||||
|
||||
return {
|
||||
'rows': [],
|
||||
'images': [],
|
||||
'form': {
|
||||
category: '',
|
||||
camera: '',
|
||||
dir: 'asc',
|
||||
view: 'list',
|
||||
},
|
||||
'page': resultPage,
|
||||
'order': order,
|
||||
'dir': dir,
|
||||
'q': q,
|
||||
'pageOptions': [15, 30, 50, 100],
|
||||
|
|
@ -57,19 +110,21 @@
|
|||
},
|
||||
methods: {
|
||||
likePhoto(photo) {
|
||||
photo.Liked = !photo.Liked;
|
||||
photo.Favorite = !photo.Favorite;
|
||||
},
|
||||
|
||||
deletePhoto(photo) {
|
||||
this.$alert.success('Photo deleted');
|
||||
},
|
||||
|
||||
formChange(event) {
|
||||
this.$alert.success('Form change');
|
||||
this.refreshList();
|
||||
},
|
||||
refreshList() {
|
||||
// Compose query parameters
|
||||
const params = {
|
||||
count: this.resultCount,
|
||||
offset: this.resultCount * (this.page - 1),
|
||||
order: this.order !== '' ? this.order + ' ' + this.dir : '',
|
||||
dir: this.dir,
|
||||
};
|
||||
|
||||
Object.assign(params, this.query);
|
||||
|
|
@ -83,7 +138,6 @@
|
|||
const urlParams = {
|
||||
count: this.resultCount,
|
||||
page: this.page,
|
||||
order: this.order,
|
||||
dir: this.dir,
|
||||
q: this.q
|
||||
};
|
||||
|
|
@ -92,13 +146,13 @@
|
|||
|
||||
this.$router.replace({query: urlParams});
|
||||
|
||||
Photo.search(params).then(response => {
|
||||
Photo.search(urlParams).then(response => {
|
||||
console.log(response);
|
||||
this.resultTotal = parseInt(response.headers['x-result-total']);
|
||||
this.resultCount = parseInt(response.headers['x-result-count']);
|
||||
this.resultOffset = parseInt(response.headers['x-result-offset']);
|
||||
this.rows = response.models;
|
||||
this.$alert.info(this.rows.length + ' photos loaded');
|
||||
this.$alert.info(this.rows.length + ' photos found');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
@ -109,17 +163,4 @@
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
h1, h2 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<!-- Right aligned nav items -->
|
||||
<b-navbar-nav class="ml-auto">
|
||||
|
||||
<b-nav-form>
|
||||
<b-nav-form action="/photos">
|
||||
<b-form-input size="sm" class="mr-sm-2" type="text" name="q" :value="q" placeholder="Search"/>
|
||||
<b-button size="sm" class="my-2 my-sm-0" type="submit">Search</b-button>
|
||||
</b-nav-form>
|
||||
|
|
@ -38,31 +38,10 @@
|
|||
const q = query.hasOwnProperty('q') ? query['q'] : '';
|
||||
|
||||
return {
|
||||
'q': q,
|
||||
q: q,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleLeftSidenav() {
|
||||
this.$refs.leftSidenav.toggle();
|
||||
},
|
||||
|
||||
open(ref) {
|
||||
},
|
||||
|
||||
close(ref) {
|
||||
},
|
||||
|
||||
login() {
|
||||
this.$refs.loginDialog.open();
|
||||
},
|
||||
|
||||
register() {
|
||||
this.$refs.registerDialog.open();
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.$session.logout();
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ class Photo extends Abstract {
|
|||
return this.ID;
|
||||
}
|
||||
|
||||
getGoogleMapsLink() {
|
||||
return 'https://www.google.com/maps/place/' + this.Lat + ',' + this.Long;
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
return 'photos';
|
||||
}
|
||||
|
|
|
|||
11
indexer.go
11
indexer.go
|
|
@ -69,13 +69,19 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) {
|
|||
photo.Tags = i.GetImageTags(jpeg)
|
||||
}
|
||||
|
||||
if location, err := mediaFile.GetLocation(); err == nil {
|
||||
i.db.FirstOrCreate(location, "id = ?", location.ID)
|
||||
photo.Location = location
|
||||
}
|
||||
|
||||
photo.TakenAt = mediaFile.GetDateCreated()
|
||||
photo.CanonicalName = canonicalName
|
||||
photo.Files = []File{}
|
||||
photo.Albums = []Album{}
|
||||
photo.Author = ""
|
||||
photo.CameraModel = mediaFile.GetCameraModel()
|
||||
photo.LocationName = ""
|
||||
photo.Liked = false
|
||||
|
||||
photo.Favorite = false
|
||||
photo.Private = true
|
||||
photo.Deleted = false
|
||||
|
||||
|
|
@ -88,6 +94,7 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) {
|
|||
file.Hash = fileHash
|
||||
file.FileType = mediaFile.GetType()
|
||||
file.MimeType = mediaFile.GetMimeType()
|
||||
file.Orientation = mediaFile.GetOrientation()
|
||||
|
||||
if mediaFile.GetWidth() > 0 && mediaFile.GetHeight() > 0 {
|
||||
file.Width = mediaFile.GetWidth()
|
||||
|
|
|
|||
104
location.go
Normal file
104
location.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"github.com/jinzhu/gorm"
|
||||
"strconv"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Location struct {
|
||||
gorm.Model
|
||||
DisplayName string
|
||||
Lat float64
|
||||
Long float64
|
||||
City string
|
||||
Postcode string
|
||||
County string
|
||||
State string
|
||||
Country string
|
||||
CountryCode string
|
||||
LocationCategory string
|
||||
LocationType string
|
||||
Favorite bool
|
||||
}
|
||||
|
||||
type OpenstreetmapAddress struct {
|
||||
Town string `json:"town"`
|
||||
City string `json:"city"`
|
||||
Postcode string `json:"postcode"`
|
||||
County string `json:"county"`
|
||||
State string `json:"state"`
|
||||
Country string `json:"country"`
|
||||
CountryCode string `json:"country_code"`
|
||||
}
|
||||
|
||||
type OpenstreetmapLocation struct {
|
||||
PlaceId string `json:"place_id"`
|
||||
Lat string `json:"lat"`
|
||||
Lon string `json:"lon"`
|
||||
Category string `json:"category"`
|
||||
Type string `json:"type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Address *OpenstreetmapAddress `json:"address"`
|
||||
}
|
||||
|
||||
// See https://wiki.openstreetmap.org/wiki/Nominatim#Reverse_Geocoding
|
||||
func (m *MediaFile) GetLocation() (*Location, error) {
|
||||
if m.location != nil {
|
||||
return m.location, nil
|
||||
}
|
||||
|
||||
location := &Location{}
|
||||
|
||||
openstreetmapLocation := &OpenstreetmapLocation{
|
||||
Address: &OpenstreetmapAddress{},
|
||||
}
|
||||
|
||||
if exifData, err := m.GetExifData(); err == nil {
|
||||
url := fmt.Sprintf("https://nominatim.openstreetmap.org/reverse?lat=%f&lon=%f&format=jsonv2", exifData.Lat, exifData.Long)
|
||||
|
||||
if res, err := http.Get(url); err == nil {
|
||||
json.NewDecoder(res.Body).Decode(openstreetmapLocation)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if id, err := strconv.Atoi(openstreetmapLocation.PlaceId); err == nil && id > 0 {
|
||||
location.ID = uint(id)
|
||||
} else {
|
||||
return nil, errors.New("no location found")
|
||||
}
|
||||
|
||||
if openstreetmapLocation.Address.City != "" {
|
||||
location.City = openstreetmapLocation.Address.City
|
||||
} else {
|
||||
location.City = openstreetmapLocation.Address.Town
|
||||
}
|
||||
|
||||
if lat, err := strconv.ParseFloat(openstreetmapLocation.Lat, 64); err == nil {
|
||||
location.Lat = lat
|
||||
}
|
||||
|
||||
if lon, err := strconv.ParseFloat(openstreetmapLocation.Lon, 64); err == nil {
|
||||
location.Long = lon
|
||||
}
|
||||
|
||||
location.Postcode = openstreetmapLocation.Address.Postcode
|
||||
location.County = openstreetmapLocation.Address.County
|
||||
location.State = openstreetmapLocation.Address.State
|
||||
location.Country = openstreetmapLocation.Address.Country
|
||||
location.CountryCode = openstreetmapLocation.Address.CountryCode
|
||||
location.DisplayName = openstreetmapLocation.DisplayName
|
||||
location.LocationCategory = openstreetmapLocation.Category
|
||||
location.LocationType = openstreetmapLocation.Type
|
||||
|
||||
m.location = location
|
||||
|
||||
return m.location, nil
|
||||
}
|
||||
15
mediafile.go
15
mediafile.go
|
|
@ -58,9 +58,10 @@ type MediaFile struct {
|
|||
mimeType string
|
||||
perceptualHash string
|
||||
tags []string
|
||||
exifData *ExifData
|
||||
width int
|
||||
height int
|
||||
exifData *ExifData
|
||||
location *Location
|
||||
}
|
||||
|
||||
func NewMediaFile(filename string) *MediaFile {
|
||||
|
|
@ -408,5 +409,13 @@ func (m *MediaFile) GetAspectRatio() float64 {
|
|||
|
||||
aspectRatio := width / height
|
||||
|
||||
return math.Round(aspectRatio * 100) / 100
|
||||
}
|
||||
return math.Round(aspectRatio*100) / 100
|
||||
}
|
||||
|
||||
func (m *MediaFile) GetOrientation() int {
|
||||
if exif, err := m.GetExifData(); err == nil {
|
||||
return exif.Orientation
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ func (m *MediaFile) GetExifData() (*ExifData, error) {
|
|||
|
||||
if orientation, err := x.Get(exif.Orientation); err == nil {
|
||||
m.exifData.Orientation, _ = orientation.Int(0)
|
||||
} else {
|
||||
m.exifData.Orientation = 1
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
9
photo.go
9
photo.go
|
|
@ -2,12 +2,14 @@ package photoprism
|
|||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Photo struct {
|
||||
gorm.Model
|
||||
Title string
|
||||
Description string `gorm:"type:text;"`
|
||||
TakenAt time.Time
|
||||
CanonicalName string
|
||||
PerceptualHash string
|
||||
Tags []Tag `gorm:"many2many:photo_tags;"`
|
||||
|
|
@ -15,10 +17,11 @@ type Photo struct {
|
|||
Albums []Album `gorm:"many2many:album_photos;"`
|
||||
Author string
|
||||
CameraModel string
|
||||
LocationName string
|
||||
Lat float64
|
||||
Long float64
|
||||
Liked bool
|
||||
Location *Location
|
||||
LocationID uint
|
||||
Favorite bool
|
||||
Private bool
|
||||
Deleted bool
|
||||
}
|
||||
}
|
||||
13
query.go
13
query.go
|
|
@ -18,8 +18,17 @@ func NewQuery(originalsPath string, db *gorm.DB) *Search {
|
|||
return instance
|
||||
}
|
||||
|
||||
func (s *Search) FindPhotos (count int, offset int) (photos []Photo) {
|
||||
s.db.Preload("Tags").Preload("Files").Preload("Albums").Where(&Photo{Deleted: false}).Limit(count).Offset(offset).Find(&photos)
|
||||
func (s *Search) FindPhotos (query string, count int, offset int) (photos []Photo) {
|
||||
q := s.db.Preload("Tags").Preload("Files").Preload("Location").Preload("Albums")
|
||||
|
||||
if query != "" {
|
||||
q = q.Joins("JOIN photo_tags ON photo_tags.photo_id=photos.id")
|
||||
q = q.Joins("JOIN tags ON photo_tags.tag_id=tags.id")
|
||||
q = q.Where("tags.label LIKE ?", "%" + query + "%")
|
||||
}
|
||||
|
||||
q = q.Where(&Photo{Deleted: false}).Order("taken_at").Limit(count).Offset(offset)
|
||||
q = q.Find(&photos)
|
||||
|
||||
return photos
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
"github.com/photoprism/photoprism"
|
||||
"strconv"
|
||||
"log"
|
||||
)
|
||||
|
||||
func Start(address string, port int, conf *photoprism.Config) {
|
||||
|
|
@ -22,11 +23,24 @@ func Start(address string, port int, conf *photoprism.Config) {
|
|||
v1.GET("/photos", func(c *gin.Context) {
|
||||
search := photoprism.NewQuery(conf.OriginalsPath, conf.GetDb())
|
||||
|
||||
photos := search.FindPhotos(70, 0)
|
||||
count, _ := strconv.Atoi(c.DefaultQuery("count", "50"))
|
||||
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
|
||||
query := c.DefaultQuery("q", "")
|
||||
|
||||
photos := search.FindPhotos(query, count, offset)
|
||||
|
||||
log.Printf("Query: %s, Count: %d", query, count)
|
||||
|
||||
c.Header("x-result-total", strconv.Itoa(len(photos)))
|
||||
c.Header("x-result-count", strconv.Itoa(count))
|
||||
c.Header("x-result-offset", strconv.Itoa(offset))
|
||||
|
||||
c.JSON(http.StatusOK, photos)
|
||||
})
|
||||
|
||||
// v1.OPTIONS()
|
||||
|
||||
v1.GET("/files", func(c *gin.Context) {
|
||||
search := photoprism.NewQuery(conf.OriginalsPath, conf.GetDb())
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"image"
|
||||
)
|
||||
|
||||
func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string, size int, square bool) {
|
||||
|
|
@ -60,21 +61,36 @@ func (m *MediaFile) GetThumbnail(path string, size int) (result *MediaFile, err
|
|||
return m.CreateThumbnail(thumbnailFilename, size)
|
||||
}
|
||||
|
||||
func (m *MediaFile) fixImageOrientation(img image.Image) image.Image {
|
||||
switch orientation := m.GetOrientation(); orientation {
|
||||
case 3:
|
||||
img = imaging.Rotate180(img)
|
||||
case 6:
|
||||
img = imaging.Rotate270(img)
|
||||
case 8:
|
||||
img = imaging.Rotate90(img)
|
||||
}
|
||||
|
||||
return img
|
||||
}
|
||||
|
||||
// Resize preserving the aspect ratio
|
||||
func (m *MediaFile) CreateThumbnail(filename string, size int) (result *MediaFile, err error) {
|
||||
image, err := imaging.Open(m.filename)
|
||||
img, err := imaging.Open(m.filename)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("open failed: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
image = imaging.Fit(image, size, size, imaging.Lanczos)
|
||||
img = m.fixImageOrientation(img)
|
||||
|
||||
err = imaging.Save(image, filename)
|
||||
img = imaging.Fit(img, size, size, imaging.Lanczos)
|
||||
|
||||
err = imaging.Save(img, filename)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("failed to save image: %v", err)
|
||||
log.Fatalf("failed to save img: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -102,19 +118,21 @@ func (m *MediaFile) GetSquareThumbnail(path string, size int) (result *MediaFile
|
|||
|
||||
// Resize and crop to square format
|
||||
func (m *MediaFile) CreateSquareThumbnail(filename string, size int) (result *MediaFile, err error) {
|
||||
image, err := imaging.Open(m.filename)
|
||||
img, err := imaging.Open(m.filename)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("open failed: %s", err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
image = imaging.Fill(image, size, size, imaging.Center, imaging.Lanczos)
|
||||
img = m.fixImageOrientation(img)
|
||||
|
||||
err = imaging.Save(image, filename)
|
||||
img = imaging.Fill(img, size, size, imaging.Center, imaging.Lanczos)
|
||||
|
||||
err = imaging.Save(img, filename)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("failed to save image: %v", err)
|
||||
log.Fatalf("failed to save img: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue