mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
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:
@@ -1,175 +1,40 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
type Gallery struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
Path string `db:"path" json:"path"`
|
||||
Path sql.NullString `db:"path" json:"path"`
|
||||
Checksum string `db:"checksum" json:"checksum"`
|
||||
Zip bool `db:"zip" json:"zip"`
|
||||
Title sql.NullString `db:"title" json:"title"`
|
||||
URL sql.NullString `db:"url" json:"url"`
|
||||
Date SQLiteDate `db:"date" json:"date"`
|
||||
Details sql.NullString `db:"details" json:"details"`
|
||||
Rating sql.NullInt64 `db:"rating" json:"rating"`
|
||||
StudioID sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
|
||||
SceneID sql.NullInt64 `db:"scene_id,omitempty" json:"scene_id"`
|
||||
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
const DefaultGthumbWidth int = 200
|
||||
|
||||
func (g *Gallery) CountFiles() int {
|
||||
filteredFiles, readCloser, err := g.listZipContents()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
defer readCloser.Close()
|
||||
|
||||
return len(filteredFiles)
|
||||
// GalleryPartial represents part of a Gallery object. It is used to update
|
||||
// the database entry. Only non-nil fields will be updated.
|
||||
type GalleryPartial struct {
|
||||
ID int `db:"id" json:"id"`
|
||||
Path *sql.NullString `db:"path" json:"path"`
|
||||
Checksum *string `db:"checksum" json:"checksum"`
|
||||
Title *sql.NullString `db:"title" json:"title"`
|
||||
URL *sql.NullString `db:"url" json:"url"`
|
||||
Date *SQLiteDate `db:"date" json:"date"`
|
||||
Details *sql.NullString `db:"details" json:"details"`
|
||||
Rating *sql.NullInt64 `db:"rating" json:"rating"`
|
||||
StudioID *sql.NullInt64 `db:"studio_id,omitempty" json:"studio_id"`
|
||||
SceneID *sql.NullInt64 `db:"scene_id,omitempty" json:"scene_id"`
|
||||
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
|
||||
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (g *Gallery) GetFiles(baseURL string) []*GalleryFilesType {
|
||||
var galleryFiles []*GalleryFilesType
|
||||
filteredFiles, readCloser, err := g.listZipContents()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer readCloser.Close()
|
||||
|
||||
builder := urlbuilders.NewGalleryURLBuilder(baseURL, g.ID)
|
||||
for i, file := range filteredFiles {
|
||||
galleryURL := builder.GetGalleryImageURL(i)
|
||||
galleryFile := GalleryFilesType{
|
||||
Index: i,
|
||||
Name: &file.Name,
|
||||
Path: &galleryURL,
|
||||
}
|
||||
galleryFiles = append(galleryFiles, &galleryFile)
|
||||
}
|
||||
|
||||
return galleryFiles
|
||||
}
|
||||
|
||||
func (g *Gallery) GetImage(index int) []byte {
|
||||
data, _ := g.readZipFile(index)
|
||||
return data
|
||||
}
|
||||
|
||||
func (g *Gallery) GetThumbnail(index int, width int) []byte {
|
||||
data, _ := g.readZipFile(index)
|
||||
srcImage, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
resizedImage := imaging.Resize(srcImage, width, 0, imaging.Box)
|
||||
buf := new(bytes.Buffer)
|
||||
err = jpeg.Encode(buf, resizedImage, nil)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (g *Gallery) readZipFile(index int) ([]byte, error) {
|
||||
filteredFiles, readCloser, err := g.listZipContents()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer readCloser.Close()
|
||||
|
||||
zipFile := filteredFiles[index]
|
||||
zipFileReadCloser, err := zipFile.Open()
|
||||
if err != nil {
|
||||
logger.Warn("failed to read file inside zip file")
|
||||
return nil, err
|
||||
}
|
||||
defer zipFileReadCloser.Close()
|
||||
|
||||
return ioutil.ReadAll(zipFileReadCloser)
|
||||
}
|
||||
|
||||
func (g *Gallery) listZipContents() ([]*zip.File, *zip.ReadCloser, error) {
|
||||
readCloser, err := zip.OpenReader(g.Path)
|
||||
if err != nil {
|
||||
logger.Warnf("failed to read zip file %s", g.Path)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
filteredFiles := make([]*zip.File, 0)
|
||||
for _, file := range readCloser.File {
|
||||
if file.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
ext := filepath.Ext(file.Name)
|
||||
ext = strings.ToLower(ext)
|
||||
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" && ext != ".webp" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(file.Name, "__MACOSX") {
|
||||
continue
|
||||
}
|
||||
filteredFiles = append(filteredFiles, file)
|
||||
}
|
||||
sort.Slice(filteredFiles, func(i, j int) bool {
|
||||
a := filteredFiles[i]
|
||||
b := filteredFiles[j]
|
||||
return utils.NaturalCompare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
cover := contains(filteredFiles, "cover.jpg") // first image with cover.jpg in the name
|
||||
if cover >= 0 { // will be moved to the start
|
||||
reorderedFiles := reorder(filteredFiles, cover)
|
||||
if reorderedFiles != nil {
|
||||
return reorderedFiles, readCloser, nil
|
||||
}
|
||||
}
|
||||
|
||||
return filteredFiles, readCloser, nil
|
||||
}
|
||||
|
||||
// return index of first occurrenece of string x ( case insensitive ) in name of zip contents, -1 otherwise
|
||||
func contains(a []*zip.File, x string) int {
|
||||
for i, n := range a {
|
||||
if strings.Contains(strings.ToLower(n.Name), strings.ToLower(x)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// reorder slice so that element with position toFirst gets at the start
|
||||
func reorder(a []*zip.File, toFirst int) []*zip.File {
|
||||
var first *zip.File
|
||||
switch {
|
||||
case toFirst < 0 || toFirst >= len(a):
|
||||
return nil
|
||||
case toFirst == 0:
|
||||
return a
|
||||
default:
|
||||
first = a[toFirst]
|
||||
copy(a[toFirst:], a[toFirst+1:]) // Shift a[toFirst+1:] left one index removing a[toFirst] element
|
||||
a[len(a)-1] = nil // Nil now unused element for garbage collection
|
||||
a = a[:len(a)-1] // Truncate slice
|
||||
a = append([]*zip.File{first}, a...) // Push first to the start of the slice
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (g *Gallery) ImageCount() int {
|
||||
images, _, _ := g.listZipContents()
|
||||
if images == nil {
|
||||
return 0
|
||||
}
|
||||
return len(images)
|
||||
}
|
||||
const DefaultGthumbWidth int = 640
|
||||
|
||||
Reference in New Issue
Block a user