Refactoring the photoprism package #53
* Started refactoring the photoprism package. * A lot of more comments and eliminating utils. * Fixed search. * See #50 The great refactor -- Refactor config to YAML, Add Docs, Revise exported fields
This commit is contained in:
parent
891870b05e
commit
b202bb6cc7
2
go.mod
2
go.mod
|
@ -50,5 +50,5 @@ require (
|
|||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
|
||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce // indirect
|
||||
gopkg.in/yaml.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.1
|
||||
)
|
||||
|
|
|
@ -40,7 +40,7 @@ func LikePhoto(router *gin.RouterGroup, conf *photoprism.Config) {
|
|||
photoId, err := strconv.ParseUint(c.Param("photoId"), 10, 64)
|
||||
|
||||
if err == nil {
|
||||
photo := search.FindPhotoById(photoId)
|
||||
photo := search.FindPhotoByID(photoId)
|
||||
photo.PhotoFavorite = true
|
||||
conf.GetDb().Save(&photo)
|
||||
c.JSON(http.StatusAccepted, http.Response{})
|
||||
|
@ -58,7 +58,7 @@ func DislikePhoto(router *gin.RouterGroup, conf *photoprism.Config) {
|
|||
photoId, err := strconv.ParseUint(c.Param("photoId"), 10, 64)
|
||||
|
||||
if err == nil {
|
||||
photo := search.FindPhotoById(photoId)
|
||||
photo := search.FindPhotoByID(photoId)
|
||||
photo.PhotoFavorite = false
|
||||
conf.GetDb().Save(&photo)
|
||||
c.JSON(http.StatusAccepted, http.Response{})
|
||||
|
|
|
@ -25,6 +25,7 @@ func getColorNames(actualColor colorful.Color) (result []string) {
|
|||
return result
|
||||
}
|
||||
|
||||
// GetColors returns color information for a given mediafiles.
|
||||
func (m *MediaFile) GetColors() (colors []string, vibrantHex string, mutedHex string) {
|
||||
file, _ := os.Open(m.filename)
|
||||
|
||||
|
|
|
@ -3,18 +3,21 @@ package photoprism
|
|||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/mssql"
|
||||
_ "github.com/jinzhu/gorm/dialects/mssql" // Import gorm drivers
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
"github.com/kylelemons/go-gypsy/yaml"
|
||||
. "github.com/photoprism/photoprism/internal/models"
|
||||
"github.com/photoprism/photoprism/internal/models"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// Config provides a struct in which application configuration is stored.
|
||||
type Config struct {
|
||||
Debug bool
|
||||
ConfigFile string
|
||||
|
@ -32,8 +35,12 @@ type Config struct {
|
|||
db *gorm.DB
|
||||
}
|
||||
|
||||
type ConfigValues map[string]interface{}
|
||||
type configValues map[string]interface{}
|
||||
|
||||
// NewConfig creates a new configuration entity by using two methods.
|
||||
// 1: SetValuesFromFile: This will initialize values from a yaml config file.
|
||||
// 2: SetValuesFromCliContext: Which comes after SetValuesFromFile and overrides
|
||||
// any previous values giving an option two override file configs through the CLI.
|
||||
func NewConfig(context *cli.Context) *Config {
|
||||
c := &Config{}
|
||||
c.SetValuesFromFile(GetExpandedFilename(context.GlobalString("config-file")))
|
||||
|
@ -42,6 +49,7 @@ func NewConfig(context *cli.Context) *Config {
|
|||
return c
|
||||
}
|
||||
|
||||
// SetValuesFromFile uses a yaml config file to initiate the configuration entity.
|
||||
func (c *Config) SetValuesFromFile(fileName string) error {
|
||||
yamlConfig, err := yaml.ReadFile(fileName)
|
||||
|
||||
|
@ -50,7 +58,6 @@ func (c *Config) SetValuesFromFile(fileName string) error {
|
|||
}
|
||||
|
||||
c.ConfigFile = fileName
|
||||
|
||||
if debug, err := yamlConfig.GetBool("debug"); err == nil {
|
||||
c.Debug = debug
|
||||
}
|
||||
|
@ -102,6 +109,8 @@ func (c *Config) SetValuesFromFile(fileName string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// SetValuesFromCliContext uses values from the CLI to setup configuration overrides
|
||||
// for the entity.
|
||||
func (c *Config) SetValuesFromCliContext(context *cli.Context) error {
|
||||
if context.GlobalBool("debug") {
|
||||
c.Debug = context.GlobalBool("debug")
|
||||
|
@ -142,6 +151,11 @@ func (c *Config) SetValuesFromCliContext(context *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// CreateDirectories creates all the folders that photoprism needs. These are:
|
||||
// OriginalsPath
|
||||
// ThumbnailsPath
|
||||
// ImportPath
|
||||
// ExportPath
|
||||
func (c *Config) CreateDirectories() error {
|
||||
if err := os.MkdirAll(c.OriginalsPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
|
@ -162,7 +176,9 @@ func (c *Config) CreateDirectories() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) ConnectToDatabase() error {
|
||||
// connectToDatabase estabilishes a connection to a database given a driver.
|
||||
// It tries to do this 12 times with a 5 second sleep intervall in between.
|
||||
func (c *Config) connectToDatabase() error {
|
||||
db, err := gorm.Open(c.DatabaseDriver, c.DatabaseDsn)
|
||||
|
||||
if err != nil || db == nil {
|
||||
|
@ -186,52 +202,68 @@ func (c *Config) ConnectToDatabase() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// GetAssetsPath returns the path to the assets.
|
||||
func (c *Config) GetAssetsPath() string {
|
||||
return c.AssetsPath
|
||||
}
|
||||
|
||||
// GetTensorFlowModelPath returns the tensorflow model path.
|
||||
func (c *Config) GetTensorFlowModelPath() string {
|
||||
return c.GetAssetsPath() + "/tensorflow"
|
||||
}
|
||||
|
||||
// GetTemplatesPath returns the templates path.
|
||||
func (c *Config) GetTemplatesPath() string {
|
||||
return c.GetAssetsPath() + "/templates"
|
||||
}
|
||||
|
||||
// GetFaviconsPath returns the favicons path.
|
||||
func (c *Config) GetFaviconsPath() string {
|
||||
return c.GetAssetsPath() + "/favicons"
|
||||
}
|
||||
|
||||
// GetPublicPath returns the public path.
|
||||
func (c *Config) GetPublicPath() string {
|
||||
return c.GetAssetsPath() + "/public"
|
||||
}
|
||||
|
||||
// GetPublicBuildPath returns the public build path.
|
||||
func (c *Config) GetPublicBuildPath() string {
|
||||
return c.GetPublicPath() + "/build"
|
||||
}
|
||||
|
||||
// GetDb gets a db connection. If it already is estabilished it will return that.
|
||||
func (c *Config) GetDb() *gorm.DB {
|
||||
if c.db == nil {
|
||||
c.ConnectToDatabase()
|
||||
c.connectToDatabase()
|
||||
}
|
||||
|
||||
return c.db
|
||||
}
|
||||
|
||||
// MigrateDb will start a migration process.
|
||||
func (c *Config) MigrateDb() {
|
||||
db := c.GetDb()
|
||||
|
||||
db.AutoMigrate(&File{}, &Photo{}, &Tag{}, &Album{}, &Location{}, &Camera{}, &Lens{}, &Country{})
|
||||
db.AutoMigrate(&models.File{},
|
||||
&models.Photo{},
|
||||
&models.Tag{},
|
||||
&models.Album{},
|
||||
&models.Location{},
|
||||
&models.Camera{},
|
||||
&models.Lens{},
|
||||
&models.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)")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) GetClientConfig() ConfigValues {
|
||||
// GetClientConfig returns a loaded and set configuration entity.
|
||||
func (c *Config) GetClientConfig() map[string]interface{} {
|
||||
db := c.GetDb()
|
||||
|
||||
var cameras []*Camera
|
||||
var cameras []*models.Camera
|
||||
|
||||
type country struct {
|
||||
LocCountry string
|
||||
|
@ -240,14 +272,14 @@ func (c *Config) GetClientConfig() ConfigValues {
|
|||
|
||||
var countries []country
|
||||
|
||||
db.Model(&Location{}).Select("DISTINCT loc_country_code, loc_country").Scan(&countries)
|
||||
db.Model(&models.Location{}).Select("DISTINCT loc_country_code, loc_country").Scan(&countries)
|
||||
|
||||
db.Where("deleted_at IS NULL").Limit(1000).Order("camera_model").Find(&cameras)
|
||||
|
||||
jsHash := fileHash(c.GetPublicBuildPath() + "/app.js")
|
||||
cssHash := fileHash(c.GetPublicBuildPath() + "/app.css")
|
||||
|
||||
result := ConfigValues{
|
||||
result := configValues{
|
||||
"title": "PhotoPrism",
|
||||
"debug": c.Debug,
|
||||
"cameras": cameras,
|
||||
|
@ -258,3 +290,21 @@ func (c *Config) GetClientConfig() ConfigValues {
|
|||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetExpandedFilename returns the expanded format for a filename.
|
||||
func GetExpandedFilename(filename string) string {
|
||||
usr, _ := user.Current()
|
||||
dir := usr.HomeDir
|
||||
|
||||
if filename == "" {
|
||||
panic("filename was empty")
|
||||
}
|
||||
|
||||
if len(filename) > 2 && filename[:2] == "~/" {
|
||||
filename = filepath.Join(dir, filename[2:])
|
||||
}
|
||||
|
||||
result, _ := filepath.Abs(filename)
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
|
@ -12,7 +18,7 @@ import (
|
|||
)
|
||||
|
||||
const testDataPath = "testdata"
|
||||
const testDataUrl = "https://www.dropbox.com/s/na9p9wwt98l7m5b/import.zip?dl=1"
|
||||
const testDataURL = "https://www.dropbox.com/s/na9p9wwt98l7m5b/import.zip?dl=1"
|
||||
const testDataHash = "ed3bdb2fe86ea662bc863b63e219b47b8d9a74024757007f7979887d"
|
||||
const testConfigFile = "../../configs/photoprism.yml"
|
||||
|
||||
|
@ -44,9 +50,9 @@ func (c *Config) DownloadTestData(t *testing.T) {
|
|||
}
|
||||
|
||||
if !fileExists(testDataZip) {
|
||||
fmt.Printf("Downloading latest test data zip file from %s\n", testDataUrl)
|
||||
fmt.Printf("Downloading latest test data zip file from %s\n", testDataURL)
|
||||
|
||||
if err := downloadFile(testDataZip, testDataUrl); err != nil {
|
||||
if err := downloadFile(testDataZip, testDataURL); err != nil {
|
||||
fmt.Printf("Download failed: %s\n", err.Error())
|
||||
}
|
||||
}
|
||||
|
@ -131,9 +137,104 @@ func TestConfig_SetValuesFromFile(t *testing.T) {
|
|||
func TestConfig_ConnectToDatabase(t *testing.T) {
|
||||
c := NewTestConfig()
|
||||
|
||||
c.ConnectToDatabase()
|
||||
c.connectToDatabase()
|
||||
|
||||
db := c.GetDb()
|
||||
|
||||
assert.IsType(t, &gorm.DB{}, db)
|
||||
}
|
||||
|
||||
func unzip(src, dest string) ([]string, error) {
|
||||
|
||||
var filenames []string
|
||||
|
||||
r, err := zip.OpenReader(src)
|
||||
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
// Skip directories like __OSX
|
||||
if strings.HasPrefix(f.Name, "__") {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
defer rc.Close()
|
||||
|
||||
// Store filename/path for returning and using later on
|
||||
fpath := filepath.Join(dest, f.Name)
|
||||
filenames = append(filenames, fpath)
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
|
||||
// Make Folder
|
||||
os.MkdirAll(fpath, os.ModePerm)
|
||||
|
||||
} else {
|
||||
|
||||
// Make File
|
||||
var fdir string
|
||||
if lastIndex := strings.LastIndex(fpath, string(os.PathSeparator)); lastIndex > -1 {
|
||||
fdir = fpath[:lastIndex]
|
||||
}
|
||||
|
||||
err = os.MkdirAll(fdir, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return filenames, err
|
||||
}
|
||||
f, err := os.OpenFile(
|
||||
fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.Copy(f, rc)
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return filenames, nil
|
||||
}
|
||||
|
||||
func downloadFile(filepath string, url string) (err error) {
|
||||
// Create the file
|
||||
out, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Get the data
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check server response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Writer the body to file
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package photoprism
|
||||
|
||||
const PerceptualHashSize = 4
|
|
@ -8,10 +8,13 @@ import (
|
|||
"path/filepath"
|
||||
)
|
||||
|
||||
// Converter wraps a darktable cli binary.
|
||||
type Converter struct {
|
||||
darktableCli string
|
||||
}
|
||||
|
||||
// NewConverter returns a new converter by setting the darktable
|
||||
// cli binary location.
|
||||
func NewConverter(darktableCli string) *Converter {
|
||||
if stat, err := os.Stat(darktableCli); err != nil {
|
||||
log.Print("Darktable CLI binary could not be found at " + darktableCli)
|
||||
|
@ -22,6 +25,8 @@ func NewConverter(darktableCli string) *Converter {
|
|||
return &Converter{darktableCli: darktableCli}
|
||||
}
|
||||
|
||||
// ConvertAll converts all the files given a path to JPEG. This function
|
||||
// ignores error during this process.
|
||||
func (c *Converter) ConvertAll(path string) {
|
||||
err := filepath.Walk(path, func(filename string, fileInfo os.FileInfo, err error) error {
|
||||
|
||||
|
@ -52,6 +57,7 @@ func (c *Converter) ConvertAll(path string) {
|
|||
}
|
||||
}
|
||||
|
||||
// ConvertToJpeg converts a single image the JPEG format.
|
||||
func (c *Converter) ConvertToJpeg(image *MediaFile) (*MediaFile, error) {
|
||||
if !image.Exists() {
|
||||
return nil, fmt.Errorf("can not convert, file does not exist: %s", image.GetFilename())
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/rwcarlsen/goexif/mknote"
|
||||
)
|
||||
|
||||
// ExifData returns information about a single image.
|
||||
type ExifData struct {
|
||||
DateTime time.Time
|
||||
Artist string
|
||||
|
@ -28,6 +29,7 @@ type ExifData struct {
|
|||
Orientation int
|
||||
}
|
||||
|
||||
// GetExifData return ExifData given a single mediaFile.
|
||||
func (m *MediaFile) GetExifData() (*ExifData, error) {
|
||||
if m == nil {
|
||||
return nil, errors.New("media file is null")
|
||||
|
@ -116,8 +118,8 @@ func (m *MediaFile) GetExifData() (*ExifData, error) {
|
|||
m.exifData.Thumbnail = thumbnail
|
||||
}
|
||||
|
||||
if uniqueId, err := x.Get(exif.ImageUniqueID); err == nil {
|
||||
m.exifData.UniqueID = uniqueId.String()
|
||||
if uniqueID, err := x.Get(exif.ImageUniqueID); err == nil {
|
||||
m.exifData.UniqueID = uniqueID.String()
|
||||
}
|
||||
|
||||
if width, err := x.Get(exif.ImageWidth); err == nil {
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// FindOriginalsByDate searches the originalsPath given a time frame in the format of
|
||||
// after <=> before and returns a list of results.
|
||||
func FindOriginalsByDate(originalsPath string, after time.Time, before time.Time) (result []*MediaFile) {
|
||||
filepath.Walk(originalsPath, func(filename string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil || fileInfo.IsDir() || strings.HasPrefix(filepath.Base(filename), ".") {
|
||||
|
@ -35,6 +37,8 @@ func FindOriginalsByDate(originalsPath string, after time.Time, before time.Time
|
|||
return result
|
||||
}
|
||||
|
||||
// ExportPhotosFromOriginals takes a list of original mediafiles and exports
|
||||
// them to JPEG.
|
||||
func ExportPhotosFromOriginals(originals []*MediaFile, thumbnailsPath string, exportPath string, size int) (err error) {
|
||||
for _, mediaFile := range originals {
|
||||
|
||||
|
|
34
internal/photoprism/file_check.go
Normal file
34
internal/photoprism/file_check.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
func fileHash(filename string) string {
|
||||
var result []byte
|
||||
|
||||
file, err := os.Open(filename)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
hash := sha1.New()
|
||||
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hash.Sum(result))
|
||||
}
|
|
@ -2,6 +2,7 @@ package photoprism
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -10,6 +11,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// Importer todo: Fill me.
|
||||
type Importer struct {
|
||||
originalsPath string
|
||||
indexer *Indexer
|
||||
|
@ -19,6 +21,7 @@ type Importer struct {
|
|||
removeEmptyDirectories bool
|
||||
}
|
||||
|
||||
// NewImporter returns a new importer.
|
||||
func NewImporter(originalsPath string, indexer *Indexer, converter *Converter) *Importer {
|
||||
instance := &Importer{
|
||||
originalsPath: originalsPath,
|
||||
|
@ -32,6 +35,8 @@ func NewImporter(originalsPath string, indexer *Indexer, converter *Converter) *
|
|||
return instance
|
||||
}
|
||||
|
||||
// ImportPhotosFromDirectory imports all the photos from a given directory path.
|
||||
// This function ignores errors.
|
||||
func (i *Importer) ImportPhotosFromDirectory(importPath string) {
|
||||
var directories []string
|
||||
|
||||
|
@ -126,6 +131,7 @@ func (i *Importer) ImportPhotosFromDirectory(importPath string) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetDestinationFilename get the destination of a media file.
|
||||
func (i *Importer) GetDestinationFilename(mainFile *MediaFile, mediaFile *MediaFile) (string, error) {
|
||||
canonicalName := mainFile.GetCanonicalName()
|
||||
fileExtension := mediaFile.GetExtension()
|
||||
|
@ -150,3 +156,21 @@ func (i *Importer) GetDestinationFilename(mainFile *MediaFile, mediaFile *MediaF
|
|||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func directoryIsEmpty(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Readdirnames(1)
|
||||
|
||||
if err == io.EOF {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -9,20 +9,23 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
. "github.com/photoprism/photoprism/internal/models"
|
||||
"github.com/photoprism/photoprism/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
IndexResultUpdated = "Updated"
|
||||
IndexResultAdded = "Added"
|
||||
indexResultUpdated = "Updated"
|
||||
indexResultAdded = "Added"
|
||||
)
|
||||
|
||||
// Indexer defines an indexer with originals path tensorflow and a db.
|
||||
type Indexer struct {
|
||||
originalsPath string
|
||||
tensorFlow *TensorFlow
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewIndexer returns a new indexer.
|
||||
// TODO: Is it really necessary to return a pointer?
|
||||
func NewIndexer(originalsPath string, tensorFlow *TensorFlow, db *gorm.DB) *Indexer {
|
||||
instance := &Indexer{
|
||||
originalsPath: originalsPath,
|
||||
|
@ -33,7 +36,9 @@ func NewIndexer(originalsPath string, tensorFlow *TensorFlow, db *gorm.DB) *Inde
|
|||
return instance
|
||||
}
|
||||
|
||||
func (i *Indexer) GetImageTags(jpeg *MediaFile) (results []*Tag) {
|
||||
// getImageTags returns all tags of a given mediafile. This function returns
|
||||
// an empty list in the case of an error.
|
||||
func (i *Indexer) getImageTags(jpeg *MediaFile) (results []*models.Tag) {
|
||||
tags, err := i.tensorFlow.GetImageTagsFromFile(jpeg.filename)
|
||||
|
||||
if err != nil {
|
||||
|
@ -49,7 +54,7 @@ func (i *Indexer) GetImageTags(jpeg *MediaFile) (results []*Tag) {
|
|||
return results
|
||||
}
|
||||
|
||||
func (i *Indexer) appendTag(tags []*Tag, label string) []*Tag {
|
||||
func (i *Indexer) appendTag(tags []*models.Tag, label string) []*models.Tag {
|
||||
if label == "" {
|
||||
return tags
|
||||
}
|
||||
|
@ -62,17 +67,17 @@ func (i *Indexer) appendTag(tags []*Tag, label string) []*Tag {
|
|||
}
|
||||
}
|
||||
|
||||
tag := NewTag(label).FirstOrCreate(i.db)
|
||||
tag := models.NewTag(label).FirstOrCreate(i.db)
|
||||
|
||||
return append(tags, tag)
|
||||
}
|
||||
|
||||
func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
|
||||
var photo Photo
|
||||
var file, primaryFile File
|
||||
func (i *Indexer) indexMediaFile(mediaFile *MediaFile) string {
|
||||
var photo models.Photo
|
||||
var file, primaryFile models.File
|
||||
var isPrimary = false
|
||||
var colorNames []string
|
||||
var tags []*Tag
|
||||
var tags []*models.Tag
|
||||
|
||||
canonicalName := mediaFile.GetCanonicalNameFromFile()
|
||||
fileHash := mediaFile.GetHash()
|
||||
|
@ -95,13 +100,13 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
|
|||
photo.PhotoColors = strings.Join(colorNames, ", ")
|
||||
|
||||
// Tags (TensorFlow)
|
||||
tags = i.GetImageTags(jpeg)
|
||||
tags = i.getImageTags(jpeg)
|
||||
}
|
||||
|
||||
if location, err := mediaFile.GetLocation(); err == nil {
|
||||
i.db.FirstOrCreate(location, "id = ?", location.ID)
|
||||
photo.Location = location
|
||||
photo.Country = NewCountry(location.LocCountryCode, location.LocCountry).FirstOrCreate(i.db)
|
||||
photo.Country = models.NewCountry(location.LocCountryCode, location.LocCountry).FirstOrCreate(i.db)
|
||||
|
||||
tags = i.appendTag(tags, location.LocCity)
|
||||
tags = i.appendTag(tags, location.LocCounty)
|
||||
|
@ -122,7 +127,7 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
|
|||
photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCounty, location.LocCountry, mediaFile.GetDateCreated().Format("2006"))
|
||||
}
|
||||
} else {
|
||||
var recentPhoto Photo
|
||||
var recentPhoto models.Photo
|
||||
|
||||
if result := i.db.Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", mediaFile.GetDateCreated())).Preload("Country").First(&recentPhoto); result.Error == nil {
|
||||
if recentPhoto.Country != nil {
|
||||
|
@ -143,8 +148,8 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
|
|||
}
|
||||
}
|
||||
|
||||
photo.Camera = NewCamera(mediaFile.GetCameraModel(), mediaFile.GetCameraMake()).FirstOrCreate(i.db)
|
||||
photo.Lens = NewLens(mediaFile.GetLensModel(), mediaFile.GetLensMake()).FirstOrCreate(i.db)
|
||||
photo.Camera = models.NewCamera(mediaFile.GetCameraModel(), mediaFile.GetCameraMake()).FirstOrCreate(i.db)
|
||||
photo.Lens = models.NewLens(mediaFile.GetLensModel(), mediaFile.GetLensMake()).FirstOrCreate(i.db)
|
||||
photo.PhotoFocalLength = mediaFile.GetFocalLength()
|
||||
photo.PhotoAperture = mediaFile.GetAperture()
|
||||
|
||||
|
@ -160,14 +165,14 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
|
|||
|
||||
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.Camera = models.NewCamera(mediaFile.GetCameraModel(), mediaFile.GetCameraMake()).FirstOrCreate(i.db)
|
||||
photo.Lens = models.NewLens(mediaFile.GetLensModel(), mediaFile.GetLensMake()).FirstOrCreate(i.db)
|
||||
photo.PhotoFocalLength = mediaFile.GetFocalLength()
|
||||
photo.PhotoAperture = mediaFile.GetAperture()
|
||||
}
|
||||
|
||||
if photo.LocationID == 0 {
|
||||
var recentPhoto Photo
|
||||
var recentPhoto models.Photo
|
||||
|
||||
if result := i.db.Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", photo.TakenAt)).Preload("Country").First(&recentPhoto); result.Error == nil {
|
||||
if recentPhoto.Country != nil {
|
||||
|
@ -211,13 +216,14 @@ func (i *Indexer) IndexMediaFile(mediaFile *MediaFile) string {
|
|||
|
||||
if fileQuery.Error == nil {
|
||||
i.db.Save(&file)
|
||||
return IndexResultUpdated
|
||||
} else {
|
||||
i.db.Create(&file)
|
||||
return IndexResultAdded
|
||||
return indexResultUpdated
|
||||
}
|
||||
|
||||
i.db.Create(&file)
|
||||
return indexResultAdded
|
||||
}
|
||||
|
||||
// IndexRelated will index all mediafiles which has relate to a given mediafile.
|
||||
func (i *Indexer) IndexRelated(mediaFile *MediaFile) map[string]bool {
|
||||
indexed := make(map[string]bool)
|
||||
|
||||
|
@ -229,7 +235,7 @@ func (i *Indexer) IndexRelated(mediaFile *MediaFile) map[string]bool {
|
|||
return indexed
|
||||
}
|
||||
|
||||
mainIndexResult := i.IndexMediaFile(mainFile)
|
||||
mainIndexResult := i.indexMediaFile(mainFile)
|
||||
indexed[mainFile.GetFilename()] = true
|
||||
|
||||
log.Printf("%s main %s file \"%s\"", mainIndexResult, mainFile.GetType(), mainFile.GetRelativeFilename(i.originalsPath))
|
||||
|
@ -239,7 +245,7 @@ func (i *Indexer) IndexRelated(mediaFile *MediaFile) map[string]bool {
|
|||
continue
|
||||
}
|
||||
|
||||
indexResult := i.IndexMediaFile(relatedMediaFile)
|
||||
indexResult := i.indexMediaFile(relatedMediaFile)
|
||||
indexed[relatedMediaFile.GetFilename()] = true
|
||||
|
||||
log.Printf("%s related %s file \"%s\"", indexResult, relatedMediaFile.GetType(), relatedMediaFile.GetRelativeFilename(i.originalsPath))
|
||||
|
@ -248,6 +254,7 @@ func (i *Indexer) IndexRelated(mediaFile *MediaFile) map[string]bool {
|
|||
return indexed
|
||||
}
|
||||
|
||||
// IndexAll will index all mediafiles.
|
||||
func (i *Indexer) IndexAll() map[string]bool {
|
||||
indexed := make(map[string]bool)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"encoding/hex"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/gif" // Import for image.
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
|
@ -19,25 +19,37 @@ import (
|
|||
|
||||
"github.com/brett-lempereur/ish"
|
||||
"github.com/djherbis/times"
|
||||
. "github.com/photoprism/photoprism/internal/models"
|
||||
"github.com/photoprism/photoprism/internal/models"
|
||||
"github.com/steakknife/hamming"
|
||||
)
|
||||
|
||||
const (
|
||||
// FileTypeOther is an unkown file format.
|
||||
FileTypeOther = "unknown"
|
||||
FileTypeYaml = "yml"
|
||||
FileTypeJpeg = "jpg"
|
||||
FileTypeRaw = "raw"
|
||||
FileTypeXmp = "xmp"
|
||||
FileTypeAae = "aae"
|
||||
// FileTypeYaml is a yaml file format.
|
||||
FileTypeYaml = "yml"
|
||||
// FileTypeJpeg is a jpeg file format.
|
||||
FileTypeJpeg = "jpg"
|
||||
// FileTypeRaw is a raw file format.
|
||||
FileTypeRaw = "raw"
|
||||
// FileTypeXmp is an xmp file format.
|
||||
FileTypeXmp = "xmp"
|
||||
// FileTypeAae is an aae file format.
|
||||
FileTypeAae = "aae"
|
||||
// FileTypeMovie is a movie file format.
|
||||
FileTypeMovie = "mov"
|
||||
FileTypeHEIF = "heif" // High Efficiency Image File Format
|
||||
// FileTypeHEIF High Efficiency Image File Format
|
||||
FileTypeHEIF = "heif" // High Efficiency Image File Format
|
||||
)
|
||||
|
||||
const (
|
||||
// MimeTypeJpeg is jpeg image type
|
||||
MimeTypeJpeg = "image/jpeg"
|
||||
// PerceptualHashSize defines the default hash size.
|
||||
PerceptualHashSize = 4
|
||||
)
|
||||
|
||||
// FileExtensions lists all the available and supported image file formats.
|
||||
var FileExtensions = map[string]string{
|
||||
".crw": FileTypeRaw,
|
||||
".cr2": FileTypeRaw,
|
||||
|
@ -93,6 +105,7 @@ var FileExtensions = map[string]string{
|
|||
".x3f": FileTypeRaw,
|
||||
}
|
||||
|
||||
// MediaFile represents a single file.
|
||||
type MediaFile struct {
|
||||
filename string
|
||||
dateCreated time.Time
|
||||
|
@ -104,9 +117,10 @@ type MediaFile struct {
|
|||
width int
|
||||
height int
|
||||
exifData *ExifData
|
||||
location *Location
|
||||
location *models.Location
|
||||
}
|
||||
|
||||
// NewMediaFile returns a new MediaFile.
|
||||
func NewMediaFile(filename string) (*MediaFile, error) {
|
||||
if !fileExists(filename) {
|
||||
return nil, fmt.Errorf("file does not exist: %s", filename)
|
||||
|
@ -120,6 +134,7 @@ func NewMediaFile(filename string) (*MediaFile, error) {
|
|||
return instance, nil
|
||||
}
|
||||
|
||||
// GetDateCreated returns the date on which a mediafile was created.
|
||||
func (m *MediaFile) GetDateCreated() time.Time {
|
||||
if !m.dateCreated.IsZero() {
|
||||
return m.dateCreated
|
||||
|
@ -152,6 +167,7 @@ func (m *MediaFile) GetDateCreated() time.Time {
|
|||
return m.dateCreated
|
||||
}
|
||||
|
||||
// GetCameraModel returns the camera model with which the mediafile was created.
|
||||
func (m *MediaFile) GetCameraModel() string {
|
||||
info, err := m.GetExifData()
|
||||
|
||||
|
@ -164,6 +180,7 @@ func (m *MediaFile) GetCameraModel() string {
|
|||
return result
|
||||
}
|
||||
|
||||
// GetCameraMake returns the make of the camera with which the file was created.
|
||||
func (m *MediaFile) GetCameraMake() string {
|
||||
info, err := m.GetExifData()
|
||||
|
||||
|
@ -176,6 +193,7 @@ func (m *MediaFile) GetCameraMake() string {
|
|||
return result
|
||||
}
|
||||
|
||||
// GetLensModel returns the lens model of a mediafile.
|
||||
func (m *MediaFile) GetLensModel() string {
|
||||
info, err := m.GetExifData()
|
||||
|
||||
|
@ -188,6 +206,7 @@ func (m *MediaFile) GetLensModel() string {
|
|||
return result
|
||||
}
|
||||
|
||||
// GetLensMake returns the make of the Lens.
|
||||
func (m *MediaFile) GetLensMake() string {
|
||||
info, err := m.GetExifData()
|
||||
|
||||
|
@ -200,6 +219,7 @@ func (m *MediaFile) GetLensMake() string {
|
|||
return result
|
||||
}
|
||||
|
||||
// GetFocalLength return the length of the focal for a file.
|
||||
func (m *MediaFile) GetFocalLength() float64 {
|
||||
info, err := m.GetExifData()
|
||||
|
||||
|
@ -212,6 +232,7 @@ func (m *MediaFile) GetFocalLength() float64 {
|
|||
return result
|
||||
}
|
||||
|
||||
// GetAperture returns the aperture with which the mediafile was created.
|
||||
func (m *MediaFile) GetAperture() float64 {
|
||||
info, err := m.GetExifData()
|
||||
|
||||
|
@ -224,6 +245,7 @@ func (m *MediaFile) GetAperture() float64 {
|
|||
return result
|
||||
}
|
||||
|
||||
// GetCanonicalName returns the canonical name of a mediafile.
|
||||
func (m *MediaFile) GetCanonicalName() string {
|
||||
var postfix string
|
||||
|
||||
|
@ -240,20 +262,24 @@ func (m *MediaFile) GetCanonicalName() string {
|
|||
return result
|
||||
}
|
||||
|
||||
// GetCanonicalNameFromFile returns the canonical name of a file derived from the image name.
|
||||
func (m *MediaFile) GetCanonicalNameFromFile() string {
|
||||
basename := filepath.Base(m.GetFilename())
|
||||
|
||||
if end := strings.Index(basename, "."); end != -1 {
|
||||
return basename[:end] // Length of canonical name: 16 + 12
|
||||
} else {
|
||||
return basename
|
||||
}
|
||||
|
||||
return basename
|
||||
}
|
||||
|
||||
// GetCanonicalNameFromFileWithDirectory gets the canonical name for a mediafile
|
||||
// including the directory.
|
||||
func (m *MediaFile) GetCanonicalNameFromFileWithDirectory() string {
|
||||
return m.GetDirectory() + string(os.PathSeparator) + m.GetCanonicalNameFromFile()
|
||||
}
|
||||
|
||||
// GetPerceptualHash returns the perceptual hash of a mediafile.
|
||||
func (m *MediaFile) GetPerceptualHash() (string, error) {
|
||||
if m.perceptualHash != "" {
|
||||
return m.perceptualHash, nil
|
||||
|
@ -277,30 +303,32 @@ func (m *MediaFile) GetPerceptualHash() (string, error) {
|
|||
return m.perceptualHash, nil
|
||||
}
|
||||
|
||||
// GetPerceptualDistance returns the perceptual distance for a mediafile.
|
||||
func (m *MediaFile) GetPerceptualDistance(perceptualHash string) (int, error) {
|
||||
var hash1, hash2 []byte
|
||||
imageHash, err := m.GetPerceptualHash()
|
||||
|
||||
if imageHash, err := m.GetPerceptualHash(); err != nil {
|
||||
if err != nil {
|
||||
return -1, err
|
||||
} else {
|
||||
if decoded, err := hex.DecodeString(imageHash); err != nil {
|
||||
return -1, err
|
||||
} else {
|
||||
hash1 = decoded
|
||||
}
|
||||
}
|
||||
|
||||
if decoded, err := hex.DecodeString(perceptualHash); err != nil {
|
||||
decodedImageHash, err := hex.DecodeString(imageHash)
|
||||
|
||||
if err != nil {
|
||||
return -1, err
|
||||
} else {
|
||||
hash2 = decoded
|
||||
}
|
||||
|
||||
result := hamming.Bytes(hash1, hash2)
|
||||
decodedPerceptualHash, err := hex.DecodeString(perceptualHash)
|
||||
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
result := hamming.Bytes(decodedImageHash, decodedPerceptualHash)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetHash return a sha1 hash of a mediafile based on the filename.
|
||||
func (m *MediaFile) GetHash() string {
|
||||
if len(m.hash) == 0 {
|
||||
m.hash = fileHash(m.GetFilename())
|
||||
|
@ -309,7 +337,7 @@ func (m *MediaFile) GetHash() string {
|
|||
return m.hash
|
||||
}
|
||||
|
||||
// When editing photos, iPhones create additional files like IMG_E12345.JPG
|
||||
// GetEditedFilename When editing photos, iPhones create additional files like IMG_E12345.JPG
|
||||
func (m *MediaFile) GetEditedFilename() (result string) {
|
||||
basename := filepath.Base(m.filename)
|
||||
|
||||
|
@ -320,6 +348,7 @@ func (m *MediaFile) GetEditedFilename() (result string) {
|
|||
return result
|
||||
}
|
||||
|
||||
// GetRelatedFiles returns the mediafiles which are related to a given mediafile.
|
||||
func (m *MediaFile) GetRelatedFiles() (result MediaFiles, mainFile *MediaFile, err error) {
|
||||
baseFilename := m.GetCanonicalNameFromFileWithDirectory()
|
||||
|
||||
|
@ -356,14 +385,17 @@ func (m *MediaFile) GetRelatedFiles() (result MediaFiles, mainFile *MediaFile, e
|
|||
return result, mainFile, nil
|
||||
}
|
||||
|
||||
// GetFilename returns the filename.
|
||||
func (m *MediaFile) GetFilename() string {
|
||||
return m.filename
|
||||
}
|
||||
|
||||
// SetFilename sets the filename to the given string.
|
||||
func (m *MediaFile) SetFilename(filename string) {
|
||||
m.filename = filename
|
||||
}
|
||||
|
||||
// GetRelativeFilename returns the relative filename.
|
||||
func (m *MediaFile) GetRelativeFilename(directory string) string {
|
||||
index := strings.Index(m.filename, directory)
|
||||
|
||||
|
@ -375,14 +407,17 @@ func (m *MediaFile) GetRelativeFilename(directory string) string {
|
|||
return m.filename
|
||||
}
|
||||
|
||||
// GetDirectory returns the directory
|
||||
func (m *MediaFile) GetDirectory() string {
|
||||
return filepath.Dir(m.filename)
|
||||
}
|
||||
|
||||
// GetBasename returns the basename.
|
||||
func (m *MediaFile) GetBasename() string {
|
||||
return filepath.Base(m.filename)
|
||||
}
|
||||
|
||||
// GetMimeType returns the mimetype.
|
||||
func (m *MediaFile) GetMimeType() string {
|
||||
if m.mimeType != "" {
|
||||
return m.mimeType
|
||||
|
@ -413,26 +448,31 @@ func (m *MediaFile) GetMimeType() string {
|
|||
}
|
||||
|
||||
func (m *MediaFile) openFile() (*os.File, error) {
|
||||
if handle, err := os.Open(m.filename); err == nil {
|
||||
return handle, nil
|
||||
} else {
|
||||
handle, err := os.Open(m.filename)
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
return handle, nil
|
||||
}
|
||||
|
||||
// Exists checks if a mediafile exists by filename.
|
||||
func (m *MediaFile) Exists() bool {
|
||||
return fileExists(m.GetFilename())
|
||||
}
|
||||
|
||||
// Remove a mediafile.
|
||||
func (m *MediaFile) Remove() error {
|
||||
return os.Remove(m.GetFilename())
|
||||
}
|
||||
|
||||
// HasSameFilename compares a mediafile with another mediafile and returns if
|
||||
// their filenames are matching or not.
|
||||
func (m *MediaFile) HasSameFilename(other *MediaFile) bool {
|
||||
return m.GetFilename() == other.GetFilename()
|
||||
}
|
||||
|
||||
// Move a mediafile to a new file with the filename provided in parameter.
|
||||
func (m *MediaFile) Move(newFilename string) error {
|
||||
if err := os.Rename(m.filename, newFilename); err != nil {
|
||||
return err
|
||||
|
@ -443,6 +483,7 @@ func (m *MediaFile) Move(newFilename string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Copy a mediafile to another file by destinationFilename.
|
||||
func (m *MediaFile) Copy(destinationFilename string) error {
|
||||
file, err := m.openFile()
|
||||
|
||||
|
@ -472,10 +513,12 @@ func (m *MediaFile) Copy(destinationFilename string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetExtension returns the extension of a mediafile.
|
||||
func (m *MediaFile) GetExtension() string {
|
||||
return strings.ToLower(filepath.Ext(m.filename))
|
||||
}
|
||||
|
||||
// IsJpeg return true if the given mediafile is of mimetype Jpeg.
|
||||
func (m *MediaFile) IsJpeg() bool {
|
||||
// Don't import/use existing thumbnail files (we create our own)
|
||||
if m.GetExtension() == ".thm" {
|
||||
|
@ -485,10 +528,12 @@ func (m *MediaFile) IsJpeg() bool {
|
|||
return m.GetMimeType() == MimeTypeJpeg
|
||||
}
|
||||
|
||||
// GetType returns the type of the mediafile.
|
||||
func (m *MediaFile) GetType() string {
|
||||
return FileExtensions[m.GetExtension()]
|
||||
}
|
||||
|
||||
// HasType checks whether a mediafile is of a given type.
|
||||
func (m *MediaFile) HasType(typeString string) bool {
|
||||
if typeString == FileTypeJpeg {
|
||||
return m.IsJpeg()
|
||||
|
@ -497,18 +542,23 @@ func (m *MediaFile) HasType(typeString string) bool {
|
|||
return m.GetType() == typeString
|
||||
}
|
||||
|
||||
// IsRaw check whether the given mediafile is of Raw type.
|
||||
func (m *MediaFile) IsRaw() bool {
|
||||
return m.HasType(FileTypeRaw)
|
||||
}
|
||||
|
||||
// IsHighEfficiencyImageFile check if a given mediafile is of HEIF type.
|
||||
func (m *MediaFile) IsHighEfficiencyImageFile() bool {
|
||||
return m.HasType(FileTypeHEIF)
|
||||
}
|
||||
|
||||
// IsPhoto checks if a mediafile is a photo.
|
||||
func (m *MediaFile) IsPhoto() bool {
|
||||
return m.IsJpeg() || m.IsRaw() || m.IsHighEfficiencyImageFile()
|
||||
}
|
||||
|
||||
// GetJpeg returns a new mediafile given the current one's canonical name
|
||||
// plus the extension .jpg.
|
||||
func (m *MediaFile) GetJpeg() (*MediaFile, error) {
|
||||
if m.IsJpeg() {
|
||||
return m, nil
|
||||
|
@ -553,6 +603,7 @@ func (m *MediaFile) decodeDimensions() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetWidth return the width dimension of a mediafile.
|
||||
func (m *MediaFile) GetWidth() int {
|
||||
if m.width <= 0 {
|
||||
m.decodeDimensions()
|
||||
|
@ -561,6 +612,7 @@ func (m *MediaFile) GetWidth() int {
|
|||
return m.width
|
||||
}
|
||||
|
||||
// GetHeight returns the height dimension of a mediafile.
|
||||
func (m *MediaFile) GetHeight() int {
|
||||
if m.height <= 0 {
|
||||
m.decodeDimensions()
|
||||
|
@ -569,6 +621,7 @@ func (m *MediaFile) GetHeight() int {
|
|||
return m.height
|
||||
}
|
||||
|
||||
// GetAspectRatio returns the aspect ratio of a mediafile.
|
||||
func (m *MediaFile) GetAspectRatio() float64 {
|
||||
width := float64(m.GetWidth())
|
||||
height := float64(m.GetHeight())
|
||||
|
@ -582,10 +635,11 @@ func (m *MediaFile) GetAspectRatio() float64 {
|
|||
return math.Round(aspectRatio*100) / 100
|
||||
}
|
||||
|
||||
// GetOrientation returns the orientation of a mediafile.
|
||||
func (m *MediaFile) GetOrientation() int {
|
||||
if exif, err := m.GetExifData(); err == nil {
|
||||
return exif.Orientation
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
package photoprism
|
||||
|
||||
// MediaFiles provides a Collection for mediafiles.
|
||||
type MediaFiles []*MediaFile
|
||||
|
||||
// Len returns the length of the mediafile collection.
|
||||
func (f MediaFiles) Len() int {
|
||||
return len(f)
|
||||
}
|
||||
|
||||
// Less compares two mediafiles based on filename length.
|
||||
func (f MediaFiles) Less(i, j int) bool {
|
||||
fileName1 := f[i].GetFilename()
|
||||
fileName2 := f[j].GetFilename()
|
||||
|
||||
if len(fileName1) == len(fileName2) {
|
||||
return fileName1 < fileName2
|
||||
} else {
|
||||
return len(fileName1) < len(fileName2)
|
||||
}
|
||||
|
||||
return len(fileName1) < len(fileName2)
|
||||
}
|
||||
|
||||
// Swap changes two files order around.
|
||||
func (f MediaFiles) Swap(i, j int) {
|
||||
f[i], f[j] = f[j], f[i]
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
. "github.com/photoprism/photoprism/internal/models"
|
||||
"github.com/photoprism/photoprism/internal/models"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type OpenstreetmapAddress struct {
|
||||
type openstreetmapAddress struct {
|
||||
HouseNumber string `json:"house_number"`
|
||||
Road string `json:"road"`
|
||||
Suburb string `json:"suburb"`
|
||||
|
@ -24,27 +24,27 @@ type OpenstreetmapAddress struct {
|
|||
CountryCode string `json:"country_code"`
|
||||
}
|
||||
|
||||
type OpenstreetmapLocation struct {
|
||||
PlaceId string `json:"place_id"`
|
||||
type openstreetmapLocation struct {
|
||||
PlaceID string `json:"place_id"`
|
||||
Lat string `json:"lat"`
|
||||
Lon string `json:"lon"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Type string `json:"type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Address *OpenstreetmapAddress `json:"address"`
|
||||
Address *openstreetmapAddress `json:"address"`
|
||||
}
|
||||
|
||||
// See https://wiki.openstreetmap.org/wiki/Nominatim#Reverse_Geocoding
|
||||
func (m *MediaFile) GetLocation() (*Location, error) {
|
||||
// GetLocation See https://wiki.openstreetmap.org/wiki/Nominatim#Reverse_Geocoding
|
||||
func (m *MediaFile) GetLocation() (*models.Location, error) {
|
||||
if m.location != nil {
|
||||
return m.location, nil
|
||||
}
|
||||
|
||||
location := &Location{}
|
||||
location := &models.Location{}
|
||||
|
||||
openstreetmapLocation := &OpenstreetmapLocation{
|
||||
Address: &OpenstreetmapAddress{},
|
||||
openstreetmapLocation := &openstreetmapLocation{
|
||||
Address: &openstreetmapAddress{},
|
||||
}
|
||||
|
||||
if exifData, err := m.GetExifData(); err == nil {
|
||||
|
@ -59,7 +59,7 @@ func (m *MediaFile) GetLocation() (*Location, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if id, err := strconv.Atoi(openstreetmapLocation.PlaceId); err == nil && id > 0 {
|
||||
if id, err := strconv.Atoi(openstreetmapLocation.PlaceID); err == nil && id > 0 {
|
||||
location.ID = uint(id)
|
||||
} else {
|
||||
return nil, errors.New("no location found")
|
||||
|
|
|
@ -5,19 +5,22 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
. "github.com/photoprism/photoprism/internal/forms"
|
||||
. "github.com/photoprism/photoprism/internal/models"
|
||||
"github.com/photoprism/photoprism/internal/forms"
|
||||
"github.com/photoprism/photoprism/internal/models"
|
||||
)
|
||||
|
||||
// Search searches given an originals path and a db instance.
|
||||
type Search struct {
|
||||
originalsPath string
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// SearchCount is the total number of search hits.
|
||||
type SearchCount struct {
|
||||
Total int
|
||||
}
|
||||
|
||||
// PhotoSearchResult is a found mediafile.
|
||||
type PhotoSearchResult struct {
|
||||
// Photo
|
||||
ID uint
|
||||
|
@ -82,6 +85,7 @@ type PhotoSearchResult struct {
|
|||
Tags string
|
||||
}
|
||||
|
||||
// NewSearch returns a new Search type with a given path and db instance.
|
||||
func NewSearch(originalsPath string, db *gorm.DB) *Search {
|
||||
instance := &Search{
|
||||
originalsPath: originalsPath,
|
||||
|
@ -91,7 +95,8 @@ func NewSearch(originalsPath string, db *gorm.DB) *Search {
|
|||
return instance
|
||||
}
|
||||
|
||||
func (s *Search) Photos(form PhotoSearchForm) ([]PhotoSearchResult, error) {
|
||||
// Photos searches for photos based on a Form and returns a PhotoSearchResult slice.
|
||||
func (s *Search) Photos(form forms.PhotoSearchForm) ([]PhotoSearchResult, error) {
|
||||
q := s.db.NewScope(nil).DB()
|
||||
q = q.Table("photos").
|
||||
Select(`SQL_CALC_FOUND_ROWS photos.*,
|
||||
|
@ -177,26 +182,31 @@ func (s *Search) Photos(form PhotoSearchForm) ([]PhotoSearchResult, error) {
|
|||
return results, nil
|
||||
}
|
||||
|
||||
func (s *Search) FindFiles(count int, offset int) (files []File) {
|
||||
s.db.Where(&File{}).Limit(count).Offset(offset).Find(&files)
|
||||
// FindFiles finds files returning maximum results defined by limit
|
||||
// and finding them from an offest defined by offset.
|
||||
func (s *Search) FindFiles(limit int, offset int) (files []models.File) {
|
||||
s.db.Where(&models.File{}).Limit(limit).Offset(offset).Find(&files)
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
func (s *Search) FindFileById(id string) (file File) {
|
||||
// FindFileByID returns a mediafile given a certain ID.
|
||||
func (s *Search) FindFileByID(id string) (file models.File) {
|
||||
s.db.Where("id = ?", id).First(&file)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func (s *Search) FindFileByHash(fileHash string) (file File) {
|
||||
// FindFileByHash finds a file with a given hash string.
|
||||
func (s *Search) FindFileByHash(fileHash string) (file models.File) {
|
||||
s.db.Where("file_hash = ?", fileHash).First(&file)
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
func (s *Search) FindPhotoById(photoId uint64) (photo Photo) {
|
||||
s.db.Where("id = ?", photoId).First(&photo)
|
||||
// FindPhotoByID returns a Photo based on the ID.
|
||||
func (s *Search) FindPhotoByID(photoID uint64) (photo models.Photo) {
|
||||
s.db.Where("id = ?", photoID).First(&photo)
|
||||
|
||||
return photo
|
||||
}
|
||||
|
|
|
@ -12,27 +12,32 @@ import (
|
|||
"github.com/tensorflow/tensorflow/tensorflow/go/op"
|
||||
)
|
||||
|
||||
// TensorFlow if a tensorflow wrapper given a graph, labels and a modelPath.
|
||||
type TensorFlow struct {
|
||||
modelPath string
|
||||
graph *tf.Graph
|
||||
labels []string
|
||||
}
|
||||
|
||||
// NewTensorFlow returns a new TensorFlow.
|
||||
func NewTensorFlow(tensorFlowModelPath string) *TensorFlow {
|
||||
return &TensorFlow{modelPath: tensorFlowModelPath}
|
||||
}
|
||||
|
||||
// TensorFlowLabel defines a Json struct with label and probability.
|
||||
type TensorFlowLabel struct {
|
||||
Label string `json:"label"`
|
||||
Probability float32 `json:"probability"`
|
||||
}
|
||||
|
||||
// TensorFlowLabels is a slice of tensorflow labels.
|
||||
type TensorFlowLabels []TensorFlowLabel
|
||||
|
||||
func (a TensorFlowLabels) Len() int { return len(a) }
|
||||
func (a TensorFlowLabels) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a TensorFlowLabels) Less(i, j int) bool { return a[i].Probability > a[j].Probability }
|
||||
|
||||
// GetImageTagsFromFile returns a slice of tags given a mediafile filename.
|
||||
func (t *TensorFlow) GetImageTagsFromFile(filename string) (result []TensorFlowLabel, err error) {
|
||||
imageBuffer, err := ioutil.ReadFile(filename)
|
||||
|
||||
|
@ -43,6 +48,7 @@ func (t *TensorFlow) GetImageTagsFromFile(filename string) (result []TensorFlowL
|
|||
return t.GetImageTags(string(imageBuffer))
|
||||
}
|
||||
|
||||
// GetImageTags returns the tags for a given image.
|
||||
func (t *TensorFlow) GetImageTags(image string) (result []TensorFlowLabel, err error) {
|
||||
if err := t.loadModel(); err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
// CreateThumbnailsFromOriginals create thumbnails.
|
||||
func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string, size int, square bool) {
|
||||
err := filepath.Walk(originalsPath, func(filename string, fileInfo os.FileInfo, err error) error {
|
||||
if err != nil || fileInfo.IsDir() || strings.HasPrefix(filepath.Base(filename), ".") {
|
||||
|
@ -44,6 +45,7 @@ func CreateThumbnailsFromOriginals(originalsPath string, thumbnailsPath string,
|
|||
}
|
||||
}
|
||||
|
||||
// GetThumbnail get the thumbnail for a path.
|
||||
func (m *MediaFile) GetThumbnail(path string, size int) (result *MediaFile, err error) {
|
||||
canonicalName := m.GetCanonicalName()
|
||||
dateCreated := m.GetDateCreated()
|
||||
|
@ -61,7 +63,7 @@ func (m *MediaFile) GetThumbnail(path string, size int) (result *MediaFile, err
|
|||
return m.CreateThumbnail(thumbnailFilename, size)
|
||||
}
|
||||
|
||||
// Resize preserving the aspect ratio
|
||||
// CreateThumbnail Resize preserving the aspect ratio
|
||||
func (m *MediaFile) CreateThumbnail(filename string, size int) (result *MediaFile, err error) {
|
||||
img, err := imaging.Open(m.filename, imaging.AutoOrientation(true))
|
||||
|
||||
|
@ -82,6 +84,7 @@ func (m *MediaFile) CreateThumbnail(filename string, size int) (result *MediaFil
|
|||
return NewMediaFile(filename)
|
||||
}
|
||||
|
||||
// GetSquareThumbnail return the square thumbnail for a path and size.
|
||||
func (m *MediaFile) GetSquareThumbnail(path string, size int) (result *MediaFile, err error) {
|
||||
canonicalName := m.GetCanonicalName()
|
||||
dateCreated := m.GetDateCreated()
|
||||
|
@ -99,7 +102,7 @@ func (m *MediaFile) GetSquareThumbnail(path string, size int) (result *MediaFile
|
|||
return m.CreateSquareThumbnail(thumbnailFilename, size)
|
||||
}
|
||||
|
||||
// Resize and crop to square format
|
||||
// CreateSquareThumbnail Resize and crop to square format
|
||||
func (m *MediaFile) CreateSquareThumbnail(filename string, size int) (result *MediaFile, err error) {
|
||||
img, err := imaging.Open(m.filename, imaging.AutoOrientation(true))
|
||||
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetExpandedFilename(filename string) string {
|
||||
usr, _ := user.Current()
|
||||
dir := usr.HomeDir
|
||||
|
||||
if filename == "" {
|
||||
panic("filename was empty")
|
||||
}
|
||||
|
||||
if len(filename) > 2 && filename[:2] == "~/" {
|
||||
filename = filepath.Join(dir, filename[2:])
|
||||
}
|
||||
|
||||
result, _ := filepath.Abs(filename)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getRandomInt(min, max int) int {
|
||||
rand.Seed(time.Now().Unix())
|
||||
return rand.Intn(max-min) + min
|
||||
}
|
||||
|
||||
func fileExists(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
func pathExists(pathname string) bool {
|
||||
info, err := os.Stat(pathname)
|
||||
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
func fileHash(filename string) string {
|
||||
var result []byte
|
||||
|
||||
file, err := os.Open(filename)
|
||||
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
hash := sha1.New()
|
||||
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hash.Sum(result))
|
||||
}
|
||||
|
||||
func directoryIsEmpty(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.Readdirnames(1)
|
||||
|
||||
if err == io.EOF {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func unzip(src, dest string) ([]string, error) {
|
||||
|
||||
var filenames []string
|
||||
|
||||
r, err := zip.OpenReader(src)
|
||||
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
// Skip directories like __OSX
|
||||
if strings.HasPrefix(f.Name, "__") {
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
defer rc.Close()
|
||||
|
||||
// Store filename/path for returning and using later on
|
||||
fpath := filepath.Join(dest, f.Name)
|
||||
filenames = append(filenames, fpath)
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
|
||||
// Make Folder
|
||||
os.MkdirAll(fpath, os.ModePerm)
|
||||
|
||||
} else {
|
||||
|
||||
// Make File
|
||||
var fdir string
|
||||
if lastIndex := strings.LastIndex(fpath, string(os.PathSeparator)); lastIndex > -1 {
|
||||
fdir = fpath[:lastIndex]
|
||||
}
|
||||
|
||||
err = os.MkdirAll(fdir, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return filenames, err
|
||||
}
|
||||
f, err := os.OpenFile(
|
||||
fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.Copy(f, rc)
|
||||
if err != nil {
|
||||
return filenames, err
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return filenames, nil
|
||||
}
|
||||
|
||||
func downloadFile(filepath string, url string) (err error) {
|
||||
// Create the file
|
||||
out, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Get the data
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check server response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Writer the body to file
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uniqueStrings(input []string) []string {
|
||||
u := make([]string, 0, len(input))
|
||||
m := make(map[string]bool)
|
||||
|
||||
for _, val := range input {
|
||||
if !m[val] && val != "" {
|
||||
m[val] = true
|
||||
u = append(u, val)
|
||||
}
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetRandomInt(t *testing.T) {
|
||||
min := 5
|
||||
max := 50
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
result := getRandomInt(min, max)
|
||||
|
||||
if result > max {
|
||||
t.Errorf("Random result must not be bigger than %d", max)
|
||||
} else if result < min {
|
||||
t.Errorf("Random result must not be smaller than %d", min)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueStrings(t *testing.T) {
|
||||
input := []string{"zzz", "AAA", "ZZZ", "aaa", "foo", "1", "", "zzz", "AAA", "ZZZ", "aaa"}
|
||||
|
||||
output := uniqueStrings(input)
|
||||
|
||||
expected := []string{"foo", "1", "zzz", "AAA", "ZZZ", "aaa"}
|
||||
|
||||
assert.ElementsMatch(t, expected, output)
|
||||
}
|
Loading…
Reference in a new issue