mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
Refactored tag search and added tag_slug column; improved search form
This commit is contained in:
parent
801b680f12
commit
91411a450b
18 changed files with 331 additions and 167 deletions
4
album.go
4
album.go
|
|
@ -6,6 +6,6 @@ import (
|
|||
|
||||
type Album struct {
|
||||
gorm.Model
|
||||
AlbumName string
|
||||
Photos []Photo `gorm:"many2many:album_photos;"`
|
||||
AlbumName string
|
||||
Photos []Photo `gorm:"many2many:album_photos;"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import (
|
|||
type Camera struct {
|
||||
gorm.Model
|
||||
CameraModel string
|
||||
CameraType string
|
||||
CameraNotes string
|
||||
CameraType string
|
||||
CameraNotes string
|
||||
}
|
||||
|
||||
func NewCamera(modelName string) *Camera {
|
||||
|
|
@ -27,4 +27,4 @@ func (c *Camera) FirstOrCreate(db *gorm.DB) *Camera {
|
|||
db.FirstOrCreate(c, "camera_model = ?", c.CameraModel)
|
||||
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,6 +147,6 @@ func (c *Config) MigrateDb() {
|
|||
db.AutoMigrate(&File{}, &Photo{}, &Tag{}, &Album{}, &Location{}, &Camera{})
|
||||
|
||||
if !db.Dialect().HasIndex("photos", "photos_fulltext") {
|
||||
db.Exec("CREATE FULLTEXT INDEX photos_fulltext ON photos (photo_title, photo_description, photo_artist, photo_keywords, photo_colors)")
|
||||
db.Exec("CREATE FULLTEXT INDEX photos_fulltext ON photos (photo_title, photo_description, photo_artist, photo_colors)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,14 @@ main div.page-export {
|
|||
main div.page-settings {
|
||||
}
|
||||
|
||||
.photo-tile {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.photo-tile.selected {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.photo-grid .photo {
|
||||
background: #eeeeee;
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ Vue.use(Vuetify, {
|
|||
primary: '#FDD835',
|
||||
secondary: '#b0bec5',
|
||||
accent: '#8c9eff',
|
||||
error: '#b71c1c',
|
||||
info: '#2196F3',
|
||||
success: '#4CAF50',
|
||||
warning: '#FFC107',
|
||||
error: '#F44336',
|
||||
info: '#00B8D4',
|
||||
success: '#00BFA5',
|
||||
warning: '#FFD600',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
label="Sort By"
|
||||
flat solo
|
||||
color="blue-grey"
|
||||
v-model="dir"
|
||||
v-model="query.order"
|
||||
:items="options.sorting">
|
||||
</v-select>
|
||||
</v-flex>
|
||||
|
|
@ -84,6 +84,14 @@
|
|||
>
|
||||
<v-icon>menu</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
small
|
||||
color="deep-purple lighten-2"
|
||||
>
|
||||
<v-icon>favorite</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
dark
|
||||
|
|
@ -118,36 +126,54 @@
|
|||
<v-icon>delete</v-icon>
|
||||
</v-btn>
|
||||
</v-speed-dial>
|
||||
<v-container grid-list-sm fluid class="pa-0">
|
||||
<v-container grid-list-xs fluid class="pa-0">
|
||||
<v-layout row wrap>
|
||||
<v-flex
|
||||
v-for="photo in results"
|
||||
:key="photo.ID"
|
||||
xs2
|
||||
d-flex
|
||||
xs12 sm6 md3 lg2 d-flex
|
||||
v-bind:class="{ selected: photo.selected }"
|
||||
class="photo-tile"
|
||||
>
|
||||
<v-card-actions flat tile class="d-flex" @click="selectPhoto(photo)">
|
||||
<v-img :src="'/api/v1/files/' + photo.FileID + '/square_thumbnail?size=500'"
|
||||
aspect-ratio="1"
|
||||
:title="photo.TakenAt | moment('DD.MM.YYYY hh:mm:ss')"
|
||||
class="grey lighten-2"
|
||||
>
|
||||
<v-layout
|
||||
slot="placeholder"
|
||||
fill-height
|
||||
align-center
|
||||
justify-center
|
||||
ma-0
|
||||
<v-tooltip bottom>
|
||||
<v-card-actions flat tile class="d-flex" slot="activator" @click="selectPhoto(photo)"
|
||||
@mouseover="overPhoto(photo)" @mouseleave="leavePhoto(photo)">
|
||||
<v-img :src="'/api/v1/files/' + photo.FileID + '/square_thumbnail?size=500'"
|
||||
aspect-ratio="1"
|
||||
class="grey lighten-2"
|
||||
>
|
||||
<v-progress-circular indeterminate color="grey lighten-5"></v-progress-circular>
|
||||
</v-layout>
|
||||
</v-img>
|
||||
</v-card-actions>
|
||||
|
||||
<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-actions>
|
||||
<span>{{ photo.PhotoTitle }}<br/>{{ photo.TakenAt | moment('DD/MM/YYYY') }} / {{ photo.CameraModel }}</span>
|
||||
</v-tooltip>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
<div style="clear: both"></div>
|
||||
<v-snackbar
|
||||
v-model="snackbarVisible"
|
||||
bottom
|
||||
:timeout="0"
|
||||
>
|
||||
{{ snackbarText }}
|
||||
<v-btn
|
||||
class="pr-0"
|
||||
color="primary"
|
||||
icon
|
||||
flat
|
||||
@click="clearSelection()"
|
||||
>
|
||||
<v-icon>close</v-icon>
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -164,28 +190,46 @@
|
|||
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'] : 'taken_at';
|
||||
const order = query.hasOwnProperty('order') && query['order'] != "" ? query['order'] : 'taken_at DESC';
|
||||
const dir = query.hasOwnProperty('dir') ? query['dir'] : '';
|
||||
const q = query.hasOwnProperty('q') ? query['q'] : '';
|
||||
const view = query.hasOwnProperty('view') ? query['view'] : 'tile';
|
||||
|
||||
return {
|
||||
'snackbarVisible': false,
|
||||
'snackbarText': '',
|
||||
'advandedSearch': false,
|
||||
'results': [],
|
||||
'query': {
|
||||
category: '',
|
||||
country: '',
|
||||
camera: '',
|
||||
after: '',
|
||||
before: '',
|
||||
favorites_only: '',
|
||||
order: order,
|
||||
q: q,
|
||||
},
|
||||
'options': {
|
||||
'categories': [ { value: '', text: 'All Categories' }, { value: 'junction', text: 'Junction' }, { value: 'tourism', text: 'Tourism'}, { value: 'historic', text: 'Historic'} ],
|
||||
'countries': [{ value: '', text: 'All Countries' }, { value: 'de', text: 'Germany' }, { value: 'ca', text: 'Canada'}, { value: 'us', text: 'United States'}],
|
||||
'cameras': [{ value: '', text: 'All Cameras' }, { value: '1', text: 'iPhone SE' }, { value: '2', text: 'Canon EOS 6D'}],
|
||||
'sorting': [{ value: '', text: 'Sort by date taken' }, { value: 'imported', text: 'Sort by date imported'}, { value: 'score', text: 'Sort by relevance' }],
|
||||
'categories': [
|
||||
{value: '', text: 'All Categories'},
|
||||
{value: 'junction', text: 'Junction'},
|
||||
{value: 'tourism', text: 'Tourism'},
|
||||
{value: 'historic', text: 'Historic'},
|
||||
],
|
||||
'countries': [
|
||||
{value: '', text: 'All Countries'},
|
||||
{value: 'de', text: 'Germany'},
|
||||
{value: 'ca', text: 'Canada'},
|
||||
{value: 'us', text: 'United States'}
|
||||
],
|
||||
'cameras': [
|
||||
{value: '', text: 'All Cameras'},
|
||||
{value: '1', text: 'iPhone SE'},
|
||||
{value: '2', text: 'Canon EOS 6D'},
|
||||
],
|
||||
'sorting': [
|
||||
{value: 'taken_at DESC', text: 'Newest first'},
|
||||
{value: 'taken_at', text: 'Oldest first'},
|
||||
{value: 'created_at DESC', text: 'Recently imported'},
|
||||
],
|
||||
},
|
||||
'page': resultPage,
|
||||
'order': order,
|
||||
|
|
@ -196,11 +240,51 @@
|
|||
'resultTotal': 'Many',
|
||||
'lastQuery': {},
|
||||
'submitTimeout': false,
|
||||
'selected': []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
overPhoto(photo) {
|
||||
|
||||
},
|
||||
leavePhoto(photo) {
|
||||
|
||||
},
|
||||
clearSelection() {
|
||||
for (let i = 0; i < this.selected.length; i++) {
|
||||
this.selected[i].selected = false;
|
||||
}
|
||||
this.selected = [];
|
||||
this.snackbarText = '';
|
||||
this.snackbarVisible = false;
|
||||
},
|
||||
selectPhoto(photo) {
|
||||
this.$alert.success(photo.getEntityName());
|
||||
console.log(photo)
|
||||
if (photo.selected) {
|
||||
for (let i = 0; i < this.selected.length; i++) {
|
||||
if (this.selected[i].id === photo.id) {
|
||||
this.selected.splice(i, 1)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
photo.selected = false;
|
||||
} else {
|
||||
this.selected.push(photo);
|
||||
photo.selected = true;
|
||||
}
|
||||
|
||||
if (this.selected.length > 0) {
|
||||
if (this.selected.length === 1) {
|
||||
this.snackbarText = 'One photo selected';
|
||||
} else {
|
||||
this.snackbarText = this.selected.length + ' photos selected';
|
||||
}
|
||||
this.snackbarVisible = true;
|
||||
} else {
|
||||
this.snackbarText = '';
|
||||
this.snackbarVisible = false;
|
||||
}
|
||||
},
|
||||
likePhoto(photo) {
|
||||
photo.Favorite = !photo.Favorite;
|
||||
|
|
@ -220,7 +304,6 @@
|
|||
const params = {
|
||||
count: this.resultCount,
|
||||
offset: this.resultCount * (this.page - 1),
|
||||
order: this.order !== '' ? this.order + ' ' + this.dir : '',
|
||||
};
|
||||
|
||||
Object.assign(params, this.query);
|
||||
|
|
@ -234,8 +317,6 @@
|
|||
const urlParams = {
|
||||
count: this.resultCount,
|
||||
page: this.page,
|
||||
order: this.order,
|
||||
dir: this.dir,
|
||||
};
|
||||
|
||||
Object.assign(urlParams, this.query);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export default [
|
|||
{ name: 'photos', path: '/photos', component: Photos },
|
||||
{ name: 'filters', path: '/filters', component: Todo },
|
||||
{ name: 'calendar', path: '/calendar', component: Todo },
|
||||
{ name: 'tags', path: '/tags', component: Todo },
|
||||
{ name: 'bookmarks', path: '/bookmarks', component: Todo },
|
||||
{ name: 'favorites', path: '/favorites', component: Todo },
|
||||
{ name: 'places', path: '/places', component: Todo },
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
<template>
|
||||
<v-snackbar
|
||||
v-model="visible"
|
||||
bottom
|
||||
:color="color"
|
||||
top
|
||||
right
|
||||
>
|
||||
{{ text }}
|
||||
<v-btn
|
||||
class="pr-0"
|
||||
color="primary"
|
||||
|
||||
icon
|
||||
flat
|
||||
@click="close"
|
||||
|
|
@ -23,6 +25,7 @@
|
|||
data() {
|
||||
return {
|
||||
text: '',
|
||||
color: 'primary',
|
||||
visible: false,
|
||||
messages: [],
|
||||
lastMessageId: 1,
|
||||
|
|
@ -74,13 +77,13 @@
|
|||
this.addMessage('info', message, 3000);
|
||||
},
|
||||
|
||||
addMessage: function (type, message, delay) {
|
||||
addMessage: function (color, message, delay) {
|
||||
if (message === this.lastMessage) return;
|
||||
|
||||
this.lastMessageId++;
|
||||
this.lastMessage = message;
|
||||
|
||||
const alert = {'id': this.lastMessageId, 'type': type, 'delay': delay, 'msg': message};
|
||||
const alert = {'id': this.lastMessageId, 'color': color, 'delay': delay, 'msg': message};
|
||||
|
||||
this.messages.push(alert);
|
||||
|
||||
|
|
@ -98,6 +101,7 @@
|
|||
|
||||
if(message) {
|
||||
this.text = message.msg;
|
||||
this.color = message.color;
|
||||
this.visible = true;
|
||||
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,16 @@
|
|||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile to="/tags" @click="">
|
||||
<v-list-tile-action>
|
||||
<v-icon>label</v-icon>
|
||||
</v-list-tile-action>
|
||||
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title>Tags</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
||||
<v-list-tile to="/favorites" @click="">
|
||||
<v-list-tile-action>
|
||||
<v-icon>favorite</v-icon>
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -14,6 +14,7 @@ require (
|
|||
github.com/gin-gonic/gin v1.3.0
|
||||
github.com/go-sql-driver/mysql v1.4.0
|
||||
github.com/golang/protobuf v1.2.0
|
||||
github.com/gosimple/slug v1.2.0
|
||||
github.com/jinzhu/gorm v1.9.1
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a
|
||||
github.com/json-iterator/go v1.1.5
|
||||
|
|
@ -26,6 +27,7 @@ require (
|
|||
github.com/modern-go/reflect2 v1.0.1
|
||||
github.com/pkg/errors v0.8.0
|
||||
github.com/pmezard/go-difflib v1.0.0
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect
|
||||
github.com/rwcarlsen/goexif v0.0.0-20180518182100-8d986c03457a
|
||||
github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3
|
||||
github.com/stretchr/testify v1.2.2
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -34,6 +34,8 @@ github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc
|
|||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gosimple/slug v1.2.0 h1:DqQXHQLprYBsiO4ZtdadqBeKh7CFnl5qoVNkKkVI7No=
|
||||
github.com/gosimple/slug v1.2.0/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0=
|
||||
github.com/jinzhu/gorm v1.9.1 h1:lDSDtsCt5AGGSKTs8AHlSDbbgif4G4+CKJ8ETBDVHTA=
|
||||
github.com/jinzhu/gorm v1.9.1/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
|
||||
|
|
@ -60,6 +62,8 @@ github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
|||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20180518182100-8d986c03457a h1:ZDZdsnbMuRSoVbq1gR47o005lfn2OwODNCr23zh9gSk=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20180518182100-8d986c03457a/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/steakknife/hamming v0.0.0-20161012051909-5ac3f73b8842 h1:Xk8V2cXXyb8xE/JOy2d8+0byqbKS1pEhVaeENndbaME=
|
||||
|
|
|
|||
52
indexer.go
52
indexer.go
|
|
@ -26,28 +26,22 @@ func NewIndexer(originalsPath string, db *gorm.DB) *Indexer {
|
|||
return instance
|
||||
}
|
||||
|
||||
func (i *Indexer) GetImageTags(jpeg *MediaFile) (result []Tag) {
|
||||
func (i *Indexer) GetImageTags(jpeg *MediaFile) (results []*Tag) {
|
||||
if imageBuffer, err := ioutil.ReadFile(jpeg.filename); err == nil {
|
||||
tags, err := recognize.GetImageTags(string(imageBuffer))
|
||||
|
||||
if err != nil {
|
||||
return result
|
||||
return results
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
if tag.Probability > 0.2 { // TODO: Use config variable
|
||||
var tagModel Tag
|
||||
|
||||
if res := i.db.First(&tagModel, "tag_label = ?", tag.Label); res.Error != nil {
|
||||
tagModel.TagLabel = tag.Label
|
||||
}
|
||||
|
||||
result = append(result, tagModel)
|
||||
results = i.appendTag(results, tag.Label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return results
|
||||
}
|
||||
|
||||
func getKeywordWithSynonyms(keyword string) []string {
|
||||
|
|
@ -81,12 +75,30 @@ func getKeywordsAsString(keywords []string) string {
|
|||
return strings.ToLower(strings.Join(result, ", "))
|
||||
}
|
||||
|
||||
func (i *Indexer) appendTag(tags []*Tag, label string) []*Tag {
|
||||
if label == "" {
|
||||
return tags
|
||||
}
|
||||
|
||||
label = strings.ToLower(label)
|
||||
|
||||
for _, tag := range tags {
|
||||
if tag.TagLabel == label {
|
||||
return tags
|
||||
}
|
||||
}
|
||||
|
||||
tag := NewTag(label).FirstOrCreate(i.db)
|
||||
|
||||
return append(tags, tag)
|
||||
}
|
||||
|
||||
func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) {
|
||||
var photo Photo
|
||||
var file, primaryFile File
|
||||
var isPrimary = false
|
||||
var colorNames []string
|
||||
var keywords []string
|
||||
var tags []*Tag
|
||||
|
||||
canonicalName := mediaFile.GetCanonicalNameFromFile()
|
||||
fileHash := mediaFile.GetHash()
|
||||
|
|
@ -111,17 +123,19 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) {
|
|||
photo.PhotoColors = strings.Join(colorNames, ", ")
|
||||
|
||||
// Tags (TensorFlow)
|
||||
photo.Tags = i.GetImageTags(jpeg)
|
||||
|
||||
for _, tag := range photo.Tags {
|
||||
keywords = append(keywords, tag.TagLabel)
|
||||
}
|
||||
tags = i.GetImageTags(jpeg)
|
||||
}
|
||||
|
||||
if location, err := mediaFile.GetLocation(); err == nil {
|
||||
i.db.FirstOrCreate(location, "id = ?", location.ID)
|
||||
photo.Location = location
|
||||
keywords = append(keywords, location.LocCity, location.LocCounty, location.LocCountry, location.LocCategory, location.LocName, location.LocType)
|
||||
|
||||
tags = i.appendTag(tags, location.LocCity)
|
||||
tags = i.appendTag(tags, location.LocCounty)
|
||||
tags = i.appendTag(tags, location.LocCountry)
|
||||
tags = i.appendTag(tags, location.LocCategory)
|
||||
tags = i.appendTag(tags, location.LocName)
|
||||
tags = i.appendTag(tags, location.LocType)
|
||||
|
||||
if location.LocName != "" { // TODO: User defined title format
|
||||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocName, location.LocCountry, mediaFile.GetDateCreated().Format("2006"))
|
||||
|
|
@ -140,12 +154,10 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) {
|
|||
}
|
||||
}
|
||||
|
||||
photo.PhotoKeywords = getKeywordsAsString(keywords)
|
||||
photo.Tags = tags
|
||||
photo.Camera = NewCamera(mediaFile.GetCameraModel()).FirstOrCreate(i.db)
|
||||
photo.TakenAt = mediaFile.GetDateCreated()
|
||||
photo.PhotoCanonicalName = canonicalName
|
||||
photo.Files = []File{}
|
||||
photo.Albums = []Album{}
|
||||
|
||||
photo.PhotoFavorite = false
|
||||
|
||||
|
|
|
|||
7
photo.go
7
photo.go
|
|
@ -11,7 +11,6 @@ type Photo struct {
|
|||
PhotoTitle string
|
||||
PhotoDescription string `gorm:"type:text;"`
|
||||
PhotoArtist string
|
||||
PhotoKeywords string
|
||||
PhotoColors string
|
||||
PhotoVibrantColor string
|
||||
PhotoMutedColor string
|
||||
|
|
@ -22,9 +21,9 @@ type Photo struct {
|
|||
PhotoLong float64
|
||||
Location *Location
|
||||
LocationID uint
|
||||
Tags []Tag `gorm:"many2many:photo_tags;"`
|
||||
Files []File
|
||||
Albums []Album `gorm:"many2many:album_photos;"`
|
||||
Tags []*Tag `gorm:"many2many:photo_tags;"`
|
||||
Files []*File
|
||||
Albums []*Album `gorm:"many2many:album_photos;"`
|
||||
Camera *Camera
|
||||
CameraID uint
|
||||
}
|
||||
|
|
|
|||
18
search.go
18
search.go
|
|
@ -3,6 +3,7 @@ package photoprism
|
|||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/forms"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -57,9 +58,12 @@ type PhotoSearchResult struct {
|
|||
FileHeight int
|
||||
FileOrientation int
|
||||
FileAspectRatio float64
|
||||
|
||||
// Tags
|
||||
Tags string
|
||||
}
|
||||
|
||||
func NewQuery(originalsPath string, db *gorm.DB) *Search {
|
||||
func NewSearch(originalsPath string, db *gorm.DB) *Search {
|
||||
instance := &Search{
|
||||
originalsPath: originalsPath,
|
||||
db: db,
|
||||
|
|
@ -75,17 +79,21 @@ func (s *Search) Photos(form forms.PhotoSearchForm) ([]PhotoSearchResult, error)
|
|||
Select(`photos.*,
|
||||
files.id AS file_id, files.file_name, files.file_type, files.file_mime, files.file_width, files.file_height, files.file_aspect_ratio, files.file_orientation,
|
||||
cameras.camera_model,
|
||||
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_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("LEFT JOIN locations ON locations.id = photos.location_id").
|
||||
Where("photos.deleted_at IS NULL")
|
||||
Joins("LEFT JOIN photo_tags ON photo_tags.photo_id = photos.id").
|
||||
Joins("LEFT JOIN tags ON photo_tags.tag_id = tags.id").
|
||||
Where("photos.deleted_at IS NULL").
|
||||
Group("photos.id, files.id")
|
||||
|
||||
if form.Query != "" {
|
||||
q = q.Where("MATCH (photo_title, photo_description, photo_artist, photo_keywords, photo_colors) AGAINST (? IN BOOLEAN MODE)", form.Query)
|
||||
q = q.Where("tags.tag_label LIKE ? OR MATCH (photo_title, photo_description, photo_artist, photo_colors) AGAINST (?)", strings.ToLower(form.Query)+"%", form.Query)
|
||||
}
|
||||
|
||||
q = q.Order("taken_at").Limit(form.Count).Offset(form.Offset)
|
||||
q = q.Order(form.Order).Limit(form.Count).Offset(form.Offset)
|
||||
|
||||
results := make([]PhotoSearchResult, 0, form.Count)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ func TestSearch_Photos(t *testing.T) {
|
|||
|
||||
conf.InitializeTestData(t)
|
||||
|
||||
search := NewQuery(conf.OriginalsPath, conf.GetDb())
|
||||
search := NewSearch(conf.OriginalsPath, conf.GetDb())
|
||||
|
||||
var form forms.PhotoSearchForm
|
||||
|
||||
|
|
|
|||
95
server/routes.go
Normal file
95
server/routes.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/photoprism/photoprism"
|
||||
"github.com/photoprism/photoprism/forms"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func ConfigureRoutes(app *gin.Engine, conf *photoprism.Config) {
|
||||
app.LoadHTMLGlob("server/templates/*")
|
||||
|
||||
app.StaticFile("/favicon.ico", "./server/assets/favicon.ico")
|
||||
app.StaticFile("/robots.txt", "./server/assets/robots.txt")
|
||||
app.Static("/assets", "./server/assets")
|
||||
|
||||
// JSON-REST API Version 1
|
||||
v1 := app.Group("/api/v1")
|
||||
{
|
||||
v1.GET("/photos", func(c *gin.Context) {
|
||||
var form forms.PhotoSearchForm
|
||||
|
||||
search := photoprism.NewSearch(conf.OriginalsPath, conf.GetDb())
|
||||
|
||||
c.MustBindWith(&form, binding.Form)
|
||||
|
||||
if photos, err := search.Photos(form); err == nil {
|
||||
c.Header("x-result-total", strconv.Itoa(len(photos)))
|
||||
c.Header("x-result-count", strconv.Itoa(form.Count))
|
||||
c.Header("x-result-offset", strconv.Itoa(form.Offset))
|
||||
|
||||
c.JSON(http.StatusOK, photos)
|
||||
} else {
|
||||
c.AbortWithError(400, err)
|
||||
}
|
||||
})
|
||||
|
||||
// v1.OPTIONS()
|
||||
|
||||
v1.GET("/files", func(c *gin.Context) {
|
||||
search := photoprism.NewSearch(conf.OriginalsPath, conf.GetDb())
|
||||
|
||||
files := search.FindFiles(70, 0)
|
||||
|
||||
c.JSON(http.StatusOK, files)
|
||||
})
|
||||
|
||||
v1.GET("/files/:id/thumbnail", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
size, _ := strconv.Atoi(c.Query("size"))
|
||||
|
||||
search := photoprism.NewSearch(conf.OriginalsPath, conf.GetDb())
|
||||
|
||||
file := search.FindFile(id)
|
||||
|
||||
mediaFile := photoprism.NewMediaFile(file.FileName)
|
||||
|
||||
thumbnail, _ := mediaFile.GetThumbnail(conf.ThumbnailsPath, size)
|
||||
|
||||
c.File(thumbnail.GetFilename())
|
||||
})
|
||||
|
||||
v1.GET("/files/:id/square_thumbnail", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
size, _ := strconv.Atoi(c.Query("size"))
|
||||
|
||||
search := photoprism.NewSearch(conf.OriginalsPath, conf.GetDb())
|
||||
|
||||
file := search.FindFile(id)
|
||||
|
||||
mediaFile := photoprism.NewMediaFile(file.FileName)
|
||||
|
||||
thumbnail, _ := mediaFile.GetSquareThumbnail(conf.ThumbnailsPath, size)
|
||||
|
||||
c.File(thumbnail.GetFilename())
|
||||
})
|
||||
|
||||
v1.GET("/albums", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
v1.GET("/tags", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
}
|
||||
|
||||
app.NoRoute(func(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"title": "PhotoPrism",
|
||||
"debug": true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -3,99 +3,13 @@ package server
|
|||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/photoprism/photoprism"
|
||||
"github.com/photoprism/photoprism/forms"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Start(address string, port int, conf *photoprism.Config) {
|
||||
router := gin.Default()
|
||||
app := gin.Default()
|
||||
|
||||
router.LoadHTMLGlob("server/templates/*")
|
||||
ConfigureRoutes(app, conf)
|
||||
|
||||
router.StaticFile("/favicon.ico", "./server/assets/favicon.ico")
|
||||
router.StaticFile("/robots.txt", "./server/assets/robots.txt")
|
||||
|
||||
// JSON-REST API Version 1
|
||||
v1 := router.Group("/api/v1")
|
||||
{
|
||||
v1.GET("/photos", func(c *gin.Context) {
|
||||
search := photoprism.NewQuery(conf.OriginalsPath, conf.GetDb())
|
||||
|
||||
var form forms.PhotoSearchForm
|
||||
|
||||
c.MustBindWith(&form, binding.Form)
|
||||
|
||||
if photos, err := search.Photos(form); err == nil {
|
||||
c.Header("x-result-total", strconv.Itoa(len(photos)))
|
||||
c.Header("x-result-count", strconv.Itoa(form.Count))
|
||||
c.Header("x-result-offset", strconv.Itoa(form.Offset))
|
||||
|
||||
c.JSON(http.StatusOK, photos)
|
||||
} else {
|
||||
c.AbortWithError(500, err)
|
||||
}
|
||||
})
|
||||
|
||||
// v1.OPTIONS()
|
||||
|
||||
v1.GET("/files", func(c *gin.Context) {
|
||||
search := photoprism.NewQuery(conf.OriginalsPath, conf.GetDb())
|
||||
|
||||
files := search.FindFiles(70, 0)
|
||||
|
||||
c.JSON(http.StatusOK, files)
|
||||
})
|
||||
|
||||
v1.GET("/files/:id/thumbnail", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
size, _ := strconv.Atoi(c.Query("size"))
|
||||
|
||||
search := photoprism.NewQuery(conf.OriginalsPath, conf.GetDb())
|
||||
|
||||
file := search.FindFile(id)
|
||||
|
||||
mediaFile := photoprism.NewMediaFile(file.FileName)
|
||||
|
||||
thumbnail, _ := mediaFile.GetThumbnail(conf.ThumbnailsPath, size)
|
||||
|
||||
c.File(thumbnail.GetFilename())
|
||||
})
|
||||
|
||||
v1.GET("/files/:id/square_thumbnail", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
size, _ := strconv.Atoi(c.Query("size"))
|
||||
|
||||
search := photoprism.NewQuery(conf.OriginalsPath, conf.GetDb())
|
||||
|
||||
file := search.FindFile(id)
|
||||
|
||||
mediaFile := photoprism.NewMediaFile(file.FileName)
|
||||
|
||||
thumbnail, _ := mediaFile.GetSquareThumbnail(conf.ThumbnailsPath, size)
|
||||
|
||||
c.File(thumbnail.GetFilename())
|
||||
})
|
||||
|
||||
v1.GET("/albums", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
v1.GET("/tags", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
}
|
||||
|
||||
router.Static("/assets", "./server/assets")
|
||||
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"title": "PhotoPrism",
|
||||
"debug": true,
|
||||
})
|
||||
})
|
||||
|
||||
router.Run(fmt.Sprintf("%s:%d", address, port))
|
||||
app.Run(fmt.Sprintf("%s:%d", address, port))
|
||||
}
|
||||
|
|
|
|||
26
tag.go
26
tag.go
|
|
@ -1,10 +1,36 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/jinzhu/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
gorm.Model
|
||||
TagLabel string `gorm:"type:varchar(100);unique_index"`
|
||||
TagSlug string `gorm:"type:varchar(100);unique_index"`
|
||||
}
|
||||
|
||||
func NewTag(label string) *Tag {
|
||||
if label == "" {
|
||||
label = "unknown"
|
||||
}
|
||||
|
||||
tagLabel := strings.ToLower(label)
|
||||
|
||||
tagSlug := slug.MakeLang(tagLabel, "en")
|
||||
|
||||
result := &Tag{
|
||||
TagLabel: tagLabel,
|
||||
TagSlug: tagSlug,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (t *Tag) FirstOrCreate(db *gorm.DB) *Tag {
|
||||
db.FirstOrCreate(t, "tag_label = ?", t.TagLabel)
|
||||
|
||||
return t
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue