Images section (#813)

* Add new configuration options
* Refactor scan/clean
* Schema changes
* Add details to galleries
* Remove redundant code
* Refine thumbnail generation
* Gallery overhaul
* Don't allow modifying zip gallery images
* Show gallery card overlays
* Hide zoom slider when not in grid mode
This commit is contained in:
WithoutPants
2020-10-13 10:12:46 +11:00
committed by GitHub
parent df3252e24f
commit aca2c7c5f4
147 changed files with 12483 additions and 946 deletions

81
pkg/image/export.go Normal file
View File

@@ -0,0 +1,81 @@
package image
import (
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
)
// ToBasicJSON converts a image object into its JSON object equivalent. It
// does not convert the relationships to other objects, with the exception
// of cover image.
func ToBasicJSON(image *models.Image) *jsonschema.Image {
newImageJSON := jsonschema.Image{
Checksum: image.Checksum,
CreatedAt: models.JSONTime{Time: image.CreatedAt.Timestamp},
UpdatedAt: models.JSONTime{Time: image.UpdatedAt.Timestamp},
}
if image.Title.Valid {
newImageJSON.Title = image.Title.String
}
if image.Rating.Valid {
newImageJSON.Rating = int(image.Rating.Int64)
}
newImageJSON.OCounter = image.OCounter
newImageJSON.File = getImageFileJSON(image)
return &newImageJSON
}
func getImageFileJSON(image *models.Image) *jsonschema.ImageFile {
ret := &jsonschema.ImageFile{}
if image.Size.Valid {
ret.Size = int(image.Size.Int64)
}
if image.Width.Valid {
ret.Width = int(image.Width.Int64)
}
if image.Height.Valid {
ret.Height = int(image.Height.Int64)
}
return ret
}
// GetStudioName returns the name of the provided image's studio. It returns an
// empty string if there is no studio assigned to the image.
func GetStudioName(reader models.StudioReader, image *models.Image) (string, error) {
if image.StudioID.Valid {
studio, err := reader.Find(int(image.StudioID.Int64))
if err != nil {
return "", err
}
if studio != nil {
return studio.Name.String, nil
}
}
return "", nil
}
// GetGalleryChecksum returns the checksum of the provided image. It returns an
// empty string if there is no gallery assigned to the image.
// func GetGalleryChecksum(reader models.GalleryReader, image *models.Image) (string, error) {
// gallery, err := reader.FindByImageID(image.ID)
// if err != nil {
// return "", fmt.Errorf("error getting image gallery: %s", err.Error())
// }
// if gallery != nil {
// return gallery.Checksum, nil
// }
// return "", nil
// }

248
pkg/image/export_test.go Normal file
View File

@@ -0,0 +1,248 @@
package image
import (
"errors"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stashapp/stash/pkg/models/modelstest"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
const (
imageID = 1
noImageID = 2
errImageID = 3
studioID = 4
missingStudioID = 5
errStudioID = 6
// noGalleryID = 7
// errGalleryID = 8
noTagsID = 11
errTagsID = 12
noMoviesID = 13
errMoviesID = 14
errFindMovieID = 15
noMarkersID = 16
errMarkersID = 17
errFindPrimaryTagID = 18
errFindByMarkerID = 19
)
const (
checksum = "checksum"
title = "title"
rating = 5
ocounter = 2
size = 123
width = 100
height = 100
)
const (
studioName = "studioName"
//galleryChecksum = "galleryChecksum"
)
var names = []string{
"name1",
"name2",
}
var createTime time.Time = time.Date(2001, 01, 01, 0, 0, 0, 0, time.UTC)
var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC)
func createFullImage(id int) models.Image {
return models.Image{
ID: id,
Title: modelstest.NullString(title),
Checksum: checksum,
Height: modelstest.NullInt64(height),
OCounter: ocounter,
Rating: modelstest.NullInt64(rating),
Size: modelstest.NullInt64(int64(size)),
Width: modelstest.NullInt64(width),
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
}
}
func createEmptyImage(id int) models.Image {
return models.Image{
ID: id,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
UpdatedAt: models.SQLiteTimestamp{
Timestamp: updateTime,
},
}
}
func createFullJSONImage() *jsonschema.Image {
return &jsonschema.Image{
Title: title,
Checksum: checksum,
OCounter: ocounter,
Rating: rating,
File: &jsonschema.ImageFile{
Height: height,
Size: size,
Width: width,
},
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
}
}
func createEmptyJSONImage() *jsonschema.Image {
return &jsonschema.Image{
File: &jsonschema.ImageFile{},
CreatedAt: models.JSONTime{
Time: createTime,
},
UpdatedAt: models.JSONTime{
Time: updateTime,
},
}
}
type basicTestScenario struct {
input models.Image
expected *jsonschema.Image
}
var scenarios = []basicTestScenario{
{
createFullImage(imageID),
createFullJSONImage(),
},
}
func TestToJSON(t *testing.T) {
for i, s := range scenarios {
image := s.input
json := ToBasicJSON(&image)
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
func createStudioImage(studioID int) models.Image {
return models.Image{
StudioID: modelstest.NullInt64(int64(studioID)),
}
}
type stringTestScenario struct {
input models.Image
expected string
err bool
}
var getStudioScenarios = []stringTestScenario{
{
createStudioImage(studioID),
studioName,
false,
},
{
createStudioImage(missingStudioID),
"",
false,
},
{
createStudioImage(errStudioID),
"",
true,
},
}
func TestGetStudioName(t *testing.T) {
mockStudioReader := &mocks.StudioReaderWriter{}
studioErr := errors.New("error getting image")
mockStudioReader.On("Find", studioID).Return(&models.Studio{
Name: modelstest.NullString(studioName),
}, nil).Once()
mockStudioReader.On("Find", missingStudioID).Return(nil, nil).Once()
mockStudioReader.On("Find", errStudioID).Return(nil, studioErr).Once()
for i, s := range getStudioScenarios {
image := s.input
json, err := GetStudioName(mockStudioReader, &image)
if !s.err && err != nil {
t.Errorf("[%d] unexpected error: %s", i, err.Error())
} else if s.err && err == nil {
t.Errorf("[%d] expected error not returned", i)
} else {
assert.Equal(t, s.expected, json, "[%d]", i)
}
}
mockStudioReader.AssertExpectations(t)
}
// var getGalleryChecksumScenarios = []stringTestScenario{
// {
// createEmptyImage(imageID),
// galleryChecksum,
// false,
// },
// {
// createEmptyImage(noGalleryID),
// "",
// false,
// },
// {
// createEmptyImage(errGalleryID),
// "",
// true,
// },
// }
// func TestGetGalleryChecksum(t *testing.T) {
// mockGalleryReader := &mocks.GalleryReaderWriter{}
// galleryErr := errors.New("error getting gallery")
// mockGalleryReader.On("FindByImageID", imageID).Return(&models.Gallery{
// Checksum: galleryChecksum,
// }, nil).Once()
// mockGalleryReader.On("FindByImageID", noGalleryID).Return(nil, nil).Once()
// mockGalleryReader.On("FindByImageID", errGalleryID).Return(nil, galleryErr).Once()
// for i, s := range getGalleryChecksumScenarios {
// image := s.input
// json, err := GetGalleryChecksum(mockGalleryReader, &image)
// if !s.err && err != nil {
// t.Errorf("[%d] unexpected error: %s", i, err.Error())
// } else if s.err && err == nil {
// t.Errorf("[%d] expected error not returned", i)
// } else {
// assert.Equal(t, s.expected, json, "[%d]", i)
// }
// }
// mockGalleryReader.AssertExpectations(t)
// }

216
pkg/image/image.go Normal file
View File

@@ -0,0 +1,216 @@
package image
import (
"archive/zip"
"database/sql"
"fmt"
"image"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
_ "golang.org/x/image/webp"
)
const zipSeparator = "\x00"
func GetSourceImage(i *models.Image) (image.Image, error) {
f, err := openSourceImage(i.Path)
if err != nil {
return nil, err
}
defer f.Close()
srcImage, _, err := image.Decode(f)
if err != nil {
return nil, err
}
return srcImage, nil
}
func CalculateMD5(path string) (string, error) {
f, err := openSourceImage(path)
if err != nil {
return "", err
}
defer f.Close()
return utils.MD5FromReader(f)
}
func FileExists(path string) bool {
_, err := openSourceImage(path)
if err != nil {
return false
}
return true
}
func ZipFilename(zipFilename, filenameInZip string) string {
return zipFilename + zipSeparator + filenameInZip
}
type imageReadCloser struct {
src io.ReadCloser
zrc *zip.ReadCloser
}
func (i *imageReadCloser) Read(p []byte) (n int, err error) {
return i.src.Read(p)
}
func (i *imageReadCloser) Close() error {
err := i.src.Close()
var err2 error
if i.zrc != nil {
err2 = i.zrc.Close()
}
if err != nil {
return err
}
return err2
}
func openSourceImage(path string) (io.ReadCloser, error) {
// may need to read from a zip file
zipFilename, filename := getFilePath(path)
if zipFilename != "" {
r, err := zip.OpenReader(zipFilename)
if err != nil {
return nil, err
}
// find the file matching the filename
for _, f := range r.File {
if f.Name == filename {
src, err := f.Open()
if err != nil {
return nil, err
}
return &imageReadCloser{
src: src,
zrc: r,
}, nil
}
}
return nil, fmt.Errorf("file with name '%s' not found in zip file '%s'", filename, zipFilename)
}
return os.Open(filename)
}
func getFilePath(path string) (zipFilename, filename string) {
nullIndex := strings.Index(path, zipSeparator)
if nullIndex != -1 {
zipFilename = path[0:nullIndex]
filename = path[nullIndex+1:]
} else {
filename = path
}
return
}
func SetFileDetails(i *models.Image) error {
f, err := stat(i.Path)
if err != nil {
return err
}
src, _ := GetSourceImage(i)
if src != nil {
i.Width = sql.NullInt64{
Int64: int64(src.Bounds().Max.X),
Valid: true,
}
i.Height = sql.NullInt64{
Int64: int64(src.Bounds().Max.Y),
Valid: true,
}
}
i.Size = sql.NullInt64{
Int64: int64(f.Size()),
Valid: true,
}
return nil
}
func stat(path string) (os.FileInfo, error) {
// may need to read from a zip file
zipFilename, filename := getFilePath(path)
if zipFilename != "" {
r, err := zip.OpenReader(zipFilename)
if err != nil {
return nil, err
}
defer r.Close()
// find the file matching the filename
for _, f := range r.File {
if f.Name == filename {
return f.FileInfo(), nil
}
}
return nil, fmt.Errorf("file with name '%s' not found in zip file '%s'", filename, zipFilename)
}
return os.Stat(filename)
}
// PathDisplayName converts an image path for display. It translates the zip
// file separator character into '/', since this character is also used for
// path separators within zip files. It returns the original provided path
// if it does not contain the zip file separator character.
func PathDisplayName(path string) string {
return strings.Replace(path, zipSeparator, "/", -1)
}
func Serve(w http.ResponseWriter, r *http.Request, path string) {
zipFilename, _ := getFilePath(path)
w.Header().Add("Cache-Control", "max-age=604800000") // 1 Week
if zipFilename == "" {
http.ServeFile(w, r, path)
} else {
rc, err := openSourceImage(path)
if err != nil {
// assume not found
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
defer rc.Close()
data, err := ioutil.ReadAll(rc)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(data)
}
}
func IsCover(img *models.Image) bool {
_, fn := getFilePath(img.Path)
return fn == "cover.jpg"
}
func GetTitle(s *models.Image) string {
if s.Title.String != "" {
return s.Title.String
}
_, fn := getFilePath(s.Path)
return filepath.Base(fn)
}

366
pkg/image/import.go Normal file
View File

@@ -0,0 +1,366 @@
package image
import (
"database/sql"
"fmt"
"strings"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
)
type Importer struct {
ReaderWriter models.ImageReaderWriter
StudioWriter models.StudioReaderWriter
GalleryWriter models.GalleryReaderWriter
PerformerWriter models.PerformerReaderWriter
TagWriter models.TagReaderWriter
JoinWriter models.JoinReaderWriter
Input jsonschema.Image
Path string
MissingRefBehaviour models.ImportMissingRefEnum
ID int
image models.Image
galleries []*models.Gallery
performers []*models.Performer
tags []*models.Tag
}
func (i *Importer) PreImport() error {
i.image = i.imageJSONToImage(i.Input)
if err := i.populateStudio(); err != nil {
return err
}
if err := i.populateGalleries(); err != nil {
return err
}
if err := i.populatePerformers(); err != nil {
return err
}
if err := i.populateTags(); err != nil {
return err
}
return nil
}
func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image {
newImage := models.Image{
Checksum: imageJSON.Checksum,
Path: i.Path,
}
if imageJSON.Title != "" {
newImage.Title = sql.NullString{String: imageJSON.Title, Valid: true}
}
if imageJSON.Rating != 0 {
newImage.Rating = sql.NullInt64{Int64: int64(imageJSON.Rating), Valid: true}
}
newImage.OCounter = imageJSON.OCounter
newImage.CreatedAt = models.SQLiteTimestamp{Timestamp: imageJSON.CreatedAt.GetTime()}
newImage.UpdatedAt = models.SQLiteTimestamp{Timestamp: imageJSON.UpdatedAt.GetTime()}
if imageJSON.File != nil {
if imageJSON.File.Size != 0 {
newImage.Size = sql.NullInt64{Int64: int64(imageJSON.File.Size), Valid: true}
}
if imageJSON.File.Width != 0 {
newImage.Width = sql.NullInt64{Int64: int64(imageJSON.File.Width), Valid: true}
}
if imageJSON.File.Height != 0 {
newImage.Height = sql.NullInt64{Int64: int64(imageJSON.File.Height), Valid: true}
}
}
return newImage
}
func (i *Importer) populateStudio() error {
if i.Input.Studio != "" {
studio, err := i.StudioWriter.FindByName(i.Input.Studio, false)
if err != nil {
return fmt.Errorf("error finding studio by name: %s", err.Error())
}
if studio == nil {
if i.MissingRefBehaviour == models.ImportMissingRefEnumFail {
return fmt.Errorf("image studio '%s' not found", i.Input.Studio)
}
if i.MissingRefBehaviour == models.ImportMissingRefEnumIgnore {
return nil
}
if i.MissingRefBehaviour == models.ImportMissingRefEnumCreate {
studioID, err := i.createStudio(i.Input.Studio)
if err != nil {
return err
}
i.image.StudioID = sql.NullInt64{
Int64: int64(studioID),
Valid: true,
}
}
} else {
i.image.StudioID = sql.NullInt64{Int64: int64(studio.ID), Valid: true}
}
}
return nil
}
func (i *Importer) createStudio(name string) (int, error) {
newStudio := *models.NewStudio(name)
created, err := i.StudioWriter.Create(newStudio)
if err != nil {
return 0, err
}
return created.ID, nil
}
func (i *Importer) populateGalleries() error {
for _, checksum := range i.Input.Galleries {
gallery, err := i.GalleryWriter.FindByChecksum(checksum)
if err != nil {
return fmt.Errorf("error finding gallery: %s", err.Error())
}
if gallery == nil {
if i.MissingRefBehaviour == models.ImportMissingRefEnumFail {
return fmt.Errorf("image gallery '%s' not found", i.Input.Studio)
}
// we don't create galleries - just ignore
if i.MissingRefBehaviour == models.ImportMissingRefEnumIgnore || i.MissingRefBehaviour == models.ImportMissingRefEnumCreate {
continue
}
} else {
i.galleries = append(i.galleries, gallery)
}
}
return nil
}
func (i *Importer) populatePerformers() error {
if len(i.Input.Performers) > 0 {
names := i.Input.Performers
performers, err := i.PerformerWriter.FindByNames(names, false)
if err != nil {
return err
}
var pluckedNames []string
for _, performer := range performers {
if !performer.Name.Valid {
continue
}
pluckedNames = append(pluckedNames, performer.Name.String)
}
missingPerformers := utils.StrFilter(names, func(name string) bool {
return !utils.StrInclude(pluckedNames, name)
})
if len(missingPerformers) > 0 {
if i.MissingRefBehaviour == models.ImportMissingRefEnumFail {
return fmt.Errorf("image performers [%s] not found", strings.Join(missingPerformers, ", "))
}
if i.MissingRefBehaviour == models.ImportMissingRefEnumCreate {
createdPerformers, err := i.createPerformers(missingPerformers)
if err != nil {
return fmt.Errorf("error creating image performers: %s", err.Error())
}
performers = append(performers, createdPerformers...)
}
// ignore if MissingRefBehaviour set to Ignore
}
i.performers = performers
}
return nil
}
func (i *Importer) createPerformers(names []string) ([]*models.Performer, error) {
var ret []*models.Performer
for _, name := range names {
newPerformer := *models.NewPerformer(name)
created, err := i.PerformerWriter.Create(newPerformer)
if err != nil {
return nil, err
}
ret = append(ret, created)
}
return ret, nil
}
func (i *Importer) populateTags() error {
if len(i.Input.Tags) > 0 {
tags, err := importTags(i.TagWriter, i.Input.Tags, i.MissingRefBehaviour)
if err != nil {
return err
}
i.tags = tags
}
return nil
}
func (i *Importer) PostImport(id int) error {
if len(i.galleries) > 0 {
var galleryJoins []models.GalleriesImages
for _, gallery := range i.galleries {
join := models.GalleriesImages{
GalleryID: gallery.ID,
ImageID: id,
}
galleryJoins = append(galleryJoins, join)
}
if err := i.JoinWriter.UpdateGalleriesImages(id, galleryJoins); err != nil {
return fmt.Errorf("failed to associate galleries: %s", err.Error())
}
}
if len(i.performers) > 0 {
var performerJoins []models.PerformersImages
for _, performer := range i.performers {
join := models.PerformersImages{
PerformerID: performer.ID,
ImageID: id,
}
performerJoins = append(performerJoins, join)
}
if err := i.JoinWriter.UpdatePerformersImages(id, performerJoins); err != nil {
return fmt.Errorf("failed to associate performers: %s", err.Error())
}
}
if len(i.tags) > 0 {
var tagJoins []models.ImagesTags
for _, tag := range i.tags {
join := models.ImagesTags{
ImageID: id,
TagID: tag.ID,
}
tagJoins = append(tagJoins, join)
}
if err := i.JoinWriter.UpdateImagesTags(id, tagJoins); err != nil {
return fmt.Errorf("failed to associate tags: %s", err.Error())
}
}
return nil
}
func (i *Importer) Name() string {
return i.Path
}
func (i *Importer) FindExistingID() (*int, error) {
var existing *models.Image
var err error
existing, err = i.ReaderWriter.FindByChecksum(i.Input.Checksum)
if err != nil {
return nil, err
}
if existing != nil {
id := existing.ID
return &id, nil
}
return nil, nil
}
func (i *Importer) Create() (*int, error) {
created, err := i.ReaderWriter.Create(i.image)
if err != nil {
return nil, fmt.Errorf("error creating image: %s", err.Error())
}
id := created.ID
i.ID = id
return &id, nil
}
func (i *Importer) Update(id int) error {
image := i.image
image.ID = id
i.ID = id
_, err := i.ReaderWriter.UpdateFull(image)
if err != nil {
return fmt.Errorf("error updating existing image: %s", err.Error())
}
return nil
}
func importTags(tagWriter models.TagReaderWriter, names []string, missingRefBehaviour models.ImportMissingRefEnum) ([]*models.Tag, error) {
tags, err := tagWriter.FindByNames(names, false)
if err != nil {
return nil, err
}
var pluckedNames []string
for _, tag := range tags {
pluckedNames = append(pluckedNames, tag.Name)
}
missingTags := utils.StrFilter(names, func(name string) bool {
return !utils.StrInclude(pluckedNames, name)
})
if len(missingTags) > 0 {
if missingRefBehaviour == models.ImportMissingRefEnumFail {
return nil, fmt.Errorf("tags [%s] not found", strings.Join(missingTags, ", "))
}
if missingRefBehaviour == models.ImportMissingRefEnumCreate {
createdTags, err := createTags(tagWriter, missingTags)
if err != nil {
return nil, fmt.Errorf("error creating tags: %s", err.Error())
}
tags = append(tags, createdTags...)
}
// ignore if MissingRefBehaviour set to Ignore
}
return tags, nil
}
func createTags(tagWriter models.TagWriter, names []string) ([]*models.Tag, error) {
var ret []*models.Tag
for _, name := range names {
newTag := *models.NewTag(name)
created, err := tagWriter.Create(newTag)
if err != nil {
return nil, err
}
ret = append(ret, created)
}
return ret, nil
}

588
pkg/image/import_test.go Normal file
View File

@@ -0,0 +1,588 @@
package image
import (
"errors"
"testing"
"github.com/stashapp/stash/pkg/manager/jsonschema"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stashapp/stash/pkg/models/modelstest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
const invalidImage = "aW1hZ2VCeXRlcw&&"
const (
path = "path"
imageNameErr = "imageNameErr"
existingImageName = "existingImageName"
existingImageID = 100
existingStudioID = 101
existingGalleryID = 102
existingPerformerID = 103
existingMovieID = 104
existingTagID = 105
existingStudioName = "existingStudioName"
existingStudioErr = "existingStudioErr"
missingStudioName = "missingStudioName"
existingGalleryChecksum = "existingGalleryChecksum"
existingGalleryErr = "existingGalleryErr"
missingGalleryChecksum = "missingGalleryChecksum"
existingPerformerName = "existingPerformerName"
existingPerformerErr = "existingPerformerErr"
missingPerformerName = "missingPerformerName"
existingTagName = "existingTagName"
existingTagErr = "existingTagErr"
missingTagName = "missingTagName"
errPerformersID = 200
errGalleriesID = 201
missingChecksum = "missingChecksum"
errChecksum = "errChecksum"
)
func TestImporterName(t *testing.T) {
i := Importer{
Path: path,
Input: jsonschema.Image{},
}
assert.Equal(t, path, i.Name())
}
func TestImporterPreImport(t *testing.T) {
i := Importer{
Path: path,
}
err := i.PreImport()
assert.Nil(t, err)
}
func TestImporterPreImportWithStudio(t *testing.T) {
studioReaderWriter := &mocks.StudioReaderWriter{}
i := Importer{
StudioWriter: studioReaderWriter,
Path: path,
Input: jsonschema.Image{
Studio: existingStudioName,
},
}
studioReaderWriter.On("FindByName", existingStudioName, false).Return(&models.Studio{
ID: existingStudioID,
}, nil).Once()
studioReaderWriter.On("FindByName", existingStudioErr, false).Return(nil, errors.New("FindByName error")).Once()
err := i.PreImport()
assert.Nil(t, err)
assert.Equal(t, int64(existingStudioID), i.image.StudioID.Int64)
i.Input.Studio = existingStudioErr
err = i.PreImport()
assert.NotNil(t, err)
studioReaderWriter.AssertExpectations(t)
}
func TestImporterPreImportWithMissingStudio(t *testing.T) {
studioReaderWriter := &mocks.StudioReaderWriter{}
i := Importer{
Path: path,
StudioWriter: studioReaderWriter,
Input: jsonschema.Image{
Studio: missingStudioName,
},
MissingRefBehaviour: models.ImportMissingRefEnumFail,
}
studioReaderWriter.On("FindByName", missingStudioName, false).Return(nil, nil).Times(3)
studioReaderWriter.On("Create", mock.AnythingOfType("models.Studio")).Return(&models.Studio{
ID: existingStudioID,
}, nil)
err := i.PreImport()
assert.NotNil(t, err)
i.MissingRefBehaviour = models.ImportMissingRefEnumIgnore
err = i.PreImport()
assert.Nil(t, err)
i.MissingRefBehaviour = models.ImportMissingRefEnumCreate
err = i.PreImport()
assert.Nil(t, err)
assert.Equal(t, int64(existingStudioID), i.image.StudioID.Int64)
studioReaderWriter.AssertExpectations(t)
}
func TestImporterPreImportWithMissingStudioCreateErr(t *testing.T) {
studioReaderWriter := &mocks.StudioReaderWriter{}
i := Importer{
StudioWriter: studioReaderWriter,
Path: path,
Input: jsonschema.Image{
Studio: missingStudioName,
},
MissingRefBehaviour: models.ImportMissingRefEnumCreate,
}
studioReaderWriter.On("FindByName", missingStudioName, false).Return(nil, nil).Once()
studioReaderWriter.On("Create", mock.AnythingOfType("models.Studio")).Return(nil, errors.New("Create error"))
err := i.PreImport()
assert.NotNil(t, err)
}
func TestImporterPreImportWithGallery(t *testing.T) {
galleryReaderWriter := &mocks.GalleryReaderWriter{}
i := Importer{
GalleryWriter: galleryReaderWriter,
Path: path,
Input: jsonschema.Image{
Galleries: []string{
existingGalleryChecksum,
},
},
}
galleryReaderWriter.On("FindByChecksum", existingGalleryChecksum).Return(&models.Gallery{
ID: existingGalleryID,
}, nil).Once()
galleryReaderWriter.On("FindByChecksum", existingGalleryErr).Return(nil, errors.New("FindByChecksum error")).Once()
err := i.PreImport()
assert.Nil(t, err)
assert.Equal(t, existingGalleryID, i.galleries[0].ID)
i.Input.Galleries = []string{
existingGalleryErr,
}
err = i.PreImport()
assert.NotNil(t, err)
galleryReaderWriter.AssertExpectations(t)
}
func TestImporterPreImportWithMissingGallery(t *testing.T) {
galleryReaderWriter := &mocks.GalleryReaderWriter{}
i := Importer{
Path: path,
GalleryWriter: galleryReaderWriter,
Input: jsonschema.Image{
Galleries: []string{
missingGalleryChecksum,
},
},
MissingRefBehaviour: models.ImportMissingRefEnumFail,
}
galleryReaderWriter.On("FindByChecksum", missingGalleryChecksum).Return(nil, nil).Times(3)
err := i.PreImport()
assert.NotNil(t, err)
i.MissingRefBehaviour = models.ImportMissingRefEnumIgnore
err = i.PreImport()
assert.Nil(t, err)
assert.Nil(t, i.galleries)
i.MissingRefBehaviour = models.ImportMissingRefEnumCreate
err = i.PreImport()
assert.Nil(t, err)
assert.Nil(t, i.galleries)
galleryReaderWriter.AssertExpectations(t)
}
func TestImporterPreImportWithPerformer(t *testing.T) {
performerReaderWriter := &mocks.PerformerReaderWriter{}
i := Importer{
PerformerWriter: performerReaderWriter,
Path: path,
MissingRefBehaviour: models.ImportMissingRefEnumFail,
Input: jsonschema.Image{
Performers: []string{
existingPerformerName,
},
},
}
performerReaderWriter.On("FindByNames", []string{existingPerformerName}, false).Return([]*models.Performer{
{
ID: existingPerformerID,
Name: modelstest.NullString(existingPerformerName),
},
}, nil).Once()
performerReaderWriter.On("FindByNames", []string{existingPerformerErr}, false).Return(nil, errors.New("FindByNames error")).Once()
err := i.PreImport()
assert.Nil(t, err)
assert.Equal(t, existingPerformerID, i.performers[0].ID)
i.Input.Performers = []string{existingPerformerErr}
err = i.PreImport()
assert.NotNil(t, err)
performerReaderWriter.AssertExpectations(t)
}
func TestImporterPreImportWithMissingPerformer(t *testing.T) {
performerReaderWriter := &mocks.PerformerReaderWriter{}
i := Importer{
Path: path,
PerformerWriter: performerReaderWriter,
Input: jsonschema.Image{
Performers: []string{
missingPerformerName,
},
},
MissingRefBehaviour: models.ImportMissingRefEnumFail,
}
performerReaderWriter.On("FindByNames", []string{missingPerformerName}, false).Return(nil, nil).Times(3)
performerReaderWriter.On("Create", mock.AnythingOfType("models.Performer")).Return(&models.Performer{
ID: existingPerformerID,
}, nil)
err := i.PreImport()
assert.NotNil(t, err)
i.MissingRefBehaviour = models.ImportMissingRefEnumIgnore
err = i.PreImport()
assert.Nil(t, err)
i.MissingRefBehaviour = models.ImportMissingRefEnumCreate
err = i.PreImport()
assert.Nil(t, err)
assert.Equal(t, existingPerformerID, i.performers[0].ID)
performerReaderWriter.AssertExpectations(t)
}
func TestImporterPreImportWithMissingPerformerCreateErr(t *testing.T) {
performerReaderWriter := &mocks.PerformerReaderWriter{}
i := Importer{
PerformerWriter: performerReaderWriter,
Path: path,
Input: jsonschema.Image{
Performers: []string{
missingPerformerName,
},
},
MissingRefBehaviour: models.ImportMissingRefEnumCreate,
}
performerReaderWriter.On("FindByNames", []string{missingPerformerName}, false).Return(nil, nil).Once()
performerReaderWriter.On("Create", mock.AnythingOfType("models.Performer")).Return(nil, errors.New("Create error"))
err := i.PreImport()
assert.NotNil(t, err)
}
func TestImporterPreImportWithTag(t *testing.T) {
tagReaderWriter := &mocks.TagReaderWriter{}
i := Importer{
TagWriter: tagReaderWriter,
Path: path,
MissingRefBehaviour: models.ImportMissingRefEnumFail,
Input: jsonschema.Image{
Tags: []string{
existingTagName,
},
},
}
tagReaderWriter.On("FindByNames", []string{existingTagName}, false).Return([]*models.Tag{
{
ID: existingTagID,
Name: existingTagName,
},
}, nil).Once()
tagReaderWriter.On("FindByNames", []string{existingTagErr}, false).Return(nil, errors.New("FindByNames error")).Once()
err := i.PreImport()
assert.Nil(t, err)
assert.Equal(t, existingTagID, i.tags[0].ID)
i.Input.Tags = []string{existingTagErr}
err = i.PreImport()
assert.NotNil(t, err)
tagReaderWriter.AssertExpectations(t)
}
func TestImporterPreImportWithMissingTag(t *testing.T) {
tagReaderWriter := &mocks.TagReaderWriter{}
i := Importer{
Path: path,
TagWriter: tagReaderWriter,
Input: jsonschema.Image{
Tags: []string{
missingTagName,
},
},
MissingRefBehaviour: models.ImportMissingRefEnumFail,
}
tagReaderWriter.On("FindByNames", []string{missingTagName}, false).Return(nil, nil).Times(3)
tagReaderWriter.On("Create", mock.AnythingOfType("models.Tag")).Return(&models.Tag{
ID: existingTagID,
}, nil)
err := i.PreImport()
assert.NotNil(t, err)
i.MissingRefBehaviour = models.ImportMissingRefEnumIgnore
err = i.PreImport()
assert.Nil(t, err)
i.MissingRefBehaviour = models.ImportMissingRefEnumCreate
err = i.PreImport()
assert.Nil(t, err)
assert.Equal(t, existingTagID, i.tags[0].ID)
tagReaderWriter.AssertExpectations(t)
}
func TestImporterPreImportWithMissingTagCreateErr(t *testing.T) {
tagReaderWriter := &mocks.TagReaderWriter{}
i := Importer{
TagWriter: tagReaderWriter,
Path: path,
Input: jsonschema.Image{
Tags: []string{
missingTagName,
},
},
MissingRefBehaviour: models.ImportMissingRefEnumCreate,
}
tagReaderWriter.On("FindByNames", []string{missingTagName}, false).Return(nil, nil).Once()
tagReaderWriter.On("Create", mock.AnythingOfType("models.Tag")).Return(nil, errors.New("Create error"))
err := i.PreImport()
assert.NotNil(t, err)
}
func TestImporterPostImportUpdateGallery(t *testing.T) {
joinReaderWriter := &mocks.JoinReaderWriter{}
i := Importer{
JoinWriter: joinReaderWriter,
galleries: []*models.Gallery{
{
ID: existingGalleryID,
},
},
}
updateErr := errors.New("UpdateGalleriesImages error")
joinReaderWriter.On("UpdateGalleriesImages", imageID, []models.GalleriesImages{
{
GalleryID: existingGalleryID,
ImageID: imageID,
},
}).Return(nil).Once()
joinReaderWriter.On("UpdateGalleriesImages", errGalleriesID, mock.AnythingOfType("[]models.GalleriesImages")).Return(updateErr).Once()
err := i.PostImport(imageID)
assert.Nil(t, err)
err = i.PostImport(errGalleriesID)
assert.NotNil(t, err)
joinReaderWriter.AssertExpectations(t)
}
func TestImporterPostImportUpdatePerformers(t *testing.T) {
joinReaderWriter := &mocks.JoinReaderWriter{}
i := Importer{
JoinWriter: joinReaderWriter,
performers: []*models.Performer{
{
ID: existingPerformerID,
},
},
}
updateErr := errors.New("UpdatePerformersImages error")
joinReaderWriter.On("UpdatePerformersImages", imageID, []models.PerformersImages{
{
PerformerID: existingPerformerID,
ImageID: imageID,
},
}).Return(nil).Once()
joinReaderWriter.On("UpdatePerformersImages", errPerformersID, mock.AnythingOfType("[]models.PerformersImages")).Return(updateErr).Once()
err := i.PostImport(imageID)
assert.Nil(t, err)
err = i.PostImport(errPerformersID)
assert.NotNil(t, err)
joinReaderWriter.AssertExpectations(t)
}
func TestImporterPostImportUpdateTags(t *testing.T) {
joinReaderWriter := &mocks.JoinReaderWriter{}
i := Importer{
JoinWriter: joinReaderWriter,
tags: []*models.Tag{
{
ID: existingTagID,
},
},
}
updateErr := errors.New("UpdateImagesTags error")
joinReaderWriter.On("UpdateImagesTags", imageID, []models.ImagesTags{
{
TagID: existingTagID,
ImageID: imageID,
},
}).Return(nil).Once()
joinReaderWriter.On("UpdateImagesTags", errTagsID, mock.AnythingOfType("[]models.ImagesTags")).Return(updateErr).Once()
err := i.PostImport(imageID)
assert.Nil(t, err)
err = i.PostImport(errTagsID)
assert.NotNil(t, err)
joinReaderWriter.AssertExpectations(t)
}
func TestImporterFindExistingID(t *testing.T) {
readerWriter := &mocks.ImageReaderWriter{}
i := Importer{
ReaderWriter: readerWriter,
Path: path,
Input: jsonschema.Image{
Checksum: missingChecksum,
},
}
expectedErr := errors.New("FindBy* error")
readerWriter.On("FindByChecksum", missingChecksum).Return(nil, nil).Once()
readerWriter.On("FindByChecksum", checksum).Return(&models.Image{
ID: existingImageID,
}, nil).Once()
readerWriter.On("FindByChecksum", errChecksum).Return(nil, expectedErr).Once()
id, err := i.FindExistingID()
assert.Nil(t, id)
assert.Nil(t, err)
i.Input.Checksum = checksum
id, err = i.FindExistingID()
assert.Equal(t, existingImageID, *id)
assert.Nil(t, err)
i.Input.Checksum = errChecksum
id, err = i.FindExistingID()
assert.Nil(t, id)
assert.NotNil(t, err)
readerWriter.AssertExpectations(t)
}
func TestCreate(t *testing.T) {
readerWriter := &mocks.ImageReaderWriter{}
image := models.Image{
Title: modelstest.NullString(title),
}
imageErr := models.Image{
Title: modelstest.NullString(imageNameErr),
}
i := Importer{
ReaderWriter: readerWriter,
image: image,
}
errCreate := errors.New("Create error")
readerWriter.On("Create", image).Return(&models.Image{
ID: imageID,
}, nil).Once()
readerWriter.On("Create", imageErr).Return(nil, errCreate).Once()
id, err := i.Create()
assert.Equal(t, imageID, *id)
assert.Nil(t, err)
assert.Equal(t, imageID, i.ID)
i.image = imageErr
id, err = i.Create()
assert.Nil(t, id)
assert.NotNil(t, err)
readerWriter.AssertExpectations(t)
}
func TestUpdate(t *testing.T) {
readerWriter := &mocks.ImageReaderWriter{}
image := models.Image{
Title: modelstest.NullString(title),
}
imageErr := models.Image{
Title: modelstest.NullString(imageNameErr),
}
i := Importer{
ReaderWriter: readerWriter,
image: image,
}
errUpdate := errors.New("Update error")
// id needs to be set for the mock input
image.ID = imageID
readerWriter.On("UpdateFull", image).Return(nil, nil).Once()
err := i.Update(imageID)
assert.Nil(t, err)
assert.Equal(t, imageID, i.ID)
i.image = imageErr
// need to set id separately
imageErr.ID = errImageID
readerWriter.On("UpdateFull", imageErr).Return(nil, errUpdate).Once()
err = i.Update(errImageID)
assert.NotNil(t, err)
readerWriter.AssertExpectations(t)
}

40
pkg/image/thumbnail.go Normal file
View File

@@ -0,0 +1,40 @@
package image
import (
"bytes"
"image"
"image/jpeg"
"github.com/disintegration/imaging"
)
func ThumbnailNeeded(srcImage image.Image, maxSize int) bool {
dim := srcImage.Bounds().Max
w := dim.X
h := dim.Y
return w > maxSize || h > maxSize
}
// GetThumbnail returns the thumbnail image of the provided image resized to
// the provided max size. It resizes based on the largest X/Y direction.
// It returns nil and an error if an error occurs reading, decoding or encoding
// the image.
func GetThumbnail(srcImage image.Image, maxSize int) ([]byte, error) {
var resizedImage image.Image
// if height is longer then resize by height instead of width
dim := srcImage.Bounds().Max
if dim.Y > dim.X {
resizedImage = imaging.Resize(srcImage, 0, maxSize, imaging.Box)
} else {
resizedImage = imaging.Resize(srcImage, maxSize, 0, imaging.Box)
}
buf := new(bytes.Buffer)
err := jpeg.Encode(buf, resizedImage, nil)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}