mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +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,72 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/manager/paths"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type thumbBuffer struct {
|
||||
path string
|
||||
dir string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func newCacheThumb(dir string, path string, data []byte) *thumbBuffer {
|
||||
t := thumbBuffer{dir: dir, path: path, data: data}
|
||||
return &t
|
||||
}
|
||||
|
||||
var writeChan chan *thumbBuffer
|
||||
var touchChan chan *string
|
||||
|
||||
func startThumbCache() { // TODO add extra wait, close chan code if/when stash gets a stop mode
|
||||
|
||||
writeChan = make(chan *thumbBuffer, 20)
|
||||
go thumbnailCacheWriter()
|
||||
}
|
||||
|
||||
//serialize file writes to avoid race conditions
|
||||
func thumbnailCacheWriter() {
|
||||
|
||||
for thumb := range writeChan {
|
||||
exists, _ := utils.FileExists(thumb.path)
|
||||
if !exists {
|
||||
err := utils.WriteFile(thumb.path, thumb.data)
|
||||
if err != nil {
|
||||
logger.Errorf("Write error for thumbnail %s: %s ", thumb.path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// get thumbnail from cache, otherwise create it and store to cache
|
||||
func cacheGthumb(gallery *models.Gallery, index int, width int) []byte {
|
||||
thumbPath := paths.GetGthumbPath(gallery.Checksum, index, width)
|
||||
exists, _ := utils.FileExists(thumbPath)
|
||||
if exists { // if thumbnail exists in cache return that
|
||||
content, err := ioutil.ReadFile(thumbPath)
|
||||
if err == nil {
|
||||
return content
|
||||
} else {
|
||||
logger.Errorf("Read Error for file %s : %s", thumbPath, err)
|
||||
}
|
||||
|
||||
}
|
||||
data := gallery.GetThumbnail(index, width)
|
||||
thumbDir := paths.GetGthumbDir(gallery.Checksum)
|
||||
t := newCacheThumb(thumbDir, thumbPath, data)
|
||||
writeChan <- t // write the file to cache
|
||||
return data
|
||||
}
|
||||
|
||||
// create all thumbs for a given gallery
|
||||
func CreateGthumbs(gallery *models.Gallery) {
|
||||
count := gallery.ImageCount()
|
||||
for i := 0; i < count; i++ {
|
||||
cacheGthumb(gallery, i, models.DefaultGthumbWidth)
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,5 @@ const (
|
||||
ContextUser key = 5
|
||||
tagKey key = 6
|
||||
downloadKey key = 7
|
||||
imageKey key = 8
|
||||
)
|
||||
|
||||
@@ -27,6 +27,9 @@ func (r *Resolver) Query() models.QueryResolver {
|
||||
func (r *Resolver) Scene() models.SceneResolver {
|
||||
return &sceneResolver{r}
|
||||
}
|
||||
func (r *Resolver) Image() models.ImageResolver {
|
||||
return &imageResolver{r}
|
||||
}
|
||||
func (r *Resolver) SceneMarker() models.SceneMarkerResolver {
|
||||
return &sceneMarkerResolver{r}
|
||||
}
|
||||
@@ -67,6 +70,7 @@ type galleryResolver struct{ *Resolver }
|
||||
type performerResolver struct{ *Resolver }
|
||||
type sceneResolver struct{ *Resolver }
|
||||
type sceneMarkerResolver struct{ *Resolver }
|
||||
type imageResolver struct{ *Resolver }
|
||||
type studioResolver struct{ *Resolver }
|
||||
type movieResolver struct{ *Resolver }
|
||||
type tagResolver struct{ *Resolver }
|
||||
|
||||
@@ -3,16 +3,82 @@ package api
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *galleryResolver) Title(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
return nil, nil // TODO remove this from schema
|
||||
func (r *galleryResolver) Path(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
if obj.Path.Valid {
|
||||
return &obj.Path.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Files(ctx context.Context, obj *models.Gallery) ([]*models.GalleryFilesType, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
return obj.GetFiles(baseURL), nil
|
||||
func (r *galleryResolver) Title(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
if obj.Title.Valid {
|
||||
return &obj.Title.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Images(ctx context.Context, obj *models.Gallery) ([]*models.Image, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
|
||||
return qb.FindByGalleryID(obj.ID)
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Cover(ctx context.Context, obj *models.Gallery) (*models.Image, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
|
||||
imgs, err := qb.FindByGalleryID(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret *models.Image
|
||||
if len(imgs) > 0 {
|
||||
ret = imgs[0]
|
||||
}
|
||||
|
||||
for _, img := range imgs {
|
||||
if image.IsCover(img) {
|
||||
ret = img
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
if obj.Date.Valid {
|
||||
result := utils.GetYMDFromDatabaseDate(obj.Date.String)
|
||||
return &result, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) URL(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
if obj.URL.Valid {
|
||||
return &obj.URL.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Details(ctx context.Context, obj *models.Gallery) (*string, error) {
|
||||
if obj.Details.Valid {
|
||||
return &obj.Details.String, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Rating(ctx context.Context, obj *models.Gallery) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Scene(ctx context.Context, obj *models.Gallery) (*models.Scene, error) {
|
||||
@@ -23,3 +89,22 @@ func (r *galleryResolver) Scene(ctx context.Context, obj *models.Gallery) (*mode
|
||||
qb := models.NewSceneQueryBuilder()
|
||||
return qb.Find(int(obj.SceneID.Int64))
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (*models.Studio, error) {
|
||||
if !obj.StudioID.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.Find(int(obj.StudioID.Int64), nil)
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) ([]*models.Tag, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
return qb.FindByGalleryID(obj.ID, nil)
|
||||
}
|
||||
|
||||
func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) ([]*models.Performer, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
return qb.FindByGalleryID(obj.ID, nil)
|
||||
}
|
||||
|
||||
68
pkg/api/resolver_model_image.go
Normal file
68
pkg/api/resolver_model_image.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *imageResolver) Title(ctx context.Context, obj *models.Image) (*string, error) {
|
||||
ret := image.GetTitle(obj)
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Rating(ctx context.Context, obj *models.Image) (*int, error) {
|
||||
if obj.Rating.Valid {
|
||||
rating := int(obj.Rating.Int64)
|
||||
return &rating, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*models.ImageFileType, error) {
|
||||
width := int(obj.Width.Int64)
|
||||
height := int(obj.Height.Int64)
|
||||
size := int(obj.Size.Int64)
|
||||
return &models.ImageFileType{
|
||||
Size: &size,
|
||||
Width: &width,
|
||||
Height: &height,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*models.ImagePathsType, error) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
builder := urlbuilders.NewImageURLBuilder(baseURL, obj.ID)
|
||||
thumbnailPath := builder.GetThumbnailURL()
|
||||
imagePath := builder.GetImageURL()
|
||||
return &models.ImagePathsType{
|
||||
Image: &imagePath,
|
||||
Thumbnail: &thumbnailPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) ([]*models.Gallery, error) {
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
return qb.FindByImageID(obj.ID, nil)
|
||||
}
|
||||
|
||||
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (*models.Studio, error) {
|
||||
if !obj.StudioID.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
qb := models.NewStudioQueryBuilder()
|
||||
return qb.Find(int(obj.StudioID.Int64), nil)
|
||||
}
|
||||
|
||||
func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) ([]*models.Tag, error) {
|
||||
qb := models.NewTagQueryBuilder()
|
||||
return qb.FindByImageID(obj.ID, nil)
|
||||
}
|
||||
|
||||
func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) ([]*models.Performer, error) {
|
||||
qb := models.NewPerformerQueryBuilder()
|
||||
return qb.FindByImageID(obj.ID, nil)
|
||||
}
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
|
||||
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.ConfigGeneralInput) (*models.ConfigGeneralResult, error) {
|
||||
if len(input.Stashes) > 0 {
|
||||
for _, stashPath := range input.Stashes {
|
||||
exists, err := utils.DirExists(stashPath)
|
||||
for _, s := range input.Stashes {
|
||||
exists, err := utils.DirExists(s.Path)
|
||||
if !exists {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
@@ -119,6 +119,24 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
|
||||
config.Set(config.Exclude, input.Excludes)
|
||||
}
|
||||
|
||||
if input.ImageExcludes != nil {
|
||||
config.Set(config.ImageExclude, input.ImageExcludes)
|
||||
}
|
||||
|
||||
if input.VideoExtensions != nil {
|
||||
config.Set(config.VideoExtensions, input.VideoExtensions)
|
||||
}
|
||||
|
||||
if input.ImageExtensions != nil {
|
||||
config.Set(config.ImageExtensions, input.ImageExtensions)
|
||||
}
|
||||
|
||||
if input.GalleryExtensions != nil {
|
||||
config.Set(config.GalleryExtensions, input.GalleryExtensions)
|
||||
}
|
||||
|
||||
config.Set(config.CreateGalleriesFromFolders, input.CreateGalleriesFromFolders)
|
||||
|
||||
refreshScraperCache := false
|
||||
if input.ScraperUserAgent != nil {
|
||||
config.Set(config.ScraperUserAgent, input.ScraperUserAgent)
|
||||
|
||||
544
pkg/api/resolver_mutation_gallery.go
Normal file
544
pkg/api/resolver_mutation_gallery.go
Normal file
@@ -0,0 +1,544 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) GalleryCreate(ctx context.Context, input models.GalleryCreateInput) (*models.Gallery, error) {
|
||||
// name must be provided
|
||||
if input.Title == "" {
|
||||
return nil, errors.New("title must not be empty")
|
||||
}
|
||||
|
||||
// for manually created galleries, generate checksum from title
|
||||
checksum := utils.MD5FromString(input.Title)
|
||||
|
||||
// Populate a new performer from the input
|
||||
currentTime := time.Now()
|
||||
newGallery := models.Gallery{
|
||||
Title: sql.NullString{
|
||||
String: input.Title,
|
||||
Valid: true,
|
||||
},
|
||||
Checksum: checksum,
|
||||
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||
}
|
||||
if input.URL != nil {
|
||||
newGallery.URL = sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.Details != nil {
|
||||
newGallery.Details = sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
if input.URL != nil {
|
||||
newGallery.URL = sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.Date != nil {
|
||||
newGallery.Date = models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
if input.Rating != nil {
|
||||
newGallery.Rating = sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
// rating must be nullable
|
||||
newGallery.Rating = sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
newGallery.StudioID = sql.NullInt64{Int64: studioID, Valid: true}
|
||||
} else {
|
||||
// studio must be nullable
|
||||
newGallery.StudioID = sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
if input.SceneID != nil {
|
||||
sceneID, _ := strconv.ParseInt(*input.SceneID, 10, 64)
|
||||
newGallery.SceneID = sql.NullInt64{Int64: sceneID, Valid: true}
|
||||
} else {
|
||||
// studio must be nullable
|
||||
newGallery.SceneID = sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
// Start the transaction and save the performer
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
gallery, err := qb.Create(newGallery, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
var performerJoins []models.PerformersGalleries
|
||||
for _, pid := range input.PerformerIds {
|
||||
performerID, _ := strconv.Atoi(pid)
|
||||
performerJoin := models.PerformersGalleries{
|
||||
PerformerID: performerID,
|
||||
GalleryID: gallery.ID,
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersGalleries(gallery.ID, performerJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
var tagJoins []models.GalleriesTags
|
||||
for _, tid := range input.TagIds {
|
||||
tagID, _ := strconv.Atoi(tid)
|
||||
tagJoin := models.GalleriesTags{
|
||||
GalleryID: gallery.ID,
|
||||
TagID: tagID,
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateGalleriesTags(gallery.ID, tagJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gallery, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleryUpdate(ctx context.Context, input models.GalleryUpdateInput) (*models.Gallery, error) {
|
||||
// Start the transaction and save the gallery
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
ret, err := r.galleryUpdate(input, tx)
|
||||
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleriesUpdate(ctx context.Context, input []*models.GalleryUpdateInput) ([]*models.Gallery, error) {
|
||||
// Start the transaction and save the gallery
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
var ret []*models.Gallery
|
||||
|
||||
for _, gallery := range input {
|
||||
thisGallery, err := r.galleryUpdate(*gallery, tx)
|
||||
ret = append(ret, thisGallery)
|
||||
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) galleryUpdate(input models.GalleryUpdateInput, tx *sqlx.Tx) (*models.Gallery, error) {
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
// Populate gallery from the input
|
||||
galleryID, _ := strconv.Atoi(input.ID)
|
||||
originalGallery, err := qb.Find(galleryID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if originalGallery == nil {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
|
||||
updatedTime := time.Now()
|
||||
updatedGallery := models.GalleryPartial{
|
||||
ID: galleryID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
if input.Title != nil {
|
||||
// ensure title is not empty
|
||||
if *input.Title == "" {
|
||||
return nil, errors.New("title must not be empty")
|
||||
}
|
||||
|
||||
// if gallery is not zip-based, then generate the checksum from the title
|
||||
if !originalGallery.Path.Valid {
|
||||
checksum := utils.MD5FromString(*input.Title)
|
||||
updatedGallery.Checksum = &checksum
|
||||
}
|
||||
|
||||
updatedGallery.Title = &sql.NullString{String: *input.Title, Valid: true}
|
||||
}
|
||||
if input.Details != nil {
|
||||
updatedGallery.Details = &sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
if input.URL != nil {
|
||||
updatedGallery.URL = &sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.Date != nil {
|
||||
updatedGallery.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
updatedGallery.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
// rating must be nullable
|
||||
updatedGallery.Rating = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedGallery.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
} else {
|
||||
// studio must be nullable
|
||||
updatedGallery.StudioID = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
// gallery scene is set from the scene only
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
gallery, err := qb.UpdatePartial(updatedGallery, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
var performerJoins []models.PerformersGalleries
|
||||
for _, pid := range input.PerformerIds {
|
||||
performerID, _ := strconv.Atoi(pid)
|
||||
performerJoin := models.PerformersGalleries{
|
||||
PerformerID: performerID,
|
||||
GalleryID: galleryID,
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersGalleries(galleryID, performerJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
var tagJoins []models.GalleriesTags
|
||||
for _, tid := range input.TagIds {
|
||||
tagID, _ := strconv.Atoi(tid)
|
||||
tagJoin := models.GalleriesTags{
|
||||
GalleryID: galleryID,
|
||||
TagID: tagID,
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateGalleriesTags(galleryID, tagJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gallery, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input models.BulkGalleryUpdateInput) ([]*models.Gallery, error) {
|
||||
// Populate gallery from the input
|
||||
updatedTime := time.Now()
|
||||
|
||||
// Start the transaction and save the gallery marker
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
|
||||
updatedGallery := models.GalleryPartial{
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
if input.Details != nil {
|
||||
updatedGallery.Details = &sql.NullString{String: *input.Details, Valid: true}
|
||||
}
|
||||
if input.URL != nil {
|
||||
updatedGallery.URL = &sql.NullString{String: *input.URL, Valid: true}
|
||||
}
|
||||
if input.Date != nil {
|
||||
updatedGallery.Date = &models.SQLiteDate{String: *input.Date, Valid: true}
|
||||
}
|
||||
if input.Rating != nil {
|
||||
// a rating of 0 means unset the rating
|
||||
if *input.Rating == 0 {
|
||||
updatedGallery.Rating = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
updatedGallery.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
}
|
||||
}
|
||||
if input.StudioID != nil {
|
||||
// empty string means unset the studio
|
||||
if *input.StudioID == "" {
|
||||
updatedGallery.StudioID = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedGallery.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
}
|
||||
}
|
||||
if input.SceneID != nil {
|
||||
// empty string means unset the studio
|
||||
if *input.SceneID == "" {
|
||||
updatedGallery.SceneID = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
sceneID, _ := strconv.ParseInt(*input.SceneID, 10, 64)
|
||||
updatedGallery.SceneID = &sql.NullInt64{Int64: sceneID, Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
ret := []*models.Gallery{}
|
||||
|
||||
for _, galleryIDStr := range input.Ids {
|
||||
galleryID, _ := strconv.Atoi(galleryIDStr)
|
||||
updatedGallery.ID = galleryID
|
||||
|
||||
gallery, err := qb.UpdatePartial(updatedGallery, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = append(ret, gallery)
|
||||
|
||||
// Save the performers
|
||||
if wasFieldIncluded(ctx, "performer_ids") {
|
||||
performerIDs, err := adjustGalleryPerformerIDs(tx, galleryID, *input.PerformerIds)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var performerJoins []models.PerformersGalleries
|
||||
for _, performerID := range performerIDs {
|
||||
performerJoin := models.PerformersGalleries{
|
||||
PerformerID: performerID,
|
||||
GalleryID: galleryID,
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersGalleries(galleryID, performerJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if wasFieldIncluded(ctx, "tag_ids") {
|
||||
tagIDs, err := adjustGalleryTagIDs(tx, galleryID, *input.TagIds)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tagJoins []models.GalleriesTags
|
||||
for _, tagID := range tagIDs {
|
||||
tagJoin := models.GalleriesTags{
|
||||
GalleryID: galleryID,
|
||||
TagID: tagID,
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateGalleriesTags(galleryID, tagJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func adjustGalleryPerformerIDs(tx *sqlx.Tx, galleryID int, ids models.BulkUpdateIds) ([]int, error) {
|
||||
var ret []int
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
|
||||
// adding to the joins
|
||||
performerJoins, err := jqb.GetGalleryPerformers(galleryID, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range performerJoins {
|
||||
ret = append(ret, join.PerformerID)
|
||||
}
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustGalleryTagIDs(tx *sqlx.Tx, galleryID int, ids models.BulkUpdateIds) ([]int, error) {
|
||||
var ret []int
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
|
||||
// adding to the joins
|
||||
tagJoins, err := jqb.GetGalleryTags(galleryID, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range tagJoins {
|
||||
ret = append(ret, join.TagID)
|
||||
}
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GalleryDestroy(ctx context.Context, input models.GalleryDestroyInput) (bool, error) {
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
var galleries []*models.Gallery
|
||||
var imgsToPostProcess []*models.Image
|
||||
for _, id := range input.Ids {
|
||||
galleryID, _ := strconv.Atoi(id)
|
||||
|
||||
gallery, err := qb.Find(galleryID, tx)
|
||||
if gallery != nil {
|
||||
galleries = append(galleries, gallery)
|
||||
}
|
||||
err = qb.Destroy(galleryID, tx)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
// if this is a zip-based gallery, delete the images as well
|
||||
if gallery.Path.Valid {
|
||||
iqb := models.NewImageQueryBuilder()
|
||||
imgs, err := iqb.FindByGalleryID(galleryID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, img := range imgs {
|
||||
err = qb.Destroy(img.ID, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
imgsToPostProcess = append(imgsToPostProcess, img)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// if delete file is true, then delete the file as well
|
||||
// if it fails, just log a message
|
||||
if input.DeleteFile != nil && *input.DeleteFile {
|
||||
for _, gallery := range galleries {
|
||||
manager.DeleteGalleryFile(gallery)
|
||||
}
|
||||
}
|
||||
|
||||
// if delete generated is true, then delete the generated files
|
||||
// for the gallery
|
||||
if input.DeleteGenerated != nil && *input.DeleteGenerated {
|
||||
for _, img := range imgsToPostProcess {
|
||||
manager.DeleteGeneratedImageFiles(img)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AddGalleryImages(ctx context.Context, input models.GalleryAddInput) (bool, error) {
|
||||
galleryID, _ := strconv.Atoi(input.GalleryID)
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
gallery, err := qb.Find(galleryID, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if gallery == nil {
|
||||
return false, errors.New("gallery not found")
|
||||
}
|
||||
|
||||
if gallery.Zip {
|
||||
return false, errors.New("cannot modify zip gallery images")
|
||||
}
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
for _, id := range input.ImageIds {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
_, err := jqb.AddImageGallery(imageID, galleryID, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RemoveGalleryImages(ctx context.Context, input models.GalleryRemoveInput) (bool, error) {
|
||||
galleryID, _ := strconv.Atoi(input.GalleryID)
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
gallery, err := qb.Find(galleryID, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if gallery == nil {
|
||||
return false, errors.New("gallery not found")
|
||||
}
|
||||
|
||||
if gallery.Zip {
|
||||
return false, errors.New("cannot modify zip gallery images")
|
||||
}
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
for _, id := range input.ImageIds {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
_, err := jqb.RemoveImageGallery(imageID, galleryID, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
439
pkg/api/resolver_mutation_image.go
Normal file
439
pkg/api/resolver_mutation_image.go
Normal file
@@ -0,0 +1,439 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/stashapp/stash/pkg/database"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) ImageUpdate(ctx context.Context, input models.ImageUpdateInput) (*models.Image, error) {
|
||||
// Start the transaction and save the image
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
ret, err := r.imageUpdate(input, tx)
|
||||
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImagesUpdate(ctx context.Context, input []*models.ImageUpdateInput) ([]*models.Image, error) {
|
||||
// Start the transaction and save the image
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
var ret []*models.Image
|
||||
|
||||
for _, image := range input {
|
||||
thisImage, err := r.imageUpdate(*image, tx)
|
||||
ret = append(ret, thisImage)
|
||||
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) imageUpdate(input models.ImageUpdateInput, tx *sqlx.Tx) (*models.Image, error) {
|
||||
// Populate image from the input
|
||||
imageID, _ := strconv.Atoi(input.ID)
|
||||
|
||||
updatedTime := time.Now()
|
||||
updatedImage := models.ImagePartial{
|
||||
ID: imageID,
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
if input.Title != nil {
|
||||
updatedImage.Title = &sql.NullString{String: *input.Title, Valid: true}
|
||||
}
|
||||
|
||||
if input.Rating != nil {
|
||||
updatedImage.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
} else {
|
||||
// rating must be nullable
|
||||
updatedImage.Rating = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
if input.StudioID != nil {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedImage.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
} else {
|
||||
// studio must be nullable
|
||||
updatedImage.StudioID = &sql.NullInt64{Valid: false}
|
||||
}
|
||||
|
||||
qb := models.NewImageQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
image, err := qb.Update(updatedImage, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// don't set the galleries directly. Use add/remove gallery images interface instead
|
||||
|
||||
// Save the performers
|
||||
var performerJoins []models.PerformersImages
|
||||
for _, pid := range input.PerformerIds {
|
||||
performerID, _ := strconv.Atoi(pid)
|
||||
performerJoin := models.PerformersImages{
|
||||
PerformerID: performerID,
|
||||
ImageID: imageID,
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersImages(imageID, performerJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
var tagJoins []models.ImagesTags
|
||||
for _, tid := range input.TagIds {
|
||||
tagID, _ := strconv.Atoi(tid)
|
||||
tagJoin := models.ImagesTags{
|
||||
ImageID: imageID,
|
||||
TagID: tagID,
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateImagesTags(imageID, tagJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input models.BulkImageUpdateInput) ([]*models.Image, error) {
|
||||
// Populate image from the input
|
||||
updatedTime := time.Now()
|
||||
|
||||
// Start the transaction and save the image marker
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewImageQueryBuilder()
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
|
||||
updatedImage := models.ImagePartial{
|
||||
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||
}
|
||||
if input.Title != nil {
|
||||
updatedImage.Title = &sql.NullString{String: *input.Title, Valid: true}
|
||||
}
|
||||
if input.Rating != nil {
|
||||
// a rating of 0 means unset the rating
|
||||
if *input.Rating == 0 {
|
||||
updatedImage.Rating = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
updatedImage.Rating = &sql.NullInt64{Int64: int64(*input.Rating), Valid: true}
|
||||
}
|
||||
}
|
||||
if input.StudioID != nil {
|
||||
// empty string means unset the studio
|
||||
if *input.StudioID == "" {
|
||||
updatedImage.StudioID = &sql.NullInt64{Int64: 0, Valid: false}
|
||||
} else {
|
||||
studioID, _ := strconv.ParseInt(*input.StudioID, 10, 64)
|
||||
updatedImage.StudioID = &sql.NullInt64{Int64: studioID, Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
ret := []*models.Image{}
|
||||
|
||||
for _, imageIDStr := range input.Ids {
|
||||
imageID, _ := strconv.Atoi(imageIDStr)
|
||||
updatedImage.ID = imageID
|
||||
|
||||
image, err := qb.Update(updatedImage, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = append(ret, image)
|
||||
|
||||
// Save the galleries
|
||||
if wasFieldIncluded(ctx, "gallery_ids") {
|
||||
galleryIDs, err := adjustImageGalleryIDs(tx, imageID, *input.GalleryIds)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var galleryJoins []models.GalleriesImages
|
||||
for _, gid := range galleryIDs {
|
||||
galleryJoin := models.GalleriesImages{
|
||||
GalleryID: gid,
|
||||
ImageID: imageID,
|
||||
}
|
||||
galleryJoins = append(galleryJoins, galleryJoin)
|
||||
}
|
||||
if err := jqb.UpdateGalleriesImages(imageID, galleryJoins, tx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the performers
|
||||
if wasFieldIncluded(ctx, "performer_ids") {
|
||||
performerIDs, err := adjustImagePerformerIDs(tx, imageID, *input.PerformerIds)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var performerJoins []models.PerformersImages
|
||||
for _, performerID := range performerIDs {
|
||||
performerJoin := models.PerformersImages{
|
||||
PerformerID: performerID,
|
||||
ImageID: imageID,
|
||||
}
|
||||
performerJoins = append(performerJoins, performerJoin)
|
||||
}
|
||||
if err := jqb.UpdatePerformersImages(imageID, performerJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if wasFieldIncluded(ctx, "tag_ids") {
|
||||
tagIDs, err := adjustImageTagIDs(tx, imageID, *input.TagIds)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tagJoins []models.ImagesTags
|
||||
for _, tagID := range tagIDs {
|
||||
tagJoin := models.ImagesTags{
|
||||
ImageID: imageID,
|
||||
TagID: tagID,
|
||||
}
|
||||
tagJoins = append(tagJoins, tagJoin)
|
||||
}
|
||||
if err := jqb.UpdateImagesTags(imageID, tagJoins, tx); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func adjustImageGalleryIDs(tx *sqlx.Tx, imageID int, ids models.BulkUpdateIds) ([]int, error) {
|
||||
var ret []int
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
|
||||
// adding to the joins
|
||||
galleryJoins, err := jqb.GetImageGalleries(imageID, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range galleryJoins {
|
||||
ret = append(ret, join.GalleryID)
|
||||
}
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustImagePerformerIDs(tx *sqlx.Tx, imageID int, ids models.BulkUpdateIds) ([]int, error) {
|
||||
var ret []int
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
|
||||
// adding to the joins
|
||||
performerJoins, err := jqb.GetImagePerformers(imageID, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range performerJoins {
|
||||
ret = append(ret, join.PerformerID)
|
||||
}
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func adjustImageTagIDs(tx *sqlx.Tx, imageID int, ids models.BulkUpdateIds) ([]int, error) {
|
||||
var ret []int
|
||||
|
||||
jqb := models.NewJoinsQueryBuilder()
|
||||
if ids.Mode == models.BulkUpdateIDModeAdd || ids.Mode == models.BulkUpdateIDModeRemove {
|
||||
// adding to the joins
|
||||
tagJoins, err := jqb.GetImageTags(imageID, tx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, join := range tagJoins {
|
||||
ret = append(ret, join.TagID)
|
||||
}
|
||||
}
|
||||
|
||||
return adjustIDs(ret, ids), nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImageDestroy(ctx context.Context, input models.ImageDestroyInput) (bool, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
imageID, _ := strconv.Atoi(input.ID)
|
||||
image, err := qb.Find(imageID)
|
||||
err = qb.Destroy(imageID, tx)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// if delete generated is true, then delete the generated files
|
||||
// for the image
|
||||
if input.DeleteGenerated != nil && *input.DeleteGenerated {
|
||||
manager.DeleteGeneratedImageFiles(image)
|
||||
}
|
||||
|
||||
// if delete file is true, then delete the file as well
|
||||
// if it fails, just log a message
|
||||
if input.DeleteFile != nil && *input.DeleteFile {
|
||||
manager.DeleteImageFile(image)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImagesDestroy(ctx context.Context, input models.ImagesDestroyInput) (bool, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
|
||||
var images []*models.Image
|
||||
for _, id := range input.Ids {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
|
||||
image, err := qb.Find(imageID)
|
||||
if image != nil {
|
||||
images = append(images, image)
|
||||
}
|
||||
err = qb.Destroy(imageID, tx)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
// if delete generated is true, then delete the generated files
|
||||
// for the image
|
||||
if input.DeleteGenerated != nil && *input.DeleteGenerated {
|
||||
manager.DeleteGeneratedImageFiles(image)
|
||||
}
|
||||
|
||||
// if delete file is true, then delete the file as well
|
||||
// if it fails, just log a message
|
||||
if input.DeleteFile != nil && *input.DeleteFile {
|
||||
manager.DeleteImageFile(image)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImageIncrementO(ctx context.Context, id string) (int, error) {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewImageQueryBuilder()
|
||||
|
||||
newVal, err := qb.IncrementOCounter(imageID, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newVal, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImageDecrementO(ctx context.Context, id string) (int, error) {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewImageQueryBuilder()
|
||||
|
||||
newVal, err := qb.DecrementOCounter(imageID, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newVal, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ImageResetO(ctx context.Context, id string) (int, error) {
|
||||
imageID, _ := strconv.Atoi(id)
|
||||
|
||||
tx := database.DB.MustBeginTx(ctx, nil)
|
||||
qb := models.NewImageQueryBuilder()
|
||||
|
||||
newVal, err := qb.ResetOCounter(imageID, tx)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Commit
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newVal, nil
|
||||
}
|
||||
@@ -43,30 +43,35 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
|
||||
scraperCDPPath := config.GetScraperCDPPath()
|
||||
|
||||
return &models.ConfigGeneralResult{
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
CalculateMd5: config.IsCalculateMD5(),
|
||||
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
PreviewSegments: config.GetPreviewSegments(),
|
||||
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
|
||||
PreviewExcludeStart: config.GetPreviewExcludeStart(),
|
||||
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
|
||||
PreviewPreset: config.GetPreviewPreset(),
|
||||
MaxTranscodeSize: &maxTranscodeSize,
|
||||
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
||||
Username: config.GetUsername(),
|
||||
Password: config.GetPasswordHash(),
|
||||
MaxSessionAge: config.GetMaxSessionAge(),
|
||||
LogFile: &logFile,
|
||||
LogOut: config.GetLogOut(),
|
||||
LogLevel: config.GetLogLevel(),
|
||||
LogAccess: config.GetLogAccess(),
|
||||
Excludes: config.GetExcludes(),
|
||||
ScraperUserAgent: &scraperUserAgent,
|
||||
ScraperCDPPath: &scraperCDPPath,
|
||||
StashBoxes: config.GetStashBoxes(),
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
CalculateMd5: config.IsCalculateMD5(),
|
||||
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
PreviewSegments: config.GetPreviewSegments(),
|
||||
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
|
||||
PreviewExcludeStart: config.GetPreviewExcludeStart(),
|
||||
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
|
||||
PreviewPreset: config.GetPreviewPreset(),
|
||||
MaxTranscodeSize: &maxTranscodeSize,
|
||||
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
||||
Username: config.GetUsername(),
|
||||
Password: config.GetPasswordHash(),
|
||||
MaxSessionAge: config.GetMaxSessionAge(),
|
||||
LogFile: &logFile,
|
||||
LogOut: config.GetLogOut(),
|
||||
LogLevel: config.GetLogLevel(),
|
||||
LogAccess: config.GetLogAccess(),
|
||||
VideoExtensions: config.GetVideoExtensions(),
|
||||
ImageExtensions: config.GetImageExtensions(),
|
||||
GalleryExtensions: config.GetGalleryExtensions(),
|
||||
CreateGalleriesFromFolders: config.GetCreateGalleriesFromFolders(),
|
||||
Excludes: config.GetExcludes(),
|
||||
ImageExcludes: config.GetImageExcludes(),
|
||||
ScraperUserAgent: &scraperUserAgent,
|
||||
ScraperCDPPath: &scraperCDPPath,
|
||||
StashBoxes: config.GetStashBoxes(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func (r *queryResolver) FindGallery(ctx context.Context, id string) (*models.Gallery, error) {
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
idInt, _ := strconv.Atoi(id)
|
||||
return qb.Find(idInt)
|
||||
return qb.Find(idInt, nil)
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (*models.FindGalleriesResultType, error) {
|
||||
|
||||
30
pkg/api/resolver_query_find_image.go
Normal file
30
pkg/api/resolver_query_find_image.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
var image *models.Image
|
||||
var err error
|
||||
if id != nil {
|
||||
idInt, _ := strconv.Atoi(*id)
|
||||
image, err = qb.Find(idInt)
|
||||
} else if checksum != nil {
|
||||
image, err = qb.FindByChecksum(*checksum)
|
||||
}
|
||||
return image, err
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (*models.FindImagesResultType, error) {
|
||||
qb := models.NewImageQueryBuilder()
|
||||
images, total := qb.Query(imageFilter, filter)
|
||||
return &models.FindImagesResultType{
|
||||
Count: total,
|
||||
Images: images,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type galleryRoutes struct{}
|
||||
|
||||
func (rs galleryRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{galleryId}", func(r chi.Router) {
|
||||
r.Use(GalleryCtx)
|
||||
r.Get("/{fileIndex}", rs.File)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (rs galleryRoutes) File(w http.ResponseWriter, r *http.Request) {
|
||||
gallery := r.Context().Value(galleryKey).(*models.Gallery)
|
||||
if gallery == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
fileIndex, _ := strconv.Atoi(chi.URLParam(r, "fileIndex"))
|
||||
thumb := r.URL.Query().Get("thumb")
|
||||
w.Header().Add("Cache-Control", "max-age=604800000") // 1 Week
|
||||
if thumb == "true" {
|
||||
_, _ = w.Write(cacheGthumb(gallery, fileIndex, models.DefaultGthumbWidth))
|
||||
} else if thumb == "" {
|
||||
_, _ = w.Write(gallery.GetImage(fileIndex))
|
||||
} else {
|
||||
width, err := strconv.ParseInt(thumb, 0, 64)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(400), 400)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(cacheGthumb(gallery, fileIndex, int(width)))
|
||||
}
|
||||
}
|
||||
|
||||
func GalleryCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
galleryID, err := strconv.Atoi(chi.URLParam(r, "galleryId"))
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
qb := models.NewGalleryQueryBuilder()
|
||||
gallery, err := qb.Find(galleryID)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), galleryKey, gallery)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
75
pkg/api/routes_image.go
Normal file
75
pkg/api/routes_image.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type imageRoutes struct{}
|
||||
|
||||
func (rs imageRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{imageId}", func(r chi.Router) {
|
||||
r.Use(ImageCtx)
|
||||
|
||||
r.Get("/image", rs.Image)
|
||||
r.Get("/thumbnail", rs.Thumbnail)
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// region Handlers
|
||||
|
||||
func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
image := r.Context().Value(imageKey).(*models.Image)
|
||||
filepath := manager.GetInstance().Paths.Generated.GetThumbnailPath(image.Checksum, models.DefaultGthumbWidth)
|
||||
|
||||
// if the thumbnail doesn't exist, fall back to the original file
|
||||
exists, _ := utils.FileExists(filepath)
|
||||
if exists {
|
||||
http.ServeFile(w, r, filepath)
|
||||
} else {
|
||||
rs.Image(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (rs imageRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
||||
i := r.Context().Value(imageKey).(*models.Image)
|
||||
|
||||
// if image is in a zip file, we need to serve it specifically
|
||||
image.Serve(w, r, i.Path)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
func ImageCtx(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
imageIdentifierQueryParam := chi.URLParam(r, "imageId")
|
||||
imageID, _ := strconv.Atoi(imageIdentifierQueryParam)
|
||||
|
||||
var image *models.Image
|
||||
qb := models.NewImageQueryBuilder()
|
||||
if imageID == 0 {
|
||||
image, _ = qb.FindByChecksum(imageIdentifierQueryParam)
|
||||
} else {
|
||||
image, _ = qb.Find(imageID)
|
||||
}
|
||||
|
||||
if image == nil {
|
||||
http.Error(w, http.StatusText(404), 404)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), imageKey, image)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
@@ -145,9 +145,9 @@ func Start() {
|
||||
|
||||
r.Get(loginEndPoint, getLoginHandler)
|
||||
|
||||
r.Mount("/gallery", galleryRoutes{}.Routes())
|
||||
r.Mount("/performer", performerRoutes{}.Routes())
|
||||
r.Mount("/scene", sceneRoutes{}.Routes())
|
||||
r.Mount("/image", imageRoutes{}.Routes())
|
||||
r.Mount("/studio", studioRoutes{}.Routes())
|
||||
r.Mount("/movie", movieRoutes{}.Routes())
|
||||
r.Mount("/tag", tagRoutes{}.Routes())
|
||||
@@ -248,8 +248,6 @@ func Start() {
|
||||
http.Redirect(w, r, "/", 301)
|
||||
})
|
||||
|
||||
startThumbCache()
|
||||
|
||||
// Serve static folders
|
||||
customServedFolders := config.GetCustomServedFolders()
|
||||
if customServedFolders != nil {
|
||||
|
||||
25
pkg/api/urlbuilders/image.go
Normal file
25
pkg/api/urlbuilders/image.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package urlbuilders
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ImageURLBuilder struct {
|
||||
BaseURL string
|
||||
ImageID string
|
||||
}
|
||||
|
||||
func NewImageURLBuilder(baseURL string, imageID int) ImageURLBuilder {
|
||||
return ImageURLBuilder{
|
||||
BaseURL: baseURL,
|
||||
ImageID: strconv.Itoa(imageID),
|
||||
}
|
||||
}
|
||||
|
||||
func (b ImageURLBuilder) GetImageURL() string {
|
||||
return b.BaseURL + "/image/" + b.ImageID + "/image"
|
||||
}
|
||||
|
||||
func (b ImageURLBuilder) GetThumbnailURL() string {
|
||||
return b.BaseURL + "/image/" + b.ImageID + "/thumbnail"
|
||||
}
|
||||
Reference in New Issue
Block a user