mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Model refactor (#3915)
* Add mockery config file * Move basic file/folder structs to models * Fix hack due to import loop * Move file interfaces to models * Move folder interfaces to models * Move scene interfaces to models * Move scene marker interfaces to models * Move image interfaces to models * Move gallery interfaces to models * Move gallery chapter interfaces to models * Move studio interfaces to models * Move movie interfaces to models * Move performer interfaces to models * Move tag interfaces to models * Move autotag interfaces to models * Regenerate mocks
This commit is contained in:
parent
20520a58b4
commit
c364346a59
185 changed files with 3840 additions and 2559 deletions
4
.mockery.yml
Normal file
4
.mockery.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
dir: ./pkg/models
|
||||||
|
name: ".*ReaderWriter"
|
||||||
|
outpkg: mocks
|
||||||
|
output: ./pkg/models/mocks
|
||||||
2
Makefile
2
Makefile
|
|
@ -319,7 +319,7 @@ it:
|
||||||
# generates test mocks
|
# generates test mocks
|
||||||
.PHONY: generate-test-mocks
|
.PHONY: generate-test-mocks
|
||||||
generate-test-mocks:
|
generate-test-mocks:
|
||||||
go run github.com/vektra/mockery/v2 --dir ./pkg/models --name '.*ReaderWriter' --outpkg mocks --output ./pkg/models/mocks
|
go run github.com/vektra/mockery/v2
|
||||||
|
|
||||||
# runs server
|
# runs server
|
||||||
# sets the config file to use the local dev config
|
# sets the config file to use the local dev config
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import (
|
||||||
|
|
||||||
flag "github.com/spf13/pflag"
|
flag "github.com/spf13/pflag"
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/hash/videophash"
|
"github.com/stashapp/stash/pkg/hash/videophash"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func customUsage() {
|
func customUsage() {
|
||||||
|
|
@ -28,8 +28,8 @@ func printPhash(ff *ffmpeg.FFMpeg, ffp ffmpeg.FFProbe, inputfile string, quiet *
|
||||||
// videoFile.Path (from BaseFile)
|
// videoFile.Path (from BaseFile)
|
||||||
// videoFile.Duration
|
// videoFile.Duration
|
||||||
// The rest of the struct isn't needed.
|
// The rest of the struct isn't needed.
|
||||||
vf := &file.VideoFile{
|
vf := &models.VideoFile{
|
||||||
BaseFile: &file.BaseFile{Path: inputfile},
|
BaseFile: &models.BaseFile{Path: inputfile},
|
||||||
Duration: ffvideoFile.FileDuration,
|
Duration: ffvideoFile.FileDuration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
30
gqlgen.yml
30
gqlgen.yml
|
|
@ -23,6 +23,12 @@ autobind:
|
||||||
|
|
||||||
models:
|
models:
|
||||||
# Scalars
|
# Scalars
|
||||||
|
ID:
|
||||||
|
model:
|
||||||
|
- github.com/99designs/gqlgen/graphql.ID
|
||||||
|
- github.com/99designs/gqlgen/graphql.IntID
|
||||||
|
- github.com/stashapp/stash/pkg/models.FileID
|
||||||
|
- github.com/stashapp/stash/pkg/models.FolderID
|
||||||
Int64:
|
Int64:
|
||||||
model: github.com/99designs/gqlgen/graphql.Int64
|
model: github.com/99designs/gqlgen/graphql.Int64
|
||||||
Timestamp:
|
Timestamp:
|
||||||
|
|
@ -33,6 +39,30 @@ models:
|
||||||
fields:
|
fields:
|
||||||
title:
|
title:
|
||||||
resolver: true
|
resolver: true
|
||||||
|
# override models, from internal/api/models.go
|
||||||
|
BaseFile:
|
||||||
|
model: github.com/stashapp/stash/internal/api.BaseFile
|
||||||
|
GalleryFile:
|
||||||
|
model: github.com/stashapp/stash/internal/api.GalleryFile
|
||||||
|
fields:
|
||||||
|
# override fingerprint field
|
||||||
|
fingerprints:
|
||||||
|
fieldName: FingerprintSlice
|
||||||
|
VideoFile:
|
||||||
|
fields:
|
||||||
|
# override fingerprint field
|
||||||
|
fingerprints:
|
||||||
|
fieldName: FingerprintSlice
|
||||||
|
# override float fields - #1572
|
||||||
|
duration:
|
||||||
|
fieldName: DurationFinite
|
||||||
|
frame_rate:
|
||||||
|
fieldName: FrameRateFinite
|
||||||
|
ImageFile:
|
||||||
|
fields:
|
||||||
|
# override fingerprint field
|
||||||
|
fingerprints:
|
||||||
|
fieldName: FingerprintSlice
|
||||||
# autobind on config causes generation issues
|
# autobind on config causes generation issues
|
||||||
BlobsStorageType:
|
BlobsStorageType:
|
||||||
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType
|
model: github.com/stashapp/stash/internal/manager/config.BlobsStorageType
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden SceneLoader int *github.com/stashapp/stash/pkg/models.Scene
|
//go:generate go run github.com/vektah/dataloaden SceneLoader int *github.com/stashapp/stash/pkg/models.Scene
|
||||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden GalleryLoader int *github.com/stashapp/stash/pkg/models.Gallery
|
//go:generate go run github.com/vektah/dataloaden GalleryLoader int *github.com/stashapp/stash/pkg/models.Gallery
|
||||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden ImageLoader int *github.com/stashapp/stash/pkg/models.Image
|
//go:generate go run github.com/vektah/dataloaden ImageLoader int *github.com/stashapp/stash/pkg/models.Image
|
||||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden PerformerLoader int *github.com/stashapp/stash/pkg/models.Performer
|
//go:generate go run github.com/vektah/dataloaden PerformerLoader int *github.com/stashapp/stash/pkg/models.Performer
|
||||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden StudioLoader int *github.com/stashapp/stash/pkg/models.Studio
|
//go:generate go run github.com/vektah/dataloaden StudioLoader int *github.com/stashapp/stash/pkg/models.Studio
|
||||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden TagLoader int *github.com/stashapp/stash/pkg/models.Tag
|
//go:generate go run github.com/vektah/dataloaden TagLoader int *github.com/stashapp/stash/pkg/models.Tag
|
||||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden MovieLoader int *github.com/stashapp/stash/pkg/models.Movie
|
//go:generate go run github.com/vektah/dataloaden MovieLoader int *github.com/stashapp/stash/pkg/models.Movie
|
||||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden FileLoader github.com/stashapp/stash/pkg/file.ID github.com/stashapp/stash/pkg/file.File
|
//go:generate go run github.com/vektah/dataloaden FileLoader github.com/stashapp/stash/pkg/models.FileID github.com/stashapp/stash/pkg/models.File
|
||||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
|
//go:generate go run github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
|
//go:generate go run github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||||
//go:generate go run -mod=vendor github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/file.ID
|
//go:generate go run github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||||
|
|
||||||
package loaders
|
package loaders
|
||||||
|
|
||||||
|
|
@ -18,7 +18,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/manager"
|
"github.com/stashapp/stash/internal/manager"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
)
|
)
|
||||||
|
|
@ -216,8 +215,8 @@ func (m Middleware) fetchMovies(ctx context.Context) func(keys []int) ([]*models
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Middleware) fetchFiles(ctx context.Context) func(keys []file.ID) ([]file.File, []error) {
|
func (m Middleware) fetchFiles(ctx context.Context) func(keys []models.FileID) ([]models.File, []error) {
|
||||||
return func(keys []file.ID) (ret []file.File, errs []error) {
|
return func(keys []models.FileID) (ret []models.File, errs []error) {
|
||||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
ret, err = m.Repository.File.Find(ctx, keys...)
|
ret, err = m.Repository.File.Find(ctx, keys...)
|
||||||
|
|
@ -227,8 +226,8 @@ func (m Middleware) fetchFiles(ctx context.Context) func(keys []file.ID) ([]file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
|
func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
|
||||||
return func(keys []int) (ret [][]file.ID, errs []error) {
|
return func(keys []int) (ret [][]models.FileID, errs []error) {
|
||||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
ret, err = m.Repository.Scene.GetManyFileIDs(ctx, keys)
|
ret, err = m.Repository.Scene.GetManyFileIDs(ctx, keys)
|
||||||
|
|
@ -238,8 +237,8 @@ func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
|
func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
|
||||||
return func(keys []int) (ret [][]file.ID, errs []error) {
|
return func(keys []int) (ret [][]models.FileID, errs []error) {
|
||||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
ret, err = m.Repository.Image.GetManyFileIDs(ctx, keys)
|
ret, err = m.Repository.Image.GetManyFileIDs(ctx, keys)
|
||||||
|
|
@ -249,8 +248,8 @@ func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Middleware) fetchGalleriesFileIDs(ctx context.Context) func(keys []int) ([][]file.ID, []error) {
|
func (m Middleware) fetchGalleriesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
|
||||||
return func(keys []int) (ret [][]file.ID, errs []error) {
|
return func(keys []int) (ret [][]models.FileID, errs []error) {
|
||||||
err := m.withTxn(ctx, func(ctx context.Context) error {
|
err := m.withTxn(ctx, func(ctx context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
ret, err = m.Repository.Gallery.GetManyFileIDs(ctx, keys)
|
ret, err = m.Repository.Gallery.GetManyFileIDs(ctx, keys)
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileLoaderConfig captures the config to create a new FileLoader
|
// FileLoaderConfig captures the config to create a new FileLoader
|
||||||
type FileLoaderConfig struct {
|
type FileLoaderConfig struct {
|
||||||
// Fetch is a method that provides the data for the loader
|
// Fetch is a method that provides the data for the loader
|
||||||
Fetch func(keys []file.ID) ([]file.File, []error)
|
Fetch func(keys []models.FileID) ([]models.File, []error)
|
||||||
|
|
||||||
// Wait is how long wait before sending a batch
|
// Wait is how long wait before sending a batch
|
||||||
Wait time.Duration
|
Wait time.Duration
|
||||||
|
|
@ -33,7 +33,7 @@ func NewFileLoader(config FileLoaderConfig) *FileLoader {
|
||||||
// FileLoader batches and caches requests
|
// FileLoader batches and caches requests
|
||||||
type FileLoader struct {
|
type FileLoader struct {
|
||||||
// this method provides the data for the loader
|
// this method provides the data for the loader
|
||||||
fetch func(keys []file.ID) ([]file.File, []error)
|
fetch func(keys []models.FileID) ([]models.File, []error)
|
||||||
|
|
||||||
// how long to done before sending a batch
|
// how long to done before sending a batch
|
||||||
wait time.Duration
|
wait time.Duration
|
||||||
|
|
@ -44,7 +44,7 @@ type FileLoader struct {
|
||||||
// INTERNAL
|
// INTERNAL
|
||||||
|
|
||||||
// lazily created cache
|
// lazily created cache
|
||||||
cache map[file.ID]file.File
|
cache map[models.FileID]models.File
|
||||||
|
|
||||||
// the current batch. keys will continue to be collected until timeout is hit,
|
// the current batch. keys will continue to be collected until timeout is hit,
|
||||||
// then everything will be sent to the fetch method and out to the listeners
|
// then everything will be sent to the fetch method and out to the listeners
|
||||||
|
|
@ -55,26 +55,26 @@ type FileLoader struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type fileLoaderBatch struct {
|
type fileLoaderBatch struct {
|
||||||
keys []file.ID
|
keys []models.FileID
|
||||||
data []file.File
|
data []models.File
|
||||||
error []error
|
error []error
|
||||||
closing bool
|
closing bool
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load a File by key, batching and caching will be applied automatically
|
// Load a File by key, batching and caching will be applied automatically
|
||||||
func (l *FileLoader) Load(key file.ID) (file.File, error) {
|
func (l *FileLoader) Load(key models.FileID) (models.File, error) {
|
||||||
return l.LoadThunk(key)()
|
return l.LoadThunk(key)()
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadThunk returns a function that when called will block waiting for a File.
|
// LoadThunk returns a function that when called will block waiting for a File.
|
||||||
// This method should be used if you want one goroutine to make requests to many
|
// This method should be used if you want one goroutine to make requests to many
|
||||||
// different data loaders without blocking until the thunk is called.
|
// different data loaders without blocking until the thunk is called.
|
||||||
func (l *FileLoader) LoadThunk(key file.ID) func() (file.File, error) {
|
func (l *FileLoader) LoadThunk(key models.FileID) func() (models.File, error) {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
if it, ok := l.cache[key]; ok {
|
if it, ok := l.cache[key]; ok {
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
return func() (file.File, error) {
|
return func() (models.File, error) {
|
||||||
return it, nil
|
return it, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,10 +85,10 @@ func (l *FileLoader) LoadThunk(key file.ID) func() (file.File, error) {
|
||||||
pos := batch.keyIndex(l, key)
|
pos := batch.keyIndex(l, key)
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
|
|
||||||
return func() (file.File, error) {
|
return func() (models.File, error) {
|
||||||
<-batch.done
|
<-batch.done
|
||||||
|
|
||||||
var data file.File
|
var data models.File
|
||||||
if pos < len(batch.data) {
|
if pos < len(batch.data) {
|
||||||
data = batch.data[pos]
|
data = batch.data[pos]
|
||||||
}
|
}
|
||||||
|
|
@ -113,14 +113,14 @@ func (l *FileLoader) LoadThunk(key file.ID) func() (file.File, error) {
|
||||||
|
|
||||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||||
// sub batches depending on how the loader is configured
|
// sub batches depending on how the loader is configured
|
||||||
func (l *FileLoader) LoadAll(keys []file.ID) ([]file.File, []error) {
|
func (l *FileLoader) LoadAll(keys []models.FileID) ([]models.File, []error) {
|
||||||
results := make([]func() (file.File, error), len(keys))
|
results := make([]func() (models.File, error), len(keys))
|
||||||
|
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
results[i] = l.LoadThunk(key)
|
results[i] = l.LoadThunk(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
files := make([]file.File, len(keys))
|
files := make([]models.File, len(keys))
|
||||||
errors := make([]error, len(keys))
|
errors := make([]error, len(keys))
|
||||||
for i, thunk := range results {
|
for i, thunk := range results {
|
||||||
files[i], errors[i] = thunk()
|
files[i], errors[i] = thunk()
|
||||||
|
|
@ -131,13 +131,13 @@ func (l *FileLoader) LoadAll(keys []file.ID) ([]file.File, []error) {
|
||||||
// LoadAllThunk returns a function that when called will block waiting for a Files.
|
// LoadAllThunk returns a function that when called will block waiting for a Files.
|
||||||
// This method should be used if you want one goroutine to make requests to many
|
// This method should be used if you want one goroutine to make requests to many
|
||||||
// different data loaders without blocking until the thunk is called.
|
// different data loaders without blocking until the thunk is called.
|
||||||
func (l *FileLoader) LoadAllThunk(keys []file.ID) func() ([]file.File, []error) {
|
func (l *FileLoader) LoadAllThunk(keys []models.FileID) func() ([]models.File, []error) {
|
||||||
results := make([]func() (file.File, error), len(keys))
|
results := make([]func() (models.File, error), len(keys))
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
results[i] = l.LoadThunk(key)
|
results[i] = l.LoadThunk(key)
|
||||||
}
|
}
|
||||||
return func() ([]file.File, []error) {
|
return func() ([]models.File, []error) {
|
||||||
files := make([]file.File, len(keys))
|
files := make([]models.File, len(keys))
|
||||||
errors := make([]error, len(keys))
|
errors := make([]error, len(keys))
|
||||||
for i, thunk := range results {
|
for i, thunk := range results {
|
||||||
files[i], errors[i] = thunk()
|
files[i], errors[i] = thunk()
|
||||||
|
|
@ -149,7 +149,7 @@ func (l *FileLoader) LoadAllThunk(keys []file.ID) func() ([]file.File, []error)
|
||||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||||
// and false is returned.
|
// and false is returned.
|
||||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||||
func (l *FileLoader) Prime(key file.ID, value file.File) bool {
|
func (l *FileLoader) Prime(key models.FileID, value models.File) bool {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
var found bool
|
var found bool
|
||||||
if _, found = l.cache[key]; !found {
|
if _, found = l.cache[key]; !found {
|
||||||
|
|
@ -160,22 +160,22 @@ func (l *FileLoader) Prime(key file.ID, value file.File) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the value at key from the cache, if it exists
|
// Clear the value at key from the cache, if it exists
|
||||||
func (l *FileLoader) Clear(key file.ID) {
|
func (l *FileLoader) Clear(key models.FileID) {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
delete(l.cache, key)
|
delete(l.cache, key)
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *FileLoader) unsafeSet(key file.ID, value file.File) {
|
func (l *FileLoader) unsafeSet(key models.FileID, value models.File) {
|
||||||
if l.cache == nil {
|
if l.cache == nil {
|
||||||
l.cache = map[file.ID]file.File{}
|
l.cache = map[models.FileID]models.File{}
|
||||||
}
|
}
|
||||||
l.cache[key] = value
|
l.cache[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// keyIndex will return the location of the key in the batch, if its not found
|
// keyIndex will return the location of the key in the batch, if its not found
|
||||||
// it will add the key to the batch
|
// it will add the key to the batch
|
||||||
func (b *fileLoaderBatch) keyIndex(l *FileLoader, key file.ID) int {
|
func (b *fileLoaderBatch) keyIndex(l *FileLoader, key models.FileID) int {
|
||||||
for i, existingKey := range b.keys {
|
for i, existingKey := range b.keys {
|
||||||
if key == existingKey {
|
if key == existingKey {
|
||||||
return i
|
return i
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GalleryFileIDsLoaderConfig captures the config to create a new GalleryFileIDsLoader
|
// GalleryFileIDsLoaderConfig captures the config to create a new GalleryFileIDsLoader
|
||||||
type GalleryFileIDsLoaderConfig struct {
|
type GalleryFileIDsLoaderConfig struct {
|
||||||
// Fetch is a method that provides the data for the loader
|
// Fetch is a method that provides the data for the loader
|
||||||
Fetch func(keys []int) ([][]file.ID, []error)
|
Fetch func(keys []int) ([][]models.FileID, []error)
|
||||||
|
|
||||||
// Wait is how long wait before sending a batch
|
// Wait is how long wait before sending a batch
|
||||||
Wait time.Duration
|
Wait time.Duration
|
||||||
|
|
@ -33,7 +33,7 @@ func NewGalleryFileIDsLoader(config GalleryFileIDsLoaderConfig) *GalleryFileIDsL
|
||||||
// GalleryFileIDsLoader batches and caches requests
|
// GalleryFileIDsLoader batches and caches requests
|
||||||
type GalleryFileIDsLoader struct {
|
type GalleryFileIDsLoader struct {
|
||||||
// this method provides the data for the loader
|
// this method provides the data for the loader
|
||||||
fetch func(keys []int) ([][]file.ID, []error)
|
fetch func(keys []int) ([][]models.FileID, []error)
|
||||||
|
|
||||||
// how long to done before sending a batch
|
// how long to done before sending a batch
|
||||||
wait time.Duration
|
wait time.Duration
|
||||||
|
|
@ -44,7 +44,7 @@ type GalleryFileIDsLoader struct {
|
||||||
// INTERNAL
|
// INTERNAL
|
||||||
|
|
||||||
// lazily created cache
|
// lazily created cache
|
||||||
cache map[int][]file.ID
|
cache map[int][]models.FileID
|
||||||
|
|
||||||
// the current batch. keys will continue to be collected until timeout is hit,
|
// the current batch. keys will continue to be collected until timeout is hit,
|
||||||
// then everything will be sent to the fetch method and out to the listeners
|
// then everything will be sent to the fetch method and out to the listeners
|
||||||
|
|
@ -56,25 +56,25 @@ type GalleryFileIDsLoader struct {
|
||||||
|
|
||||||
type galleryFileIDsLoaderBatch struct {
|
type galleryFileIDsLoaderBatch struct {
|
||||||
keys []int
|
keys []int
|
||||||
data [][]file.ID
|
data [][]models.FileID
|
||||||
error []error
|
error []error
|
||||||
closing bool
|
closing bool
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load a ID by key, batching and caching will be applied automatically
|
// Load a FileID by key, batching and caching will be applied automatically
|
||||||
func (l *GalleryFileIDsLoader) Load(key int) ([]file.ID, error) {
|
func (l *GalleryFileIDsLoader) Load(key int) ([]models.FileID, error) {
|
||||||
return l.LoadThunk(key)()
|
return l.LoadThunk(key)()
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadThunk returns a function that when called will block waiting for a ID.
|
// LoadThunk returns a function that when called will block waiting for a FileID.
|
||||||
// This method should be used if you want one goroutine to make requests to many
|
// This method should be used if you want one goroutine to make requests to many
|
||||||
// different data loaders without blocking until the thunk is called.
|
// different data loaders without blocking until the thunk is called.
|
||||||
func (l *GalleryFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
func (l *GalleryFileIDsLoader) LoadThunk(key int) func() ([]models.FileID, error) {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
if it, ok := l.cache[key]; ok {
|
if it, ok := l.cache[key]; ok {
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
return func() ([]file.ID, error) {
|
return func() ([]models.FileID, error) {
|
||||||
return it, nil
|
return it, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,10 +85,10 @@ func (l *GalleryFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||||
pos := batch.keyIndex(l, key)
|
pos := batch.keyIndex(l, key)
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
|
|
||||||
return func() ([]file.ID, error) {
|
return func() ([]models.FileID, error) {
|
||||||
<-batch.done
|
<-batch.done
|
||||||
|
|
||||||
var data []file.ID
|
var data []models.FileID
|
||||||
if pos < len(batch.data) {
|
if pos < len(batch.data) {
|
||||||
data = batch.data[pos]
|
data = batch.data[pos]
|
||||||
}
|
}
|
||||||
|
|
@ -113,49 +113,49 @@ func (l *GalleryFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||||
|
|
||||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||||
// sub batches depending on how the loader is configured
|
// sub batches depending on how the loader is configured
|
||||||
func (l *GalleryFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
|
func (l *GalleryFileIDsLoader) LoadAll(keys []int) ([][]models.FileID, []error) {
|
||||||
results := make([]func() ([]file.ID, error), len(keys))
|
results := make([]func() ([]models.FileID, error), len(keys))
|
||||||
|
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
results[i] = l.LoadThunk(key)
|
results[i] = l.LoadThunk(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
iDs := make([][]file.ID, len(keys))
|
fileIDs := make([][]models.FileID, len(keys))
|
||||||
errors := make([]error, len(keys))
|
errors := make([]error, len(keys))
|
||||||
for i, thunk := range results {
|
for i, thunk := range results {
|
||||||
iDs[i], errors[i] = thunk()
|
fileIDs[i], errors[i] = thunk()
|
||||||
}
|
}
|
||||||
return iDs, errors
|
return fileIDs, errors
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAllThunk returns a function that when called will block waiting for a IDs.
|
// LoadAllThunk returns a function that when called will block waiting for a FileIDs.
|
||||||
// This method should be used if you want one goroutine to make requests to many
|
// This method should be used if you want one goroutine to make requests to many
|
||||||
// different data loaders without blocking until the thunk is called.
|
// different data loaders without blocking until the thunk is called.
|
||||||
func (l *GalleryFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
|
func (l *GalleryFileIDsLoader) LoadAllThunk(keys []int) func() ([][]models.FileID, []error) {
|
||||||
results := make([]func() ([]file.ID, error), len(keys))
|
results := make([]func() ([]models.FileID, error), len(keys))
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
results[i] = l.LoadThunk(key)
|
results[i] = l.LoadThunk(key)
|
||||||
}
|
}
|
||||||
return func() ([][]file.ID, []error) {
|
return func() ([][]models.FileID, []error) {
|
||||||
iDs := make([][]file.ID, len(keys))
|
fileIDs := make([][]models.FileID, len(keys))
|
||||||
errors := make([]error, len(keys))
|
errors := make([]error, len(keys))
|
||||||
for i, thunk := range results {
|
for i, thunk := range results {
|
||||||
iDs[i], errors[i] = thunk()
|
fileIDs[i], errors[i] = thunk()
|
||||||
}
|
}
|
||||||
return iDs, errors
|
return fileIDs, errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||||
// and false is returned.
|
// and false is returned.
|
||||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||||
func (l *GalleryFileIDsLoader) Prime(key int, value []file.ID) bool {
|
func (l *GalleryFileIDsLoader) Prime(key int, value []models.FileID) bool {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
var found bool
|
var found bool
|
||||||
if _, found = l.cache[key]; !found {
|
if _, found = l.cache[key]; !found {
|
||||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||||
// and end up with the whole cache pointing to the same value.
|
// and end up with the whole cache pointing to the same value.
|
||||||
cpy := make([]file.ID, len(value))
|
cpy := make([]models.FileID, len(value))
|
||||||
copy(cpy, value)
|
copy(cpy, value)
|
||||||
l.unsafeSet(key, cpy)
|
l.unsafeSet(key, cpy)
|
||||||
}
|
}
|
||||||
|
|
@ -170,9 +170,9 @@ func (l *GalleryFileIDsLoader) Clear(key int) {
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *GalleryFileIDsLoader) unsafeSet(key int, value []file.ID) {
|
func (l *GalleryFileIDsLoader) unsafeSet(key int, value []models.FileID) {
|
||||||
if l.cache == nil {
|
if l.cache == nil {
|
||||||
l.cache = map[int][]file.ID{}
|
l.cache = map[int][]models.FileID{}
|
||||||
}
|
}
|
||||||
l.cache[key] = value
|
l.cache[key] = value
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ImageFileIDsLoaderConfig captures the config to create a new ImageFileIDsLoader
|
// ImageFileIDsLoaderConfig captures the config to create a new ImageFileIDsLoader
|
||||||
type ImageFileIDsLoaderConfig struct {
|
type ImageFileIDsLoaderConfig struct {
|
||||||
// Fetch is a method that provides the data for the loader
|
// Fetch is a method that provides the data for the loader
|
||||||
Fetch func(keys []int) ([][]file.ID, []error)
|
Fetch func(keys []int) ([][]models.FileID, []error)
|
||||||
|
|
||||||
// Wait is how long wait before sending a batch
|
// Wait is how long wait before sending a batch
|
||||||
Wait time.Duration
|
Wait time.Duration
|
||||||
|
|
@ -33,7 +33,7 @@ func NewImageFileIDsLoader(config ImageFileIDsLoaderConfig) *ImageFileIDsLoader
|
||||||
// ImageFileIDsLoader batches and caches requests
|
// ImageFileIDsLoader batches and caches requests
|
||||||
type ImageFileIDsLoader struct {
|
type ImageFileIDsLoader struct {
|
||||||
// this method provides the data for the loader
|
// this method provides the data for the loader
|
||||||
fetch func(keys []int) ([][]file.ID, []error)
|
fetch func(keys []int) ([][]models.FileID, []error)
|
||||||
|
|
||||||
// how long to done before sending a batch
|
// how long to done before sending a batch
|
||||||
wait time.Duration
|
wait time.Duration
|
||||||
|
|
@ -44,7 +44,7 @@ type ImageFileIDsLoader struct {
|
||||||
// INTERNAL
|
// INTERNAL
|
||||||
|
|
||||||
// lazily created cache
|
// lazily created cache
|
||||||
cache map[int][]file.ID
|
cache map[int][]models.FileID
|
||||||
|
|
||||||
// the current batch. keys will continue to be collected until timeout is hit,
|
// the current batch. keys will continue to be collected until timeout is hit,
|
||||||
// then everything will be sent to the fetch method and out to the listeners
|
// then everything will be sent to the fetch method and out to the listeners
|
||||||
|
|
@ -56,25 +56,25 @@ type ImageFileIDsLoader struct {
|
||||||
|
|
||||||
type imageFileIDsLoaderBatch struct {
|
type imageFileIDsLoaderBatch struct {
|
||||||
keys []int
|
keys []int
|
||||||
data [][]file.ID
|
data [][]models.FileID
|
||||||
error []error
|
error []error
|
||||||
closing bool
|
closing bool
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load a ID by key, batching and caching will be applied automatically
|
// Load a FileID by key, batching and caching will be applied automatically
|
||||||
func (l *ImageFileIDsLoader) Load(key int) ([]file.ID, error) {
|
func (l *ImageFileIDsLoader) Load(key int) ([]models.FileID, error) {
|
||||||
return l.LoadThunk(key)()
|
return l.LoadThunk(key)()
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadThunk returns a function that when called will block waiting for a ID.
|
// LoadThunk returns a function that when called will block waiting for a FileID.
|
||||||
// This method should be used if you want one goroutine to make requests to many
|
// This method should be used if you want one goroutine to make requests to many
|
||||||
// different data loaders without blocking until the thunk is called.
|
// different data loaders without blocking until the thunk is called.
|
||||||
func (l *ImageFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
func (l *ImageFileIDsLoader) LoadThunk(key int) func() ([]models.FileID, error) {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
if it, ok := l.cache[key]; ok {
|
if it, ok := l.cache[key]; ok {
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
return func() ([]file.ID, error) {
|
return func() ([]models.FileID, error) {
|
||||||
return it, nil
|
return it, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,10 +85,10 @@ func (l *ImageFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||||
pos := batch.keyIndex(l, key)
|
pos := batch.keyIndex(l, key)
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
|
|
||||||
return func() ([]file.ID, error) {
|
return func() ([]models.FileID, error) {
|
||||||
<-batch.done
|
<-batch.done
|
||||||
|
|
||||||
var data []file.ID
|
var data []models.FileID
|
||||||
if pos < len(batch.data) {
|
if pos < len(batch.data) {
|
||||||
data = batch.data[pos]
|
data = batch.data[pos]
|
||||||
}
|
}
|
||||||
|
|
@ -113,49 +113,49 @@ func (l *ImageFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||||
|
|
||||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||||
// sub batches depending on how the loader is configured
|
// sub batches depending on how the loader is configured
|
||||||
func (l *ImageFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
|
func (l *ImageFileIDsLoader) LoadAll(keys []int) ([][]models.FileID, []error) {
|
||||||
results := make([]func() ([]file.ID, error), len(keys))
|
results := make([]func() ([]models.FileID, error), len(keys))
|
||||||
|
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
results[i] = l.LoadThunk(key)
|
results[i] = l.LoadThunk(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
iDs := make([][]file.ID, len(keys))
|
fileIDs := make([][]models.FileID, len(keys))
|
||||||
errors := make([]error, len(keys))
|
errors := make([]error, len(keys))
|
||||||
for i, thunk := range results {
|
for i, thunk := range results {
|
||||||
iDs[i], errors[i] = thunk()
|
fileIDs[i], errors[i] = thunk()
|
||||||
}
|
}
|
||||||
return iDs, errors
|
return fileIDs, errors
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAllThunk returns a function that when called will block waiting for a IDs.
|
// LoadAllThunk returns a function that when called will block waiting for a FileIDs.
|
||||||
// This method should be used if you want one goroutine to make requests to many
|
// This method should be used if you want one goroutine to make requests to many
|
||||||
// different data loaders without blocking until the thunk is called.
|
// different data loaders without blocking until the thunk is called.
|
||||||
func (l *ImageFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
|
func (l *ImageFileIDsLoader) LoadAllThunk(keys []int) func() ([][]models.FileID, []error) {
|
||||||
results := make([]func() ([]file.ID, error), len(keys))
|
results := make([]func() ([]models.FileID, error), len(keys))
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
results[i] = l.LoadThunk(key)
|
results[i] = l.LoadThunk(key)
|
||||||
}
|
}
|
||||||
return func() ([][]file.ID, []error) {
|
return func() ([][]models.FileID, []error) {
|
||||||
iDs := make([][]file.ID, len(keys))
|
fileIDs := make([][]models.FileID, len(keys))
|
||||||
errors := make([]error, len(keys))
|
errors := make([]error, len(keys))
|
||||||
for i, thunk := range results {
|
for i, thunk := range results {
|
||||||
iDs[i], errors[i] = thunk()
|
fileIDs[i], errors[i] = thunk()
|
||||||
}
|
}
|
||||||
return iDs, errors
|
return fileIDs, errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||||
// and false is returned.
|
// and false is returned.
|
||||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||||
func (l *ImageFileIDsLoader) Prime(key int, value []file.ID) bool {
|
func (l *ImageFileIDsLoader) Prime(key int, value []models.FileID) bool {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
var found bool
|
var found bool
|
||||||
if _, found = l.cache[key]; !found {
|
if _, found = l.cache[key]; !found {
|
||||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||||
// and end up with the whole cache pointing to the same value.
|
// and end up with the whole cache pointing to the same value.
|
||||||
cpy := make([]file.ID, len(value))
|
cpy := make([]models.FileID, len(value))
|
||||||
copy(cpy, value)
|
copy(cpy, value)
|
||||||
l.unsafeSet(key, cpy)
|
l.unsafeSet(key, cpy)
|
||||||
}
|
}
|
||||||
|
|
@ -170,9 +170,9 @@ func (l *ImageFileIDsLoader) Clear(key int) {
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *ImageFileIDsLoader) unsafeSet(key int, value []file.ID) {
|
func (l *ImageFileIDsLoader) unsafeSet(key int, value []models.FileID) {
|
||||||
if l.cache == nil {
|
if l.cache == nil {
|
||||||
l.cache = map[int][]file.ID{}
|
l.cache = map[int][]models.FileID{}
|
||||||
}
|
}
|
||||||
l.cache[key] = value
|
l.cache[key] = value
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SceneFileIDsLoaderConfig captures the config to create a new SceneFileIDsLoader
|
// SceneFileIDsLoaderConfig captures the config to create a new SceneFileIDsLoader
|
||||||
type SceneFileIDsLoaderConfig struct {
|
type SceneFileIDsLoaderConfig struct {
|
||||||
// Fetch is a method that provides the data for the loader
|
// Fetch is a method that provides the data for the loader
|
||||||
Fetch func(keys []int) ([][]file.ID, []error)
|
Fetch func(keys []int) ([][]models.FileID, []error)
|
||||||
|
|
||||||
// Wait is how long wait before sending a batch
|
// Wait is how long wait before sending a batch
|
||||||
Wait time.Duration
|
Wait time.Duration
|
||||||
|
|
@ -33,7 +33,7 @@ func NewSceneFileIDsLoader(config SceneFileIDsLoaderConfig) *SceneFileIDsLoader
|
||||||
// SceneFileIDsLoader batches and caches requests
|
// SceneFileIDsLoader batches and caches requests
|
||||||
type SceneFileIDsLoader struct {
|
type SceneFileIDsLoader struct {
|
||||||
// this method provides the data for the loader
|
// this method provides the data for the loader
|
||||||
fetch func(keys []int) ([][]file.ID, []error)
|
fetch func(keys []int) ([][]models.FileID, []error)
|
||||||
|
|
||||||
// how long to done before sending a batch
|
// how long to done before sending a batch
|
||||||
wait time.Duration
|
wait time.Duration
|
||||||
|
|
@ -44,7 +44,7 @@ type SceneFileIDsLoader struct {
|
||||||
// INTERNAL
|
// INTERNAL
|
||||||
|
|
||||||
// lazily created cache
|
// lazily created cache
|
||||||
cache map[int][]file.ID
|
cache map[int][]models.FileID
|
||||||
|
|
||||||
// the current batch. keys will continue to be collected until timeout is hit,
|
// the current batch. keys will continue to be collected until timeout is hit,
|
||||||
// then everything will be sent to the fetch method and out to the listeners
|
// then everything will be sent to the fetch method and out to the listeners
|
||||||
|
|
@ -56,25 +56,25 @@ type SceneFileIDsLoader struct {
|
||||||
|
|
||||||
type sceneFileIDsLoaderBatch struct {
|
type sceneFileIDsLoaderBatch struct {
|
||||||
keys []int
|
keys []int
|
||||||
data [][]file.ID
|
data [][]models.FileID
|
||||||
error []error
|
error []error
|
||||||
closing bool
|
closing bool
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load a ID by key, batching and caching will be applied automatically
|
// Load a FileID by key, batching and caching will be applied automatically
|
||||||
func (l *SceneFileIDsLoader) Load(key int) ([]file.ID, error) {
|
func (l *SceneFileIDsLoader) Load(key int) ([]models.FileID, error) {
|
||||||
return l.LoadThunk(key)()
|
return l.LoadThunk(key)()
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadThunk returns a function that when called will block waiting for a ID.
|
// LoadThunk returns a function that when called will block waiting for a FileID.
|
||||||
// This method should be used if you want one goroutine to make requests to many
|
// This method should be used if you want one goroutine to make requests to many
|
||||||
// different data loaders without blocking until the thunk is called.
|
// different data loaders without blocking until the thunk is called.
|
||||||
func (l *SceneFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
func (l *SceneFileIDsLoader) LoadThunk(key int) func() ([]models.FileID, error) {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
if it, ok := l.cache[key]; ok {
|
if it, ok := l.cache[key]; ok {
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
return func() ([]file.ID, error) {
|
return func() ([]models.FileID, error) {
|
||||||
return it, nil
|
return it, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,10 +85,10 @@ func (l *SceneFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||||
pos := batch.keyIndex(l, key)
|
pos := batch.keyIndex(l, key)
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
|
|
||||||
return func() ([]file.ID, error) {
|
return func() ([]models.FileID, error) {
|
||||||
<-batch.done
|
<-batch.done
|
||||||
|
|
||||||
var data []file.ID
|
var data []models.FileID
|
||||||
if pos < len(batch.data) {
|
if pos < len(batch.data) {
|
||||||
data = batch.data[pos]
|
data = batch.data[pos]
|
||||||
}
|
}
|
||||||
|
|
@ -113,49 +113,49 @@ func (l *SceneFileIDsLoader) LoadThunk(key int) func() ([]file.ID, error) {
|
||||||
|
|
||||||
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
// LoadAll fetches many keys at once. It will be broken into appropriate sized
|
||||||
// sub batches depending on how the loader is configured
|
// sub batches depending on how the loader is configured
|
||||||
func (l *SceneFileIDsLoader) LoadAll(keys []int) ([][]file.ID, []error) {
|
func (l *SceneFileIDsLoader) LoadAll(keys []int) ([][]models.FileID, []error) {
|
||||||
results := make([]func() ([]file.ID, error), len(keys))
|
results := make([]func() ([]models.FileID, error), len(keys))
|
||||||
|
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
results[i] = l.LoadThunk(key)
|
results[i] = l.LoadThunk(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
iDs := make([][]file.ID, len(keys))
|
fileIDs := make([][]models.FileID, len(keys))
|
||||||
errors := make([]error, len(keys))
|
errors := make([]error, len(keys))
|
||||||
for i, thunk := range results {
|
for i, thunk := range results {
|
||||||
iDs[i], errors[i] = thunk()
|
fileIDs[i], errors[i] = thunk()
|
||||||
}
|
}
|
||||||
return iDs, errors
|
return fileIDs, errors
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAllThunk returns a function that when called will block waiting for a IDs.
|
// LoadAllThunk returns a function that when called will block waiting for a FileIDs.
|
||||||
// This method should be used if you want one goroutine to make requests to many
|
// This method should be used if you want one goroutine to make requests to many
|
||||||
// different data loaders without blocking until the thunk is called.
|
// different data loaders without blocking until the thunk is called.
|
||||||
func (l *SceneFileIDsLoader) LoadAllThunk(keys []int) func() ([][]file.ID, []error) {
|
func (l *SceneFileIDsLoader) LoadAllThunk(keys []int) func() ([][]models.FileID, []error) {
|
||||||
results := make([]func() ([]file.ID, error), len(keys))
|
results := make([]func() ([]models.FileID, error), len(keys))
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
results[i] = l.LoadThunk(key)
|
results[i] = l.LoadThunk(key)
|
||||||
}
|
}
|
||||||
return func() ([][]file.ID, []error) {
|
return func() ([][]models.FileID, []error) {
|
||||||
iDs := make([][]file.ID, len(keys))
|
fileIDs := make([][]models.FileID, len(keys))
|
||||||
errors := make([]error, len(keys))
|
errors := make([]error, len(keys))
|
||||||
for i, thunk := range results {
|
for i, thunk := range results {
|
||||||
iDs[i], errors[i] = thunk()
|
fileIDs[i], errors[i] = thunk()
|
||||||
}
|
}
|
||||||
return iDs, errors
|
return fileIDs, errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
// Prime the cache with the provided key and value. If the key already exists, no change is made
|
||||||
// and false is returned.
|
// and false is returned.
|
||||||
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
|
||||||
func (l *SceneFileIDsLoader) Prime(key int, value []file.ID) bool {
|
func (l *SceneFileIDsLoader) Prime(key int, value []models.FileID) bool {
|
||||||
l.mu.Lock()
|
l.mu.Lock()
|
||||||
var found bool
|
var found bool
|
||||||
if _, found = l.cache[key]; !found {
|
if _, found = l.cache[key]; !found {
|
||||||
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
|
||||||
// and end up with the whole cache pointing to the same value.
|
// and end up with the whole cache pointing to the same value.
|
||||||
cpy := make([]file.ID, len(value))
|
cpy := make([]models.FileID, len(value))
|
||||||
copy(cpy, value)
|
copy(cpy, value)
|
||||||
l.unsafeSet(key, cpy)
|
l.unsafeSet(key, cpy)
|
||||||
}
|
}
|
||||||
|
|
@ -170,9 +170,9 @@ func (l *SceneFileIDsLoader) Clear(key int) {
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *SceneFileIDsLoader) unsafeSet(key int, value []file.ID) {
|
func (l *SceneFileIDsLoader) unsafeSet(key int, value []models.FileID) {
|
||||||
if l.cache == nil {
|
if l.cache == nil {
|
||||||
l.cache = map[int][]file.ID{}
|
l.cache = map[int][]models.FileID{}
|
||||||
}
|
}
|
||||||
l.cache[key] = value
|
l.cache[key] = value
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,16 @@ import (
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type BaseFile interface{}
|
||||||
|
|
||||||
|
type GalleryFile struct {
|
||||||
|
*models.BaseFile
|
||||||
|
}
|
||||||
|
|
||||||
var ErrTimestamp = errors.New("cannot parse Timestamp")
|
var ErrTimestamp = errors.New("cannot parse Timestamp")
|
||||||
|
|
||||||
func MarshalTimestamp(t time.Time) graphql.Marshaler {
|
func MarshalTimestamp(t time.Time) graphql.Marshaler {
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,16 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/api/loaders"
|
"github.com/stashapp/stash/internal/api/loaders"
|
||||||
"github.com/stashapp/stash/internal/manager/config"
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/image"
|
"github.com/stashapp/stash/pkg/image"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *galleryResolver) getPrimaryFile(ctx context.Context, obj *models.Gallery) (file.File, error) {
|
func (r *galleryResolver) getPrimaryFile(ctx context.Context, obj *models.Gallery) (models.File, error) {
|
||||||
if obj.PrimaryFileID != nil {
|
if obj.PrimaryFileID != nil {
|
||||||
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -26,7 +24,7 @@ func (r *galleryResolver) getPrimaryFile(ctx context.Context, obj *models.Galler
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *galleryResolver) getFiles(ctx context.Context, obj *models.Gallery) ([]file.File, error) {
|
func (r *galleryResolver) getFiles(ctx context.Context, obj *models.Gallery) ([]models.File, error) {
|
||||||
fileIDs, err := loaders.From(ctx).GalleryFiles.Load(obj.ID)
|
fileIDs, err := loaders.From(ctx).GalleryFiles.Load(obj.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -45,34 +43,20 @@ func (r *galleryResolver) Files(ctx context.Context, obj *models.Gallery) ([]*Ga
|
||||||
ret := make([]*GalleryFile, len(files))
|
ret := make([]*GalleryFile, len(files))
|
||||||
|
|
||||||
for i, f := range files {
|
for i, f := range files {
|
||||||
base := f.Base()
|
|
||||||
ret[i] = &GalleryFile{
|
ret[i] = &GalleryFile{
|
||||||
ID: strconv.Itoa(int(base.ID)),
|
BaseFile: f.Base(),
|
||||||
Path: base.Path,
|
|
||||||
Basename: base.Basename,
|
|
||||||
ParentFolderID: strconv.Itoa(int(base.ParentFolderID)),
|
|
||||||
ModTime: base.ModTime,
|
|
||||||
Size: base.Size,
|
|
||||||
CreatedAt: base.CreatedAt,
|
|
||||||
UpdatedAt: base.UpdatedAt,
|
|
||||||
Fingerprints: resolveFingerprints(base),
|
|
||||||
}
|
|
||||||
|
|
||||||
if base.ZipFileID != nil {
|
|
||||||
zipFileID := strconv.Itoa(int(*base.ZipFileID))
|
|
||||||
ret[i].ZipFileID = &zipFileID
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*Folder, error) {
|
func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*models.Folder, error) {
|
||||||
if obj.FolderID == nil {
|
if obj.FolderID == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var ret *file.Folder
|
var ret *models.Folder
|
||||||
|
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -91,25 +75,7 @@ func (r *galleryResolver) Folder(ctx context.Context, obj *models.Gallery) (*Fol
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rr := &Folder{
|
return ret, nil
|
||||||
ID: ret.ID.String(),
|
|
||||||
Path: ret.Path,
|
|
||||||
ModTime: ret.ModTime,
|
|
||||||
CreatedAt: ret.CreatedAt,
|
|
||||||
UpdatedAt: ret.UpdatedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
if ret.ParentFolderID != nil {
|
|
||||||
pfidStr := ret.ParentFolderID.String()
|
|
||||||
rr.ParentFolderID = &pfidStr
|
|
||||||
}
|
|
||||||
|
|
||||||
if ret.ZipFileID != nil {
|
|
||||||
zfidStr := ret.ZipFileID.String()
|
|
||||||
rr.ZipFileID = &zfidStr
|
|
||||||
}
|
|
||||||
|
|
||||||
return rr, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
|
func (r *galleryResolver) FileModTime(ctx context.Context, obj *models.Gallery) (*time.Time, error) {
|
||||||
|
|
|
||||||
|
|
@ -3,57 +3,35 @@ package api
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/api/loaders"
|
"github.com/stashapp/stash/internal/api/loaders"
|
||||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func convertImageFile(f *file.ImageFile) *ImageFile {
|
func convertVisualFile(f models.File) (models.VisualFile, error) {
|
||||||
ret := &ImageFile{
|
vf, ok := f.(models.VisualFile)
|
||||||
ID: strconv.Itoa(int(f.ID)),
|
if !ok {
|
||||||
Path: f.Path,
|
return nil, fmt.Errorf("file %s is not a visual file", f.Base().Path)
|
||||||
Basename: f.Basename,
|
}
|
||||||
ParentFolderID: strconv.Itoa(int(f.ParentFolderID)),
|
return vf, nil
|
||||||
ModTime: f.ModTime,
|
|
||||||
Size: f.Size,
|
|
||||||
Width: f.Width,
|
|
||||||
Height: f.Height,
|
|
||||||
CreatedAt: f.CreatedAt,
|
|
||||||
UpdatedAt: f.UpdatedAt,
|
|
||||||
Fingerprints: resolveFingerprints(f.Base()),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.ZipFileID != nil {
|
func (r *imageResolver) getPrimaryFile(ctx context.Context, obj *models.Image) (models.VisualFile, error) {
|
||||||
zipFileID := strconv.Itoa(int(*f.ZipFileID))
|
|
||||||
ret.ZipFileID = &zipFileID
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *imageResolver) getPrimaryFile(ctx context.Context, obj *models.Image) (file.VisualFile, error) {
|
|
||||||
if obj.PrimaryFileID != nil {
|
if obj.PrimaryFileID != nil {
|
||||||
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
asFrame, ok := f.(file.VisualFile)
|
return convertVisualFile(f)
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("file %T is not an frame", f)
|
|
||||||
}
|
|
||||||
|
|
||||||
return asFrame, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *imageResolver) getFiles(ctx context.Context, obj *models.Image) ([]file.File, error) {
|
func (r *imageResolver) getFiles(ctx context.Context, obj *models.Image) ([]models.File, error) {
|
||||||
fileIDs, err := loaders.From(ctx).ImageFiles.Load(obj.ID)
|
fileIDs, err := loaders.From(ctx).ImageFiles.Load(obj.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -88,30 +66,21 @@ func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*ImageFile
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertVisualFile(f file.File) VisualFile {
|
func (r *imageResolver) VisualFiles(ctx context.Context, obj *models.Image) ([]models.VisualFile, error) {
|
||||||
switch f := f.(type) {
|
files, err := r.getFiles(ctx, obj)
|
||||||
case *file.ImageFile:
|
|
||||||
return convertImageFile(f)
|
|
||||||
case *file.VideoFile:
|
|
||||||
return convertVideoFile(f)
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown file type %T", f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *imageResolver) VisualFiles(ctx context.Context, obj *models.Image) ([]VisualFile, error) {
|
|
||||||
fileIDs, err := loaders.From(ctx).ImageFiles.Load(obj.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
files, errs := loaders.From(ctx).FileByID.LoadAll(fileIDs)
|
ret := make([]models.VisualFile, len(files))
|
||||||
ret := make([]VisualFile, len(files))
|
|
||||||
for i, f := range files {
|
for i, f := range files {
|
||||||
ret[i] = convertVisualFile(f)
|
ret[i], err = convertVisualFile(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret, firstError(errs)
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *imageResolver) Date(ctx context.Context, obj *models.Image) (*string, error) {
|
func (r *imageResolver) Date(ctx context.Context, obj *models.Image) (*string, error) {
|
||||||
|
|
@ -122,24 +91,22 @@ func (r *imageResolver) Date(ctx context.Context, obj *models.Image) (*string, e
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) {
|
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*models.ImageFile, error) {
|
||||||
files, err := r.getFiles(ctx, obj)
|
files, err := r.getFiles(ctx, obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var ret []*ImageFile
|
var ret []*models.ImageFile
|
||||||
|
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
// filter out non-image files
|
// filter out non-image files
|
||||||
imageFile, ok := f.(*file.ImageFile)
|
imageFile, ok := f.(*models.ImageFile)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
thisFile := convertImageFile(imageFile)
|
ret = append(ret, imageFile)
|
||||||
|
|
||||||
ret = append(ret, thisFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
|
|
|
||||||
|
|
@ -9,50 +9,28 @@ import (
|
||||||
"github.com/stashapp/stash/internal/api/loaders"
|
"github.com/stashapp/stash/internal/api/loaders"
|
||||||
"github.com/stashapp/stash/internal/api/urlbuilders"
|
"github.com/stashapp/stash/internal/api/urlbuilders"
|
||||||
"github.com/stashapp/stash/internal/manager"
|
"github.com/stashapp/stash/internal/manager"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func convertVideoFile(f *file.VideoFile) *VideoFile {
|
func convertVideoFile(f models.File) (*models.VideoFile, error) {
|
||||||
ret := &VideoFile{
|
vf, ok := f.(*models.VideoFile)
|
||||||
ID: strconv.Itoa(int(f.ID)),
|
if !ok {
|
||||||
Path: f.Path,
|
return nil, fmt.Errorf("file %T is not a video file", f)
|
||||||
Basename: f.Basename,
|
}
|
||||||
ParentFolderID: strconv.Itoa(int(f.ParentFolderID)),
|
return vf, nil
|
||||||
ModTime: f.ModTime,
|
|
||||||
Format: f.Format,
|
|
||||||
Size: f.Size,
|
|
||||||
Duration: handleFloat64Value(f.Duration),
|
|
||||||
VideoCodec: f.VideoCodec,
|
|
||||||
AudioCodec: f.AudioCodec,
|
|
||||||
Width: f.Width,
|
|
||||||
Height: f.Height,
|
|
||||||
FrameRate: handleFloat64Value(f.FrameRate),
|
|
||||||
BitRate: int(f.BitRate),
|
|
||||||
CreatedAt: f.CreatedAt,
|
|
||||||
UpdatedAt: f.UpdatedAt,
|
|
||||||
Fingerprints: resolveFingerprints(f.Base()),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.ZipFileID != nil {
|
func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (*models.VideoFile, error) {
|
||||||
zipFileID := strconv.Itoa(int(*f.ZipFileID))
|
|
||||||
ret.ZipFileID = &zipFileID
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (*file.VideoFile, error) {
|
|
||||||
if obj.PrimaryFileID != nil {
|
if obj.PrimaryFileID != nil {
|
||||||
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
f, err := loaders.From(ctx).FileByID.Load(*obj.PrimaryFileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ret, ok := f.(*file.VideoFile)
|
ret, err := convertVideoFile(f)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("file %T is not an image file", f)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.Files.SetPrimary(ret)
|
obj.Files.SetPrimary(ret)
|
||||||
|
|
@ -65,26 +43,29 @@ func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *sceneResolver) getFiles(ctx context.Context, obj *models.Scene) ([]*file.VideoFile, error) {
|
func (r *sceneResolver) getFiles(ctx context.Context, obj *models.Scene) ([]*models.VideoFile, error) {
|
||||||
fileIDs, err := loaders.From(ctx).SceneFiles.Load(obj.ID)
|
fileIDs, err := loaders.From(ctx).SceneFiles.Load(obj.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
files, errs := loaders.From(ctx).FileByID.LoadAll(fileIDs)
|
files, errs := loaders.From(ctx).FileByID.LoadAll(fileIDs)
|
||||||
ret := make([]*file.VideoFile, len(files))
|
err = firstError(errs)
|
||||||
for i, bf := range files {
|
if err != nil {
|
||||||
f, ok := bf.(*file.VideoFile)
|
return nil, err
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("file %T is not a video file", f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret[i] = f
|
ret := make([]*models.VideoFile, len(files))
|
||||||
|
for i, f := range files {
|
||||||
|
ret[i], err = convertVideoFile(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.Files.Set(ret)
|
obj.Files.Set(ret)
|
||||||
|
|
||||||
return ret, firstError(errs)
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *sceneResolver) FileModTime(ctx context.Context, obj *models.Scene) (*time.Time, error) {
|
func (r *sceneResolver) FileModTime(ctx context.Context, obj *models.Scene) (*time.Time, error) {
|
||||||
|
|
@ -132,19 +113,13 @@ func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.Sc
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*VideoFile, error) {
|
func (r *sceneResolver) Files(ctx context.Context, obj *models.Scene) ([]*models.VideoFile, error) {
|
||||||
files, err := r.getFiles(ctx, obj)
|
files, err := r.getFiles(ctx, obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ret := make([]*VideoFile, len(files))
|
return files, nil
|
||||||
|
|
||||||
for i, f := range files {
|
|
||||||
ret[i] = convertVideoFile(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*int, error) {
|
func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*int, error) {
|
||||||
|
|
@ -159,28 +134,6 @@ func (r *sceneResolver) Rating100(ctx context.Context, obj *models.Scene) (*int,
|
||||||
return obj.Rating, nil
|
return obj.Rating, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveFingerprints(f *file.BaseFile) []*Fingerprint {
|
|
||||||
ret := make([]*Fingerprint, len(f.Fingerprints))
|
|
||||||
|
|
||||||
for i, fp := range f.Fingerprints {
|
|
||||||
ret[i] = &Fingerprint{
|
|
||||||
Type: fp.Type,
|
|
||||||
Value: formatFingerprint(fp.Fingerprint),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatFingerprint(fp interface{}) string {
|
|
||||||
switch v := fp.(type) {
|
|
||||||
case int64:
|
|
||||||
return strconv.FormatUint(uint64(v), 16)
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%v", fp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePathsType, error) {
|
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePathsType, error) {
|
||||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||||
config := manager.GetInstance().Config
|
config := manager.GetInstance().Config
|
||||||
|
|
@ -352,7 +305,7 @@ func (r *sceneResolver) Phash(ctx context.Context, obj *models.Scene) (*string,
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
val := f.Fingerprints.Get(file.FingerprintTypePhash)
|
val := f.Fingerprints.Get(models.FingerprintTypePhash)
|
||||||
if val == nil {
|
if val == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/stashapp/stash/internal/manager"
|
"github.com/stashapp/stash/internal/manager"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
"github.com/stashapp/stash/pkg/file"
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -19,7 +20,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput)
|
||||||
mover.RegisterHooks(ctx, r.txnManager)
|
mover.RegisterHooks(ctx, r.txnManager)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
folder *file.Folder
|
folder *models.Folder
|
||||||
basename string
|
basename string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -37,7 +38,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput)
|
||||||
return fmt.Errorf("invalid folder id %s: %w", *input.DestinationFolderID, err)
|
return fmt.Errorf("invalid folder id %s: %w", *input.DestinationFolderID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
folder, err = folderStore.Find(ctx, file.FolderID(folderID))
|
folder, err = folderStore.Find(ctx, models.FolderID(folderID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("finding destination folder: %w", err)
|
return fmt.Errorf("finding destination folder: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +83,7 @@ func (r *mutationResolver) MoveFiles(ctx context.Context, input MoveFilesInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fileIDInt := range fileIDs {
|
for _, fileIDInt := range fileIDs {
|
||||||
fileID := file.ID(fileIDInt)
|
fileID := models.FileID(fileIDInt)
|
||||||
f, err := fileStore.Find(ctx, fileID)
|
f, err := fileStore.Find(ctx, fileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("finding file %d: %w", fileID, err)
|
return fmt.Errorf("finding file %d: %w", fileID, err)
|
||||||
|
|
@ -158,7 +159,7 @@ func (r *mutationResolver) DeleteFiles(ctx context.Context, ids []string) (ret b
|
||||||
qb := r.repository.File
|
qb := r.repository.File
|
||||||
|
|
||||||
for _, fileIDInt := range fileIDs {
|
for _, fileIDInt := range fileIDs {
|
||||||
fileID := file.ID(fileIDInt)
|
fileID := models.FileID(fileIDInt)
|
||||||
f, err := qb.Find(ctx, fileID)
|
f, err := qb.Find(ctx, fileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
||||||
return nil, fmt.Errorf("converting primary file id: %w", err)
|
return nil, fmt.Errorf("converting primary file id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
converted := file.ID(primaryFileID)
|
converted := models.FileID(primaryFileID)
|
||||||
updatedGallery.PrimaryFileID = &converted
|
updatedGallery.PrimaryFileID = &converted
|
||||||
|
|
||||||
if err := originalGallery.LoadFiles(ctx, r.repository.Gallery); err != nil {
|
if err := originalGallery.LoadFiles(ctx, r.repository.Gallery); err != nil {
|
||||||
|
|
@ -207,7 +207,7 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure that new primary file is associated with gallery
|
// ensure that new primary file is associated with gallery
|
||||||
var f file.File
|
var f models.File
|
||||||
for _, ff := range originalGallery.Files.List() {
|
for _, ff := range originalGallery.Files.List() {
|
||||||
if ff.Base().ID == converted {
|
if ff.Base().ID == converted {
|
||||||
f = ff
|
f = ff
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
||||||
return nil, fmt.Errorf("converting primary file id: %w", err)
|
return nil, fmt.Errorf("converting primary file id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
converted := file.ID(primaryFileID)
|
converted := models.FileID(primaryFileID)
|
||||||
updatedImage.PrimaryFileID = &converted
|
updatedImage.PrimaryFileID = &converted
|
||||||
|
|
||||||
if err := i.LoadFiles(ctx, r.repository.Image); err != nil {
|
if err := i.LoadFiles(ctx, r.repository.Image); err != nil {
|
||||||
|
|
@ -131,7 +131,7 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure that new primary file is associated with image
|
// ensure that new primary file is associated with image
|
||||||
var f file.File
|
var f models.File
|
||||||
for _, ff := range i.Files.List() {
|
for _, ff := range i.Files.List() {
|
||||||
if ff.Base().ID == converted {
|
if ff.Base().ID == converted {
|
||||||
f = ff
|
f = ff
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,9 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input SceneCreateInp
|
||||||
return nil, fmt.Errorf("converting file ids: %w", err)
|
return nil, fmt.Errorf("converting file ids: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileIDs := make([]file.ID, len(fileIDsInt))
|
fileIDs := make([]models.FileID, len(fileIDsInt))
|
||||||
for i, v := range fileIDsInt {
|
for i, v := range fileIDsInt {
|
||||||
fileIDs[i] = file.ID(v)
|
fileIDs[i] = models.FileID(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate a new scene from the input
|
// Populate a new scene from the input
|
||||||
|
|
@ -212,7 +212,7 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr
|
||||||
return nil, fmt.Errorf("converting primary file id: %w", err)
|
return nil, fmt.Errorf("converting primary file id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
converted := file.ID(primaryFileID)
|
converted := models.FileID(primaryFileID)
|
||||||
updatedScene.PrimaryFileID = &converted
|
updatedScene.PrimaryFileID = &converted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,7 +300,7 @@ func (r *mutationResolver) sceneUpdate(ctx context.Context, input models.SceneUp
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure that new primary file is associated with scene
|
// ensure that new primary file is associated with scene
|
||||||
var f *file.VideoFile
|
var f *models.VideoFile
|
||||||
for _, ff := range originalScene.Files.List() {
|
for _, ff := range originalScene.Files.List() {
|
||||||
if ff.ID == newPrimaryFileID {
|
if ff.ID == newPrimaryFileID {
|
||||||
f = ff
|
f = ff
|
||||||
|
|
@ -575,7 +575,7 @@ func (r *mutationResolver) SceneAssignFile(ctx context.Context, input AssignScen
|
||||||
return false, fmt.Errorf("converting file ID: %w", err)
|
return false, fmt.Errorf("converting file ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileID := file.ID(fileIDInt)
|
fileID := models.FileID(fileIDInt)
|
||||||
|
|
||||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||||
return r.Resolver.sceneService.AssignFile(ctx, sceneID, fileID)
|
return r.Resolver.sceneService.AssignFile(ctx, sceneID, fileID)
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ImageFinder interface {
|
type ImageFinder interface {
|
||||||
Find(ctx context.Context, id int) (*models.Image, error)
|
models.ImageGetter
|
||||||
FindByChecksum(ctx context.Context, checksum string) ([]*models.Image, error)
|
FindByChecksum(ctx context.Context, checksum string) ([]*models.Image, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type imageRoutes struct {
|
type imageRoutes struct {
|
||||||
txnManager txn.Manager
|
txnManager txn.Manager
|
||||||
imageFinder ImageFinder
|
imageFinder ImageFinder
|
||||||
fileFinder file.Finder
|
fileGetter models.FileGetter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs imageRoutes) Routes() chi.Router {
|
func (rs imageRoutes) Routes() chi.Router {
|
||||||
|
|
@ -168,7 +168,7 @@ func (rs imageRoutes) ImageCtx(next http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if image != nil {
|
if image != nil {
|
||||||
if err := image.LoadPrimaryFile(ctx, rs.fileFinder); err != nil {
|
if err := image.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
|
||||||
if !errors.Is(err, context.Canceled) {
|
if !errors.Is(err, context.Canceled) {
|
||||||
logger.Errorf("error loading primary file for image %d: %v", imageID, err)
|
logger.Errorf("error loading primary file for image %d: %v", imageID, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type MovieFinder interface {
|
type MovieFinder interface {
|
||||||
|
models.MovieGetter
|
||||||
GetFrontImage(ctx context.Context, movieID int) ([]byte, error)
|
GetFrontImage(ctx context.Context, movieID int) ([]byte, error)
|
||||||
GetBackImage(ctx context.Context, movieID int) ([]byte, error)
|
GetBackImage(ctx context.Context, movieID int) ([]byte, error)
|
||||||
Find(ctx context.Context, id int) (*models.Movie, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type movieRoutes struct {
|
type movieRoutes struct {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type PerformerFinder interface {
|
type PerformerFinder interface {
|
||||||
Find(ctx context.Context, id int) (*models.Performer, error)
|
models.PerformerGetter
|
||||||
GetImage(ctx context.Context, performerID int) ([]byte, error)
|
GetImage(ctx context.Context, performerID int) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,40 +12,43 @@ import (
|
||||||
"github.com/stashapp/stash/internal/manager"
|
"github.com/stashapp/stash/internal/manager"
|
||||||
"github.com/stashapp/stash/internal/manager/config"
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/file/video"
|
"github.com/stashapp/stash/pkg/file/video"
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SceneFinder interface {
|
type SceneFinder interface {
|
||||||
manager.SceneCoverGetter
|
models.SceneGetter
|
||||||
|
|
||||||
scene.IDFinder
|
|
||||||
FindByChecksum(ctx context.Context, checksum string) ([]*models.Scene, error)
|
FindByChecksum(ctx context.Context, checksum string) ([]*models.Scene, error)
|
||||||
FindByOSHash(ctx context.Context, oshash string) ([]*models.Scene, error)
|
FindByOSHash(ctx context.Context, oshash string) ([]*models.Scene, error)
|
||||||
|
GetCover(ctx context.Context, sceneID int) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SceneMarkerFinder interface {
|
type SceneMarkerFinder interface {
|
||||||
Find(ctx context.Context, id int) (*models.SceneMarker, error)
|
models.SceneMarkerGetter
|
||||||
FindBySceneID(ctx context.Context, sceneID int) ([]*models.SceneMarker, error)
|
FindBySceneID(ctx context.Context, sceneID int) ([]*models.SceneMarker, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SceneMarkerTagFinder interface {
|
||||||
|
models.TagGetter
|
||||||
|
FindBySceneMarkerID(ctx context.Context, sceneMarkerID int) ([]*models.Tag, error)
|
||||||
|
}
|
||||||
|
|
||||||
type CaptionFinder interface {
|
type CaptionFinder interface {
|
||||||
GetCaptions(ctx context.Context, fileID file.ID) ([]*models.VideoCaption, error)
|
GetCaptions(ctx context.Context, fileID models.FileID) ([]*models.VideoCaption, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sceneRoutes struct {
|
type sceneRoutes struct {
|
||||||
txnManager txn.Manager
|
txnManager txn.Manager
|
||||||
sceneFinder SceneFinder
|
sceneFinder SceneFinder
|
||||||
fileFinder file.Finder
|
fileGetter models.FileGetter
|
||||||
captionFinder CaptionFinder
|
captionFinder CaptionFinder
|
||||||
sceneMarkerFinder SceneMarkerFinder
|
sceneMarkerFinder SceneMarkerFinder
|
||||||
tagFinder scene.MarkerTagFinder
|
tagFinder SceneMarkerTagFinder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs sceneRoutes) Routes() chi.Router {
|
func (rs sceneRoutes) Routes() chi.Router {
|
||||||
|
|
@ -574,7 +577,7 @@ func (rs sceneRoutes) SceneCtx(next http.Handler) http.Handler {
|
||||||
scene, _ = qb.Find(ctx, sceneID)
|
scene, _ = qb.Find(ctx, sceneID)
|
||||||
|
|
||||||
if scene != nil {
|
if scene != nil {
|
||||||
if err := scene.LoadPrimaryFile(ctx, rs.fileFinder); err != nil {
|
if err := scene.LoadPrimaryFile(ctx, rs.fileGetter); err != nil {
|
||||||
if !errors.Is(err, context.Canceled) {
|
if !errors.Is(err, context.Canceled) {
|
||||||
logger.Errorf("error loading primary file for scene %d: %v", sceneID, err)
|
logger.Errorf("error loading primary file for scene %d: %v", sceneID, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,12 @@ import (
|
||||||
"github.com/stashapp/stash/internal/static"
|
"github.com/stashapp/stash/internal/static"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/studio"
|
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StudioFinder interface {
|
type StudioFinder interface {
|
||||||
studio.Finder
|
models.StudioGetter
|
||||||
GetImage(ctx context.Context, studioID int) ([]byte, error)
|
GetImage(ctx context.Context, studioID int) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,12 @@ import (
|
||||||
"github.com/stashapp/stash/internal/static"
|
"github.com/stashapp/stash/internal/static"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/tag"
|
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TagFinder interface {
|
type TagFinder interface {
|
||||||
tag.Finder
|
models.TagGetter
|
||||||
GetImage(ctx context.Context, tagID int) ([]byte, error)
|
GetImage(ctx context.Context, tagID int) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ func Start() error {
|
||||||
r.Mount("/scene", sceneRoutes{
|
r.Mount("/scene", sceneRoutes{
|
||||||
txnManager: txnManager,
|
txnManager: txnManager,
|
||||||
sceneFinder: txnManager.Scene,
|
sceneFinder: txnManager.Scene,
|
||||||
fileFinder: txnManager.File,
|
fileGetter: txnManager.File,
|
||||||
captionFinder: txnManager.File,
|
captionFinder: txnManager.File,
|
||||||
sceneMarkerFinder: txnManager.SceneMarker,
|
sceneMarkerFinder: txnManager.SceneMarker,
|
||||||
tagFinder: txnManager.Tag,
|
tagFinder: txnManager.Tag,
|
||||||
|
|
@ -159,7 +159,7 @@ func Start() error {
|
||||||
r.Mount("/image", imageRoutes{
|
r.Mount("/image", imageRoutes{
|
||||||
txnManager: txnManager,
|
txnManager: txnManager,
|
||||||
imageFinder: txnManager.Image,
|
imageFinder: txnManager.Image,
|
||||||
fileFinder: txnManager.File,
|
fileGetter: txnManager.File,
|
||||||
}.Routes())
|
}.Routes())
|
||||||
r.Mount("/studio", studioRoutes{
|
r.Mount("/studio", studioRoutes{
|
||||||
txnManager: txnManager,
|
txnManager: txnManager,
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,6 @@ func handleFloat64(v float64) *float64 {
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleFloat64Value(v float64) float64 {
|
|
||||||
if math.IsInf(v, 0) || math.IsNaN(v) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func translateUpdateIDs(strIDs []string, mode models.RelationshipUpdateMode) (*models.UpdateIDs, error) {
|
func translateUpdateIDs(strIDs []string, mode models.RelationshipUpdateMode) (*models.UpdateIDs, error) {
|
||||||
ids, err := stringslice.StringSliceToIntSlice(strIDs)
|
ids, err := stringslice.StringSliceToIntSlice(strIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,19 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GalleryFinderUpdater interface {
|
||||||
|
models.GalleryQueryer
|
||||||
|
models.GalleryUpdater
|
||||||
|
}
|
||||||
|
|
||||||
type GalleryPerformerUpdater interface {
|
type GalleryPerformerUpdater interface {
|
||||||
models.PerformerIDLoader
|
models.PerformerIDLoader
|
||||||
gallery.PartialUpdater
|
models.GalleryUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
type GalleryTagUpdater interface {
|
type GalleryTagUpdater interface {
|
||||||
models.TagIDLoader
|
models.TagIDLoader
|
||||||
gallery.PartialUpdater
|
models.GalleryUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGalleryFileTagger(s *models.Gallery, cache *match.Cache) tagger {
|
func getGalleryFileTagger(s *models.Gallery, cache *match.Cache) tagger {
|
||||||
|
|
@ -39,7 +44,7 @@ func getGalleryFileTagger(s *models.Gallery, cache *match.Cache) tagger {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GalleryPerformers tags the provided gallery with performers whose name matches the gallery's path.
|
// GalleryPerformers tags the provided gallery with performers whose name matches the gallery's path.
|
||||||
func GalleryPerformers(ctx context.Context, s *models.Gallery, rw GalleryPerformerUpdater, performerReader match.PerformerAutoTagQueryer, cache *match.Cache) error {
|
func GalleryPerformers(ctx context.Context, s *models.Gallery, rw GalleryPerformerUpdater, performerReader models.PerformerAutoTagQueryer, cache *match.Cache) error {
|
||||||
t := getGalleryFileTagger(s, cache)
|
t := getGalleryFileTagger(s, cache)
|
||||||
|
|
||||||
return t.tagPerformers(ctx, performerReader, func(subjectID, otherID int) (bool, error) {
|
return t.tagPerformers(ctx, performerReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
|
@ -63,7 +68,7 @@ func GalleryPerformers(ctx context.Context, s *models.Gallery, rw GalleryPerform
|
||||||
// GalleryStudios tags the provided gallery with the first studio whose name matches the gallery's path.
|
// GalleryStudios tags the provided gallery with the first studio whose name matches the gallery's path.
|
||||||
//
|
//
|
||||||
// Gallerys will not be tagged if studio is already set.
|
// Gallerys will not be tagged if studio is already set.
|
||||||
func GalleryStudios(ctx context.Context, s *models.Gallery, rw GalleryFinderUpdater, studioReader match.StudioAutoTagQueryer, cache *match.Cache) error {
|
func GalleryStudios(ctx context.Context, s *models.Gallery, rw GalleryFinderUpdater, studioReader models.StudioAutoTagQueryer, cache *match.Cache) error {
|
||||||
if s.StudioID != nil {
|
if s.StudioID != nil {
|
||||||
// don't modify
|
// don't modify
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -77,7 +82,7 @@ func GalleryStudios(ctx context.Context, s *models.Gallery, rw GalleryFinderUpda
|
||||||
}
|
}
|
||||||
|
|
||||||
// GalleryTags tags the provided gallery with tags whose name matches the gallery's path.
|
// GalleryTags tags the provided gallery with tags whose name matches the gallery's path.
|
||||||
func GalleryTags(ctx context.Context, s *models.Gallery, rw GalleryTagUpdater, tagReader match.TagAutoTagQueryer, cache *match.Cache) error {
|
func GalleryTags(ctx context.Context, s *models.Gallery, rw GalleryTagUpdater, tagReader models.TagAutoTagQueryer, cache *match.Cache) error {
|
||||||
t := getGalleryFileTagger(s, cache)
|
t := getGalleryFileTagger(s, cache)
|
||||||
|
|
||||||
return t.tagTags(ctx, tagReader, func(subjectID, otherID int) (bool, error) {
|
return t.tagTags(ctx, tagReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,19 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ImageFinderUpdater interface {
|
||||||
|
models.ImageQueryer
|
||||||
|
models.ImageUpdater
|
||||||
|
}
|
||||||
|
|
||||||
type ImagePerformerUpdater interface {
|
type ImagePerformerUpdater interface {
|
||||||
models.PerformerIDLoader
|
models.PerformerIDLoader
|
||||||
image.PartialUpdater
|
models.ImageUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageTagUpdater interface {
|
type ImageTagUpdater interface {
|
||||||
models.TagIDLoader
|
models.TagIDLoader
|
||||||
image.PartialUpdater
|
models.ImageUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
func getImageFileTagger(s *models.Image, cache *match.Cache) tagger {
|
func getImageFileTagger(s *models.Image, cache *match.Cache) tagger {
|
||||||
|
|
@ -30,7 +35,7 @@ func getImageFileTagger(s *models.Image, cache *match.Cache) tagger {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImagePerformers tags the provided image with performers whose name matches the image's path.
|
// ImagePerformers tags the provided image with performers whose name matches the image's path.
|
||||||
func ImagePerformers(ctx context.Context, s *models.Image, rw ImagePerformerUpdater, performerReader match.PerformerAutoTagQueryer, cache *match.Cache) error {
|
func ImagePerformers(ctx context.Context, s *models.Image, rw ImagePerformerUpdater, performerReader models.PerformerAutoTagQueryer, cache *match.Cache) error {
|
||||||
t := getImageFileTagger(s, cache)
|
t := getImageFileTagger(s, cache)
|
||||||
|
|
||||||
return t.tagPerformers(ctx, performerReader, func(subjectID, otherID int) (bool, error) {
|
return t.tagPerformers(ctx, performerReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
|
@ -54,7 +59,7 @@ func ImagePerformers(ctx context.Context, s *models.Image, rw ImagePerformerUpda
|
||||||
// ImageStudios tags the provided image with the first studio whose name matches the image's path.
|
// ImageStudios tags the provided image with the first studio whose name matches the image's path.
|
||||||
//
|
//
|
||||||
// Images will not be tagged if studio is already set.
|
// Images will not be tagged if studio is already set.
|
||||||
func ImageStudios(ctx context.Context, s *models.Image, rw ImageFinderUpdater, studioReader match.StudioAutoTagQueryer, cache *match.Cache) error {
|
func ImageStudios(ctx context.Context, s *models.Image, rw ImageFinderUpdater, studioReader models.StudioAutoTagQueryer, cache *match.Cache) error {
|
||||||
if s.StudioID != nil {
|
if s.StudioID != nil {
|
||||||
// don't modify
|
// don't modify
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -68,7 +73,7 @@ func ImageStudios(ctx context.Context, s *models.Image, rw ImageFinderUpdater, s
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageTags tags the provided image with tags whose name matches the image's path.
|
// ImageTags tags the provided image with tags whose name matches the image's path.
|
||||||
func ImageTags(ctx context.Context, s *models.Image, rw ImageTagUpdater, tagReader match.TagAutoTagQueryer, cache *match.Cache) error {
|
func ImageTags(ctx context.Context, s *models.Image, rw ImageTagUpdater, tagReader models.TagAutoTagQueryer, cache *match.Cache) error {
|
||||||
t := getImageFileTagger(s, cache)
|
t := getImageFileTagger(s, cache)
|
||||||
|
|
||||||
return t.tagTags(ctx, tagReader, func(subjectID, otherID int) (bool, error) {
|
return t.tagTags(ctx, tagReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/sqlite"
|
"github.com/stashapp/stash/pkg/sqlite"
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
|
|
@ -124,12 +123,12 @@ func createTag(ctx context.Context, qb models.TagWriter) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createScenes(ctx context.Context, sqb models.SceneReaderWriter, folderStore file.FolderStore, fileStore file.Store) error {
|
func createScenes(ctx context.Context, sqb models.SceneReaderWriter, folderStore models.FolderFinderCreator, fileCreator models.FileCreator) error {
|
||||||
// create the scenes
|
// create the scenes
|
||||||
scenePatterns, falseScenePatterns := generateTestPaths(testName, sceneExt)
|
scenePatterns, falseScenePatterns := generateTestPaths(testName, sceneExt)
|
||||||
|
|
||||||
for _, fn := range scenePatterns {
|
for _, fn := range scenePatterns {
|
||||||
f, err := createSceneFile(ctx, fn, folderStore, fileStore)
|
f, err := createSceneFile(ctx, fn, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +140,7 @@ func createScenes(ctx context.Context, sqb models.SceneReaderWriter, folderStore
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fn := range falseScenePatterns {
|
for _, fn := range falseScenePatterns {
|
||||||
f, err := createSceneFile(ctx, fn, folderStore, fileStore)
|
f, err := createSceneFile(ctx, fn, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +153,7 @@ func createScenes(ctx context.Context, sqb models.SceneReaderWriter, folderStore
|
||||||
|
|
||||||
// add organized scenes
|
// add organized scenes
|
||||||
for _, fn := range scenePatterns {
|
for _, fn := range scenePatterns {
|
||||||
f, err := createSceneFile(ctx, "organized"+fn, folderStore, fileStore)
|
f, err := createSceneFile(ctx, "organized"+fn, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +167,7 @@ func createScenes(ctx context.Context, sqb models.SceneReaderWriter, folderStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// create scene with existing studio io
|
// create scene with existing studio io
|
||||||
f, err := createSceneFile(ctx, existingStudioSceneName, folderStore, fileStore)
|
f, err := createSceneFile(ctx, existingStudioSceneName, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -196,7 +195,7 @@ func makeScene(expectedResult bool) *models.Scene {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSceneFile(ctx context.Context, name string, folderStore file.FolderStore, fileStore file.Store) (*file.VideoFile, error) {
|
func createSceneFile(ctx context.Context, name string, folderStore models.FolderFinderCreator, fileCreator models.FileCreator) (*models.VideoFile, error) {
|
||||||
folderPath := filepath.Dir(name)
|
folderPath := filepath.Dir(name)
|
||||||
basename := filepath.Base(name)
|
basename := filepath.Base(name)
|
||||||
|
|
||||||
|
|
@ -207,21 +206,21 @@ func createSceneFile(ctx context.Context, name string, folderStore file.FolderSt
|
||||||
|
|
||||||
folderID := folder.ID
|
folderID := folder.ID
|
||||||
|
|
||||||
f := &file.VideoFile{
|
f := &models.VideoFile{
|
||||||
BaseFile: &file.BaseFile{
|
BaseFile: &models.BaseFile{
|
||||||
Basename: basename,
|
Basename: basename,
|
||||||
ParentFolderID: folderID,
|
ParentFolderID: folderID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fileStore.Create(ctx, f); err != nil {
|
if err := fileCreator.Create(ctx, f); err != nil {
|
||||||
return nil, fmt.Errorf("creating scene file %q: %w", name, err)
|
return nil, fmt.Errorf("creating scene file %q: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOrCreateFolder(ctx context.Context, folderStore file.FolderStore, folderPath string) (*file.Folder, error) {
|
func getOrCreateFolder(ctx context.Context, folderStore models.FolderFinderCreator, folderPath string) (*models.Folder, error) {
|
||||||
f, err := folderStore.FindByPath(ctx, folderPath)
|
f, err := folderStore.FindByPath(ctx, folderPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting folder by path: %w", err)
|
return nil, fmt.Errorf("getting folder by path: %w", err)
|
||||||
|
|
@ -231,7 +230,7 @@ func getOrCreateFolder(ctx context.Context, folderStore file.FolderStore, folder
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var parentID file.FolderID
|
var parentID models.FolderID
|
||||||
dir := filepath.Dir(folderPath)
|
dir := filepath.Dir(folderPath)
|
||||||
if dir != "." {
|
if dir != "." {
|
||||||
parent, err := getOrCreateFolder(ctx, folderStore, dir)
|
parent, err := getOrCreateFolder(ctx, folderStore, dir)
|
||||||
|
|
@ -242,7 +241,7 @@ func getOrCreateFolder(ctx context.Context, folderStore file.FolderStore, folder
|
||||||
parentID = parent.ID
|
parentID = parent.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
f = &file.Folder{
|
f = &models.Folder{
|
||||||
Path: folderPath,
|
Path: folderPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,8 +256,8 @@ func getOrCreateFolder(ctx context.Context, folderStore file.FolderStore, folder
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createScene(ctx context.Context, sqb models.SceneWriter, s *models.Scene, f *file.VideoFile) error {
|
func createScene(ctx context.Context, sqb models.SceneWriter, s *models.Scene, f *models.VideoFile) error {
|
||||||
err := sqb.Create(ctx, s, []file.ID{f.ID})
|
err := sqb.Create(ctx, s, []models.FileID{f.ID})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to create scene with path '%s': %s", f.Path, err.Error())
|
return fmt.Errorf("Failed to create scene with path '%s': %s", f.Path, err.Error())
|
||||||
|
|
@ -267,12 +266,12 @@ func createScene(ctx context.Context, sqb models.SceneWriter, s *models.Scene, f
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createImages(ctx context.Context, w models.ImageReaderWriter, folderStore file.FolderStore, fileStore file.Store) error {
|
func createImages(ctx context.Context, w models.ImageReaderWriter, folderStore models.FolderFinderCreator, fileCreator models.FileCreator) error {
|
||||||
// create the images
|
// create the images
|
||||||
imagePatterns, falseImagePatterns := generateTestPaths(testName, imageExt)
|
imagePatterns, falseImagePatterns := generateTestPaths(testName, imageExt)
|
||||||
|
|
||||||
for _, fn := range imagePatterns {
|
for _, fn := range imagePatterns {
|
||||||
f, err := createImageFile(ctx, fn, folderStore, fileStore)
|
f, err := createImageFile(ctx, fn, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +282,7 @@ func createImages(ctx context.Context, w models.ImageReaderWriter, folderStore f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, fn := range falseImagePatterns {
|
for _, fn := range falseImagePatterns {
|
||||||
f, err := createImageFile(ctx, fn, folderStore, fileStore)
|
f, err := createImageFile(ctx, fn, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -296,7 +295,7 @@ func createImages(ctx context.Context, w models.ImageReaderWriter, folderStore f
|
||||||
|
|
||||||
// add organized images
|
// add organized images
|
||||||
for _, fn := range imagePatterns {
|
for _, fn := range imagePatterns {
|
||||||
f, err := createImageFile(ctx, "organized"+fn, folderStore, fileStore)
|
f, err := createImageFile(ctx, "organized"+fn, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +309,7 @@ func createImages(ctx context.Context, w models.ImageReaderWriter, folderStore f
|
||||||
}
|
}
|
||||||
|
|
||||||
// create image with existing studio io
|
// create image with existing studio io
|
||||||
f, err := createImageFile(ctx, existingStudioImageName, folderStore, fileStore)
|
f, err := createImageFile(ctx, existingStudioImageName, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -326,7 +325,7 @@ func createImages(ctx context.Context, w models.ImageReaderWriter, folderStore f
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createImageFile(ctx context.Context, name string, folderStore file.FolderStore, fileStore file.Store) (*file.ImageFile, error) {
|
func createImageFile(ctx context.Context, name string, folderStore models.FolderFinderCreator, fileCreator models.FileCreator) (*models.ImageFile, error) {
|
||||||
folderPath := filepath.Dir(name)
|
folderPath := filepath.Dir(name)
|
||||||
basename := filepath.Base(name)
|
basename := filepath.Base(name)
|
||||||
|
|
||||||
|
|
@ -337,14 +336,14 @@ func createImageFile(ctx context.Context, name string, folderStore file.FolderSt
|
||||||
|
|
||||||
folderID := folder.ID
|
folderID := folder.ID
|
||||||
|
|
||||||
f := &file.ImageFile{
|
f := &models.ImageFile{
|
||||||
BaseFile: &file.BaseFile{
|
BaseFile: &models.BaseFile{
|
||||||
Basename: basename,
|
Basename: basename,
|
||||||
ParentFolderID: folderID,
|
ParentFolderID: folderID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fileStore.Create(ctx, f); err != nil {
|
if err := fileCreator.Create(ctx, f); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -362,10 +361,10 @@ func makeImage(expectedResult bool) *models.Image {
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
func createImage(ctx context.Context, w models.ImageWriter, o *models.Image, f *file.ImageFile) error {
|
func createImage(ctx context.Context, w models.ImageWriter, o *models.Image, f *models.ImageFile) error {
|
||||||
err := w.Create(ctx, &models.ImageCreateInput{
|
err := w.Create(ctx, &models.ImageCreateInput{
|
||||||
Image: o,
|
Image: o,
|
||||||
FileIDs: []file.ID{f.ID},
|
FileIDs: []models.FileID{f.ID},
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -375,12 +374,12 @@ func createImage(ctx context.Context, w models.ImageWriter, o *models.Image, f *
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createGalleries(ctx context.Context, w models.GalleryReaderWriter, folderStore file.FolderStore, fileStore file.Store) error {
|
func createGalleries(ctx context.Context, w models.GalleryReaderWriter, folderStore models.FolderFinderCreator, fileCreator models.FileCreator) error {
|
||||||
// create the galleries
|
// create the galleries
|
||||||
galleryPatterns, falseGalleryPatterns := generateTestPaths(testName, galleryExt)
|
galleryPatterns, falseGalleryPatterns := generateTestPaths(testName, galleryExt)
|
||||||
|
|
||||||
for _, fn := range galleryPatterns {
|
for _, fn := range galleryPatterns {
|
||||||
f, err := createGalleryFile(ctx, fn, folderStore, fileStore)
|
f, err := createGalleryFile(ctx, fn, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -391,7 +390,7 @@ func createGalleries(ctx context.Context, w models.GalleryReaderWriter, folderSt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, fn := range falseGalleryPatterns {
|
for _, fn := range falseGalleryPatterns {
|
||||||
f, err := createGalleryFile(ctx, fn, folderStore, fileStore)
|
f, err := createGalleryFile(ctx, fn, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -404,7 +403,7 @@ func createGalleries(ctx context.Context, w models.GalleryReaderWriter, folderSt
|
||||||
|
|
||||||
// add organized galleries
|
// add organized galleries
|
||||||
for _, fn := range galleryPatterns {
|
for _, fn := range galleryPatterns {
|
||||||
f, err := createGalleryFile(ctx, "organized"+fn, folderStore, fileStore)
|
f, err := createGalleryFile(ctx, "organized"+fn, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -418,7 +417,7 @@ func createGalleries(ctx context.Context, w models.GalleryReaderWriter, folderSt
|
||||||
}
|
}
|
||||||
|
|
||||||
// create gallery with existing studio io
|
// create gallery with existing studio io
|
||||||
f, err := createGalleryFile(ctx, existingStudioGalleryName, folderStore, fileStore)
|
f, err := createGalleryFile(ctx, existingStudioGalleryName, folderStore, fileCreator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -434,7 +433,7 @@ func createGalleries(ctx context.Context, w models.GalleryReaderWriter, folderSt
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createGalleryFile(ctx context.Context, name string, folderStore file.FolderStore, fileStore file.Store) (*file.BaseFile, error) {
|
func createGalleryFile(ctx context.Context, name string, folderStore models.FolderFinderCreator, fileCreator models.FileCreator) (*models.BaseFile, error) {
|
||||||
folderPath := filepath.Dir(name)
|
folderPath := filepath.Dir(name)
|
||||||
basename := filepath.Base(name)
|
basename := filepath.Base(name)
|
||||||
|
|
||||||
|
|
@ -445,12 +444,12 @@ func createGalleryFile(ctx context.Context, name string, folderStore file.Folder
|
||||||
|
|
||||||
folderID := folder.ID
|
folderID := folder.ID
|
||||||
|
|
||||||
f := &file.BaseFile{
|
f := &models.BaseFile{
|
||||||
Basename: basename,
|
Basename: basename,
|
||||||
ParentFolderID: folderID,
|
ParentFolderID: folderID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := fileStore.Create(ctx, f); err != nil {
|
if err := fileCreator.Create(ctx, f); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -468,8 +467,8 @@ func makeGallery(expectedResult bool) *models.Gallery {
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
func createGallery(ctx context.Context, w models.GalleryWriter, o *models.Gallery, f *file.BaseFile) error {
|
func createGallery(ctx context.Context, w models.GalleryWriter, o *models.Gallery, f *models.BaseFile) error {
|
||||||
err := w.Create(ctx, o, []file.ID{f.ID})
|
err := w.Create(ctx, o, []models.FileID{f.ID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to create gallery with path '%s': %s", f.Path, err.Error())
|
return fmt.Errorf("Failed to create gallery with path '%s': %s", f.Path, err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type SceneQueryPerformerUpdater interface {
|
type SceneQueryPerformerUpdater interface {
|
||||||
scene.Queryer
|
models.SceneQueryer
|
||||||
models.PerformerIDLoader
|
models.PerformerIDLoader
|
||||||
scene.PartialUpdater
|
models.SceneUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageQueryPerformerUpdater interface {
|
type ImageQueryPerformerUpdater interface {
|
||||||
image.Queryer
|
models.ImageQueryer
|
||||||
models.PerformerIDLoader
|
models.PerformerIDLoader
|
||||||
image.PartialUpdater
|
models.ImageUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
type GalleryQueryPerformerUpdater interface {
|
type GalleryQueryPerformerUpdater interface {
|
||||||
gallery.Queryer
|
models.GalleryQueryer
|
||||||
models.PerformerIDLoader
|
models.PerformerIDLoader
|
||||||
gallery.PartialUpdater
|
models.GalleryUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPerformerTaggers(p *models.Performer, cache *match.Cache) []tagger {
|
func getPerformerTaggers(p *models.Performer, cache *match.Cache) []tagger {
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,19 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SceneFinderUpdater interface {
|
||||||
|
models.SceneQueryer
|
||||||
|
models.SceneUpdater
|
||||||
|
}
|
||||||
|
|
||||||
type ScenePerformerUpdater interface {
|
type ScenePerformerUpdater interface {
|
||||||
models.PerformerIDLoader
|
models.PerformerIDLoader
|
||||||
scene.PartialUpdater
|
models.SceneUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
type SceneTagUpdater interface {
|
type SceneTagUpdater interface {
|
||||||
models.TagIDLoader
|
models.TagIDLoader
|
||||||
scene.PartialUpdater
|
models.SceneUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSceneFileTagger(s *models.Scene, cache *match.Cache) tagger {
|
func getSceneFileTagger(s *models.Scene, cache *match.Cache) tagger {
|
||||||
|
|
@ -30,7 +35,7 @@ func getSceneFileTagger(s *models.Scene, cache *match.Cache) tagger {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScenePerformers tags the provided scene with performers whose name matches the scene's path.
|
// ScenePerformers tags the provided scene with performers whose name matches the scene's path.
|
||||||
func ScenePerformers(ctx context.Context, s *models.Scene, rw ScenePerformerUpdater, performerReader match.PerformerAutoTagQueryer, cache *match.Cache) error {
|
func ScenePerformers(ctx context.Context, s *models.Scene, rw ScenePerformerUpdater, performerReader models.PerformerAutoTagQueryer, cache *match.Cache) error {
|
||||||
t := getSceneFileTagger(s, cache)
|
t := getSceneFileTagger(s, cache)
|
||||||
|
|
||||||
return t.tagPerformers(ctx, performerReader, func(subjectID, otherID int) (bool, error) {
|
return t.tagPerformers(ctx, performerReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
|
@ -54,7 +59,7 @@ func ScenePerformers(ctx context.Context, s *models.Scene, rw ScenePerformerUpda
|
||||||
// SceneStudios tags the provided scene with the first studio whose name matches the scene's path.
|
// SceneStudios tags the provided scene with the first studio whose name matches the scene's path.
|
||||||
//
|
//
|
||||||
// Scenes will not be tagged if studio is already set.
|
// Scenes will not be tagged if studio is already set.
|
||||||
func SceneStudios(ctx context.Context, s *models.Scene, rw SceneFinderUpdater, studioReader match.StudioAutoTagQueryer, cache *match.Cache) error {
|
func SceneStudios(ctx context.Context, s *models.Scene, rw SceneFinderUpdater, studioReader models.StudioAutoTagQueryer, cache *match.Cache) error {
|
||||||
if s.StudioID != nil {
|
if s.StudioID != nil {
|
||||||
// don't modify
|
// don't modify
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -68,7 +73,7 @@ func SceneStudios(ctx context.Context, s *models.Scene, rw SceneFinderUpdater, s
|
||||||
}
|
}
|
||||||
|
|
||||||
// SceneTags tags the provided scene with tags whose name matches the scene's path.
|
// SceneTags tags the provided scene with tags whose name matches the scene's path.
|
||||||
func SceneTags(ctx context.Context, s *models.Scene, rw SceneTagUpdater, tagReader match.TagAutoTagQueryer, cache *match.Cache) error {
|
func SceneTags(ctx context.Context, s *models.Scene, rw SceneTagUpdater, tagReader models.TagAutoTagQueryer, cache *match.Cache) error {
|
||||||
t := getSceneFileTagger(s, cache)
|
t := getSceneFileTagger(s, cache)
|
||||||
|
|
||||||
return t.tagTags(ctx, tagReader, func(subjectID, otherID int) (bool, error) {
|
return t.tagTags(ctx, tagReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,15 @@ package autotag
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/gallery"
|
|
||||||
"github.com/stashapp/stash/pkg/image"
|
|
||||||
"github.com/stashapp/stash/pkg/match"
|
"github.com/stashapp/stash/pkg/match"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
)
|
)
|
||||||
|
|
||||||
// the following functions aren't used in Tagger because they assume
|
// the following functions aren't used in Tagger because they assume
|
||||||
// use within a transaction
|
// use within a transaction
|
||||||
|
|
||||||
func addSceneStudio(ctx context.Context, sceneWriter scene.PartialUpdater, o *models.Scene, studioID int) (bool, error) {
|
func addSceneStudio(ctx context.Context, sceneWriter models.SceneUpdater, o *models.Scene, studioID int) (bool, error) {
|
||||||
// don't set if already set
|
// don't set if already set
|
||||||
if o.StudioID != nil {
|
if o.StudioID != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
@ -31,7 +28,7 @@ func addSceneStudio(ctx context.Context, sceneWriter scene.PartialUpdater, o *mo
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addImageStudio(ctx context.Context, imageWriter image.PartialUpdater, i *models.Image, studioID int) (bool, error) {
|
func addImageStudio(ctx context.Context, imageWriter models.ImageUpdater, i *models.Image, studioID int) (bool, error) {
|
||||||
// don't set if already set
|
// don't set if already set
|
||||||
if i.StudioID != nil {
|
if i.StudioID != nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
|
@ -84,11 +81,6 @@ func getStudioTagger(p *models.Studio, aliases []string, cache *match.Cache) []t
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
type SceneFinderUpdater interface {
|
|
||||||
scene.Queryer
|
|
||||||
scene.PartialUpdater
|
|
||||||
}
|
|
||||||
|
|
||||||
// StudioScenes searches for scenes whose path matches the provided studio name and tags the scene with the studio, if studio is not already set on the scene.
|
// StudioScenes searches for scenes whose path matches the provided studio name and tags the scene with the studio, if studio is not already set on the scene.
|
||||||
func (tagger *Tagger) StudioScenes(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw SceneFinderUpdater) error {
|
func (tagger *Tagger) StudioScenes(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw SceneFinderUpdater) error {
|
||||||
t := getStudioTagger(p, aliases, tagger.Cache)
|
t := getStudioTagger(p, aliases, tagger.Cache)
|
||||||
|
|
@ -120,12 +112,6 @@ func (tagger *Tagger) StudioScenes(ctx context.Context, p *models.Studio, paths
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageFinderUpdater interface {
|
|
||||||
image.Queryer
|
|
||||||
Find(ctx context.Context, id int) (*models.Image, error)
|
|
||||||
UpdatePartial(ctx context.Context, id int, partial models.ImagePartial) (*models.Image, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StudioImages searches for images whose path matches the provided studio name and tags the image with the studio, if studio is not already set on the image.
|
// StudioImages searches for images whose path matches the provided studio name and tags the image with the studio, if studio is not already set on the image.
|
||||||
func (tagger *Tagger) StudioImages(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw ImageFinderUpdater) error {
|
func (tagger *Tagger) StudioImages(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw ImageFinderUpdater) error {
|
||||||
t := getStudioTagger(p, aliases, tagger.Cache)
|
t := getStudioTagger(p, aliases, tagger.Cache)
|
||||||
|
|
@ -157,12 +143,6 @@ func (tagger *Tagger) StudioImages(ctx context.Context, p *models.Studio, paths
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type GalleryFinderUpdater interface {
|
|
||||||
gallery.Queryer
|
|
||||||
gallery.PartialUpdater
|
|
||||||
Find(ctx context.Context, id int) (*models.Gallery, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StudioGalleries searches for galleries whose path matches the provided studio name and tags the gallery with the studio, if studio is not already set on the gallery.
|
// StudioGalleries searches for galleries whose path matches the provided studio name and tags the gallery with the studio, if studio is not already set on the gallery.
|
||||||
func (tagger *Tagger) StudioGalleries(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw GalleryFinderUpdater) error {
|
func (tagger *Tagger) StudioGalleries(ctx context.Context, p *models.Studio, paths []string, aliases []string, rw GalleryFinderUpdater) error {
|
||||||
t := getStudioTagger(p, aliases, tagger.Cache)
|
t := getStudioTagger(p, aliases, tagger.Cache)
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type SceneQueryTagUpdater interface {
|
type SceneQueryTagUpdater interface {
|
||||||
scene.Queryer
|
models.SceneQueryer
|
||||||
models.TagIDLoader
|
models.TagIDLoader
|
||||||
scene.PartialUpdater
|
models.SceneUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageQueryTagUpdater interface {
|
type ImageQueryTagUpdater interface {
|
||||||
image.Queryer
|
models.ImageQueryer
|
||||||
models.TagIDLoader
|
models.TagIDLoader
|
||||||
image.PartialUpdater
|
models.ImageUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
type GalleryQueryTagUpdater interface {
|
type GalleryQueryTagUpdater interface {
|
||||||
gallery.Queryer
|
models.GalleryQueryer
|
||||||
models.TagIDLoader
|
models.TagIDLoader
|
||||||
gallery.PartialUpdater
|
models.GalleryUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTagTaggers(p *models.Tag, aliases []string, cache *match.Cache) []tagger {
|
func getTagTaggers(p *models.Tag, aliases []string, cache *match.Cache) []tagger {
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/gallery"
|
|
||||||
"github.com/stashapp/stash/pkg/image"
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/match"
|
"github.com/stashapp/stash/pkg/match"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -54,7 +51,7 @@ func (t *tagger) addLog(otherType, otherName string) {
|
||||||
logger.Infof("Added %s '%s' to %s '%s'", otherType, otherName, t.Type, t.Name)
|
logger.Infof("Added %s '%s' to %s '%s'", otherType, otherName, t.Type, t.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tagger) tagPerformers(ctx context.Context, performerReader match.PerformerAutoTagQueryer, addFunc addLinkFunc) error {
|
func (t *tagger) tagPerformers(ctx context.Context, performerReader models.PerformerAutoTagQueryer, addFunc addLinkFunc) error {
|
||||||
others, err := match.PathToPerformers(ctx, t.Path, performerReader, t.cache, t.trimExt)
|
others, err := match.PathToPerformers(ctx, t.Path, performerReader, t.cache, t.trimExt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -75,7 +72,7 @@ func (t *tagger) tagPerformers(ctx context.Context, performerReader match.Perfor
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tagger) tagStudios(ctx context.Context, studioReader match.StudioAutoTagQueryer, addFunc addLinkFunc) error {
|
func (t *tagger) tagStudios(ctx context.Context, studioReader models.StudioAutoTagQueryer, addFunc addLinkFunc) error {
|
||||||
studio, err := match.PathToStudio(ctx, t.Path, studioReader, t.cache, t.trimExt)
|
studio, err := match.PathToStudio(ctx, t.Path, studioReader, t.cache, t.trimExt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -96,7 +93,7 @@ func (t *tagger) tagStudios(ctx context.Context, studioReader match.StudioAutoTa
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tagger) tagTags(ctx context.Context, tagReader match.TagAutoTagQueryer, addFunc addLinkFunc) error {
|
func (t *tagger) tagTags(ctx context.Context, tagReader models.TagAutoTagQueryer, addFunc addLinkFunc) error {
|
||||||
others, err := match.PathToTags(ctx, t.Path, tagReader, t.cache, t.trimExt)
|
others, err := match.PathToTags(ctx, t.Path, tagReader, t.cache, t.trimExt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -117,7 +114,7 @@ func (t *tagger) tagTags(ctx context.Context, tagReader match.TagAutoTagQueryer,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tagger) tagScenes(ctx context.Context, paths []string, sceneReader scene.Queryer, addFunc addSceneLinkFunc) error {
|
func (t *tagger) tagScenes(ctx context.Context, paths []string, sceneReader models.SceneQueryer, addFunc addSceneLinkFunc) error {
|
||||||
return match.PathToScenesFn(ctx, t.Name, paths, sceneReader, func(ctx context.Context, p *models.Scene) error {
|
return match.PathToScenesFn(ctx, t.Name, paths, sceneReader, func(ctx context.Context, p *models.Scene) error {
|
||||||
added, err := addFunc(p)
|
added, err := addFunc(p)
|
||||||
|
|
||||||
|
|
@ -133,7 +130,7 @@ func (t *tagger) tagScenes(ctx context.Context, paths []string, sceneReader scen
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tagger) tagImages(ctx context.Context, paths []string, imageReader image.Queryer, addFunc addImageLinkFunc) error {
|
func (t *tagger) tagImages(ctx context.Context, paths []string, imageReader models.ImageQueryer, addFunc addImageLinkFunc) error {
|
||||||
return match.PathToImagesFn(ctx, t.Name, paths, imageReader, func(ctx context.Context, p *models.Image) error {
|
return match.PathToImagesFn(ctx, t.Name, paths, imageReader, func(ctx context.Context, p *models.Image) error {
|
||||||
added, err := addFunc(p)
|
added, err := addFunc(p)
|
||||||
|
|
||||||
|
|
@ -149,7 +146,7 @@ func (t *tagger) tagImages(ctx context.Context, paths []string, imageReader imag
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tagger) tagGalleries(ctx context.Context, paths []string, galleryReader gallery.Queryer, addFunc addGalleryLinkFunc) error {
|
func (t *tagger) tagGalleries(ctx context.Context, paths []string, galleryReader models.GalleryQueryer, addFunc addGalleryLinkFunc) error {
|
||||||
return match.PathToGalleriesFn(ctx, t.Name, paths, galleryReader, func(ctx context.Context, p *models.Gallery) error {
|
return match.PathToGalleriesFn(ctx, t.Name, paths, galleryReader, func(ctx context.Context, p *models.Gallery) error {
|
||||||
added, err := addFunc(p)
|
added, err := addFunc(p)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -363,7 +363,7 @@ func (me *contentDirectoryService) handleBrowseMetadata(obj object, host string)
|
||||||
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
if err := txn.WithReadTxn(context.TODO(), me.txnManager, func(ctx context.Context) error {
|
||||||
scene, err = me.repository.SceneFinder.Find(ctx, sceneID)
|
scene, err = me.repository.SceneFinder.Find(ctx, sceneID)
|
||||||
if scene != nil {
|
if scene != nil {
|
||||||
err = scene.LoadPrimaryFile(ctx, me.repository.FileFinder)
|
err = scene.LoadPrimaryFile(ctx, me.repository.FileGetter)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -478,7 +478,7 @@ func (me *contentDirectoryService) getVideos(sceneFilter *models.SceneFilterType
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, s := range scenes {
|
for _, s := range scenes {
|
||||||
if err := s.LoadPrimaryFile(ctx, me.repository.FileFinder); err != nil {
|
if err := s.LoadPrimaryFile(ctx, me.repository.FileGetter); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -506,7 +506,7 @@ func (me *contentDirectoryService) getPageVideos(sceneFilter *models.SceneFilter
|
||||||
sort := me.VideoSortOrder
|
sort := me.VideoSortOrder
|
||||||
direction := getSortDirection(sceneFilter, sort)
|
direction := getSortDirection(sceneFilter, sort)
|
||||||
var err error
|
var err error
|
||||||
objs, err = pager.getPageVideos(ctx, me.repository.SceneFinder, me.repository.FileFinder, page, host, sort, direction)
|
objs, err = pager.getPageVideos(ctx, me.repository.SceneFinder, me.repository.FileGetter, page, host, sort, direction)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,13 +48,12 @@ import (
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SceneFinder interface {
|
type SceneFinder interface {
|
||||||
scene.Queryer
|
models.SceneGetter
|
||||||
scene.IDFinder
|
models.SceneQueryer
|
||||||
}
|
}
|
||||||
|
|
||||||
type StudioFinder interface {
|
type StudioFinder interface {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
"github.com/stashapp/stash/pkg/scene"
|
||||||
)
|
)
|
||||||
|
|
@ -20,7 +19,7 @@ func (p *scenePager) getPageID(page int) string {
|
||||||
return p.parentID + "/page/" + strconv.Itoa(page)
|
return p.parentID + "/page/" + strconv.Itoa(page)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *scenePager) getPages(ctx context.Context, r scene.Queryer, total int) ([]interface{}, error) {
|
func (p *scenePager) getPages(ctx context.Context, r models.SceneQueryer, total int) ([]interface{}, error) {
|
||||||
var objs []interface{}
|
var objs []interface{}
|
||||||
|
|
||||||
// get the first scene of each page to set an appropriate title
|
// get the first scene of each page to set an appropriate title
|
||||||
|
|
@ -60,7 +59,7 @@ func (p *scenePager) getPages(ctx context.Context, r scene.Queryer, total int) (
|
||||||
return objs, nil
|
return objs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *scenePager) getPageVideos(ctx context.Context, r SceneFinder, f file.Finder, page int, host string, sort string, direction models.SortDirectionEnum) ([]interface{}, error) {
|
func (p *scenePager) getPageVideos(ctx context.Context, r SceneFinder, f models.FileGetter, page int, host string, sort string, direction models.SortDirectionEnum) ([]interface{}, error) {
|
||||||
var objs []interface{}
|
var objs []interface{}
|
||||||
|
|
||||||
findFilter := &models.FindFilterType{
|
findFilter := &models.FindFilterType{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
|
|
@ -16,7 +15,7 @@ import (
|
||||||
|
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
SceneFinder SceneFinder
|
SceneFinder SceneFinder
|
||||||
FileFinder file.Finder
|
FileGetter models.FileGetter
|
||||||
StudioFinder StudioFinder
|
StudioFinder StudioFinder
|
||||||
TagFinder TagFinder
|
TagFinder TagFinder
|
||||||
PerformerFinder PerformerFinder
|
PerformerFinder PerformerFinder
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ type SceneIdentifier struct {
|
||||||
SceneReaderUpdater SceneReaderUpdater
|
SceneReaderUpdater SceneReaderUpdater
|
||||||
StudioReaderWriter models.StudioReaderWriter
|
StudioReaderWriter models.StudioReaderWriter
|
||||||
PerformerCreator PerformerCreator
|
PerformerCreator PerformerCreator
|
||||||
TagCreatorFinder TagCreatorFinder
|
TagFinderCreator models.TagFinderCreator
|
||||||
|
|
||||||
DefaultOptions *MetadataOptions
|
DefaultOptions *MetadataOptions
|
||||||
Sources []ScraperSource
|
Sources []ScraperSource
|
||||||
|
|
@ -176,7 +176,7 @@ func (t *SceneIdentifier) getSceneUpdater(ctx context.Context, s *models.Scene,
|
||||||
sceneReader: t.SceneReaderUpdater,
|
sceneReader: t.SceneReaderUpdater,
|
||||||
studioReaderWriter: t.StudioReaderWriter,
|
studioReaderWriter: t.StudioReaderWriter,
|
||||||
performerCreator: t.PerformerCreator,
|
performerCreator: t.PerformerCreator,
|
||||||
tagCreatorFinder: t.TagCreatorFinder,
|
tagCreator: t.TagFinderCreator,
|
||||||
scene: s,
|
scene: s,
|
||||||
result: result,
|
result: result,
|
||||||
fieldOptions: fieldOptions,
|
fieldOptions: fieldOptions,
|
||||||
|
|
@ -332,7 +332,7 @@ func (t *SceneIdentifier) addTagToScene(ctx context.Context, txnManager txn.Mana
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ret, err := t.TagCreatorFinder.Find(ctx, tagID)
|
ret, err := t.TagFinderCreator.Find(ctx, tagID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Infof("Added tag id %s to skipped scene %s", tagToAdd, s.Path)
|
logger.Infof("Added tag id %s to skipped scene %s", tagToAdd, s.Path)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ func TestSceneIdentifier_Identify(t *testing.T) {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
identifier := SceneIdentifier{
|
identifier := SceneIdentifier{
|
||||||
SceneReaderUpdater: mockSceneReaderWriter,
|
SceneReaderUpdater: mockSceneReaderWriter,
|
||||||
TagCreatorFinder: mockTagFinderCreator,
|
TagFinderCreator: mockTagFinderCreator,
|
||||||
DefaultOptions: defaultOptions,
|
DefaultOptions: defaultOptions,
|
||||||
Sources: sources,
|
Sources: sources,
|
||||||
SceneUpdatePostHookExecutor: mockHookExecutor{},
|
SceneUpdatePostHookExecutor: mockHookExecutor{},
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type PerformerCreator interface {
|
type PerformerCreator interface {
|
||||||
Create(ctx context.Context, newPerformer *models.Performer) error
|
models.PerformerCreator
|
||||||
UpdateImage(ctx context.Context, performerID int, image []byte) error
|
UpdateImage(ctx context.Context, performerID int, image []byte) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,32 +11,29 @@ import (
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
|
||||||
"github.com/stashapp/stash/pkg/sliceutil"
|
"github.com/stashapp/stash/pkg/sliceutil"
|
||||||
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
||||||
"github.com/stashapp/stash/pkg/tag"
|
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SceneReaderUpdater interface {
|
type SceneCoverGetter interface {
|
||||||
GetCover(ctx context.Context, sceneID int) ([]byte, error)
|
GetCover(ctx context.Context, sceneID int) ([]byte, error)
|
||||||
scene.Updater
|
}
|
||||||
|
|
||||||
|
type SceneReaderUpdater interface {
|
||||||
|
SceneCoverGetter
|
||||||
|
models.SceneUpdater
|
||||||
models.PerformerIDLoader
|
models.PerformerIDLoader
|
||||||
models.TagIDLoader
|
models.TagIDLoader
|
||||||
models.StashIDLoader
|
models.StashIDLoader
|
||||||
models.URLLoader
|
models.URLLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
type TagCreatorFinder interface {
|
|
||||||
Create(ctx context.Context, newTag *models.Tag) error
|
|
||||||
tag.Finder
|
|
||||||
}
|
|
||||||
|
|
||||||
type sceneRelationships struct {
|
type sceneRelationships struct {
|
||||||
sceneReader SceneReaderUpdater
|
sceneReader SceneCoverGetter
|
||||||
studioReaderWriter models.StudioReaderWriter
|
studioReaderWriter models.StudioReaderWriter
|
||||||
performerCreator PerformerCreator
|
performerCreator PerformerCreator
|
||||||
tagCreatorFinder TagCreatorFinder
|
tagCreator models.TagCreator
|
||||||
scene *models.Scene
|
scene *models.Scene
|
||||||
result *scrapeResult
|
result *scrapeResult
|
||||||
fieldOptions map[string]*FieldOptions
|
fieldOptions map[string]*FieldOptions
|
||||||
|
|
@ -173,7 +170,7 @@ func (g sceneRelationships) tags(ctx context.Context) ([]int, error) {
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
err := g.tagCreatorFinder.Create(ctx, &newTag)
|
err := g.tagCreator.Create(ctx, &newTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating tag: %w", err)
|
return nil, fmt.Errorf("error creating tag: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -378,7 +378,7 @@ func Test_sceneRelationships_tags(t *testing.T) {
|
||||||
|
|
||||||
tr := sceneRelationships{
|
tr := sceneRelationships{
|
||||||
sceneReader: mockSceneReaderWriter,
|
sceneReader: mockSceneReaderWriter,
|
||||||
tagCreatorFinder: mockTagReaderWriter,
|
tagCreator: mockTagReaderWriter,
|
||||||
fieldOptions: make(map[string]*FieldOptions),
|
fieldOptions: make(map[string]*FieldOptions),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/hash/md5"
|
"github.com/stashapp/stash/pkg/hash/md5"
|
||||||
"github.com/stashapp/stash/pkg/hash/oshash"
|
"github.com/stashapp/stash/pkg/hash/oshash"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fingerprintCalculator struct {
|
type fingerprintCalculator struct {
|
||||||
Config *config.Instance
|
Config *config.Instance
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fingerprintCalculator) calculateOshash(f *file.BaseFile, o file.Opener) (*file.Fingerprint, error) {
|
func (c *fingerprintCalculator) calculateOshash(f *models.BaseFile, o file.Opener) (*models.Fingerprint, error) {
|
||||||
r, err := o.Open()
|
r, err := o.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("opening file: %w", err)
|
return nil, fmt.Errorf("opening file: %w", err)
|
||||||
|
|
@ -34,13 +35,13 @@ func (c *fingerprintCalculator) calculateOshash(f *file.BaseFile, o file.Opener)
|
||||||
return nil, fmt.Errorf("calculating oshash: %w", err)
|
return nil, fmt.Errorf("calculating oshash: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &file.Fingerprint{
|
return &models.Fingerprint{
|
||||||
Type: file.FingerprintTypeOshash,
|
Type: models.FingerprintTypeOshash,
|
||||||
Fingerprint: hash,
|
Fingerprint: hash,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fingerprintCalculator) calculateMD5(o file.Opener) (*file.Fingerprint, error) {
|
func (c *fingerprintCalculator) calculateMD5(o file.Opener) (*models.Fingerprint, error) {
|
||||||
r, err := o.Open()
|
r, err := o.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("opening file: %w", err)
|
return nil, fmt.Errorf("opening file: %w", err)
|
||||||
|
|
@ -53,24 +54,24 @@ func (c *fingerprintCalculator) calculateMD5(o file.Opener) (*file.Fingerprint,
|
||||||
return nil, fmt.Errorf("calculating md5: %w", err)
|
return nil, fmt.Errorf("calculating md5: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &file.Fingerprint{
|
return &models.Fingerprint{
|
||||||
Type: file.FingerprintTypeMD5,
|
Type: models.FingerprintTypeMD5,
|
||||||
Fingerprint: hash,
|
Fingerprint: hash,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *fingerprintCalculator) CalculateFingerprints(f *file.BaseFile, o file.Opener, useExisting bool) ([]file.Fingerprint, error) {
|
func (c *fingerprintCalculator) CalculateFingerprints(f *models.BaseFile, o file.Opener, useExisting bool) ([]models.Fingerprint, error) {
|
||||||
var ret []file.Fingerprint
|
var ret []models.Fingerprint
|
||||||
calculateMD5 := true
|
calculateMD5 := true
|
||||||
|
|
||||||
if useAsVideo(f.Path) {
|
if useAsVideo(f.Path) {
|
||||||
var (
|
var (
|
||||||
fp *file.Fingerprint
|
fp *models.Fingerprint
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if useExisting {
|
if useExisting {
|
||||||
fp = f.Fingerprints.For(file.FingerprintTypeOshash)
|
fp = f.Fingerprints.For(models.FingerprintTypeOshash)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fp == nil {
|
if fp == nil {
|
||||||
|
|
@ -89,12 +90,12 @@ func (c *fingerprintCalculator) CalculateFingerprints(f *file.BaseFile, o file.O
|
||||||
|
|
||||||
if calculateMD5 {
|
if calculateMD5 {
|
||||||
var (
|
var (
|
||||||
fp *file.Fingerprint
|
fp *models.Fingerprint
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if useExisting {
|
if useExisting {
|
||||||
fp = f.Fingerprints.For(file.FingerprintTypeMD5)
|
fp = f.Fingerprints.For(models.FingerprintTypeMD5)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fp == nil {
|
if fp == nil {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/image"
|
"github.com/stashapp/stash/pkg/image"
|
||||||
"github.com/stashapp/stash/pkg/job"
|
"github.com/stashapp/stash/pkg/job"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/paths"
|
"github.com/stashapp/stash/pkg/models/paths"
|
||||||
"github.com/stashapp/stash/pkg/plugin"
|
"github.com/stashapp/stash/pkg/plugin"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
"github.com/stashapp/stash/pkg/scene"
|
||||||
|
|
@ -222,7 +223,7 @@ func initialize() error {
|
||||||
|
|
||||||
instance.DLNAService = dlna.NewService(instance.Repository, dlna.Repository{
|
instance.DLNAService = dlna.NewService(instance.Repository, dlna.Repository{
|
||||||
SceneFinder: instance.Repository.Scene,
|
SceneFinder: instance.Repository.Scene,
|
||||||
FileFinder: instance.Repository.File,
|
FileGetter: instance.Repository.File,
|
||||||
StudioFinder: instance.Repository.Studio,
|
StudioFinder: instance.Repository.Studio,
|
||||||
TagFinder: instance.Repository.Tag,
|
TagFinder: instance.Repository.Tag,
|
||||||
PerformerFinder: instance.Repository.Performer,
|
PerformerFinder: instance.Repository.Performer,
|
||||||
|
|
@ -280,15 +281,15 @@ func initialize() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func videoFileFilter(ctx context.Context, f file.File) bool {
|
func videoFileFilter(ctx context.Context, f models.File) bool {
|
||||||
return useAsVideo(f.Base().Path)
|
return useAsVideo(f.Base().Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageFileFilter(ctx context.Context, f file.File) bool {
|
func imageFileFilter(ctx context.Context, f models.File) bool {
|
||||||
return useAsImage(f.Base().Path)
|
return useAsImage(f.Base().Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func galleryFileFilter(ctx context.Context, f file.File) bool {
|
func galleryFileFilter(ctx context.Context, f models.File) bool {
|
||||||
return isZip(f.Base().Basename)
|
return isZip(f.Base().Basename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,7 +298,7 @@ func makeScanner(db *sqlite.Database, pluginCache *plugin.Cache) *file.Scanner {
|
||||||
Repository: file.Repository{
|
Repository: file.Repository{
|
||||||
Manager: db,
|
Manager: db,
|
||||||
DatabaseProvider: db,
|
DatabaseProvider: db,
|
||||||
Store: db.File,
|
FileStore: db.File,
|
||||||
FolderStore: db.Folder,
|
FolderStore: db.Folder,
|
||||||
},
|
},
|
||||||
FileDecorators: []file.Decorator{
|
FileDecorators: []file.Decorator{
|
||||||
|
|
@ -325,7 +326,7 @@ func makeCleaner(db *sqlite.Database, pluginCache *plugin.Cache) *file.Cleaner {
|
||||||
Repository: file.Repository{
|
Repository: file.Repository{
|
||||||
Manager: db,
|
Manager: db,
|
||||||
DatabaseProvider: db,
|
DatabaseProvider: db,
|
||||||
Store: db.File,
|
FileStore: db.File,
|
||||||
FolderStore: db.Folder,
|
FolderStore: db.Folder,
|
||||||
},
|
},
|
||||||
Handlers: []file.CleanHandler{
|
Handlers: []file.CleanHandler{
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ package manager
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/gallery"
|
|
||||||
"github.com/stashapp/stash/pkg/image"
|
"github.com/stashapp/stash/pkg/image"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
"github.com/stashapp/stash/pkg/scene"
|
||||||
|
|
@ -12,49 +10,17 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ImageReaderWriter interface {
|
|
||||||
models.ImageReaderWriter
|
|
||||||
image.FinderCreatorUpdater
|
|
||||||
GetManyFileIDs(ctx context.Context, ids []int) ([][]file.ID, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type GalleryReaderWriter interface {
|
|
||||||
models.GalleryReaderWriter
|
|
||||||
gallery.FinderCreatorUpdater
|
|
||||||
gallery.Finder
|
|
||||||
models.FileLoader
|
|
||||||
GetManyFileIDs(ctx context.Context, ids []int) ([][]file.ID, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type SceneReaderWriter interface {
|
|
||||||
models.SceneReaderWriter
|
|
||||||
scene.CreatorUpdater
|
|
||||||
models.URLLoader
|
|
||||||
GetManyFileIDs(ctx context.Context, ids []int) ([][]file.ID, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileReaderWriter interface {
|
|
||||||
file.Store
|
|
||||||
Query(ctx context.Context, options models.FileQueryOptions) (*models.FileQueryResult, error)
|
|
||||||
GetCaptions(ctx context.Context, fileID file.ID) ([]*models.VideoCaption, error)
|
|
||||||
IsPrimary(ctx context.Context, fileID file.ID) (bool, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FolderReaderWriter interface {
|
|
||||||
file.FolderStore
|
|
||||||
}
|
|
||||||
|
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
models.TxnManager
|
models.TxnManager
|
||||||
|
|
||||||
File FileReaderWriter
|
File models.FileReaderWriter
|
||||||
Folder FolderReaderWriter
|
Folder models.FolderReaderWriter
|
||||||
Gallery GalleryReaderWriter
|
Gallery models.GalleryReaderWriter
|
||||||
GalleryChapter models.GalleryChapterReaderWriter
|
GalleryChapter models.GalleryChapterReaderWriter
|
||||||
Image ImageReaderWriter
|
Image models.ImageReaderWriter
|
||||||
Movie models.MovieReaderWriter
|
Movie models.MovieReaderWriter
|
||||||
Performer models.PerformerReaderWriter
|
Performer models.PerformerReaderWriter
|
||||||
Scene SceneReaderWriter
|
Scene models.SceneReaderWriter
|
||||||
SceneMarker models.SceneMarkerReaderWriter
|
SceneMarker models.SceneMarkerReaderWriter
|
||||||
Studio models.StudioReaderWriter
|
Studio models.StudioReaderWriter
|
||||||
Tag models.TagReaderWriter
|
Tag models.TagReaderWriter
|
||||||
|
|
@ -94,15 +60,15 @@ func sqliteRepository(d *sqlite.Database) Repository {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SceneService interface {
|
type SceneService interface {
|
||||||
Create(ctx context.Context, input *models.Scene, fileIDs []file.ID, coverImage []byte) (*models.Scene, error)
|
Create(ctx context.Context, input *models.Scene, fileIDs []models.FileID, coverImage []byte) (*models.Scene, error)
|
||||||
AssignFile(ctx context.Context, sceneID int, fileID file.ID) error
|
AssignFile(ctx context.Context, sceneID int, fileID models.FileID) error
|
||||||
Merge(ctx context.Context, sourceIDs []int, destinationID int, values models.ScenePartial) error
|
Merge(ctx context.Context, sourceIDs []int, destinationID int, values models.ScenePartial) error
|
||||||
Destroy(ctx context.Context, scene *models.Scene, fileDeleter *scene.FileDeleter, deleteGenerated, deleteFile bool) error
|
Destroy(ctx context.Context, scene *models.Scene, fileDeleter *scene.FileDeleter, deleteGenerated, deleteFile bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageService interface {
|
type ImageService interface {
|
||||||
Destroy(ctx context.Context, image *models.Image, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) error
|
Destroy(ctx context.Context, image *models.Image, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) error
|
||||||
DestroyZipImages(ctx context.Context, zipFile file.File, fileDeleter *image.FileDeleter, deleteGenerated bool) ([]*models.Image, error)
|
DestroyZipImages(ctx context.Context, zipFile models.File, fileDeleter *image.FileDeleter, deleteGenerated bool) ([]*models.Image, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GalleryService interface {
|
type GalleryService interface {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/manager/config"
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
@ -57,7 +56,7 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetVideoFileContainer(file *file.VideoFile) (ffmpeg.Container, error) {
|
func GetVideoFileContainer(file *models.VideoFile) (ffmpeg.Container, error) {
|
||||||
var container ffmpeg.Container
|
var container ffmpeg.Container
|
||||||
format := file.Format
|
format := file.Format
|
||||||
if format != "" {
|
if format != "" {
|
||||||
|
|
@ -88,7 +87,7 @@ func GetSceneStreamPaths(scene *models.Scene, directStreamURL *url.URL, maxStrea
|
||||||
|
|
||||||
// convert StreamingResolutionEnum to ResolutionEnum
|
// convert StreamingResolutionEnum to ResolutionEnum
|
||||||
maxStreamingResolution := models.ResolutionEnum(maxStreamingTranscodeSize)
|
maxStreamingResolution := models.ResolutionEnum(maxStreamingTranscodeSize)
|
||||||
sceneResolution := file.GetMinResolution(pf)
|
sceneResolution := models.GetMinResolution(pf)
|
||||||
includeSceneStreamPath := func(streamingResolution models.StreamingResolutionEnum) bool {
|
includeSceneStreamPath := func(streamingResolution models.StreamingResolutionEnum) bool {
|
||||||
var minResolution int
|
var minResolution int
|
||||||
if streamingResolution == models.StreamingResolutionEnumOriginal {
|
if streamingResolution == models.StreamingResolutionEnumOriginal {
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@ type cleanHandler struct {
|
||||||
PluginCache *plugin.Cache
|
PluginCache *plugin.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *cleanHandler) HandleFile(ctx context.Context, fileDeleter *file.Deleter, fileID file.ID) error {
|
func (h *cleanHandler) HandleFile(ctx context.Context, fileDeleter *file.Deleter, fileID models.FileID) error {
|
||||||
if err := h.handleRelatedScenes(ctx, fileDeleter, fileID); err != nil {
|
if err := h.handleRelatedScenes(ctx, fileDeleter, fileID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -271,11 +271,11 @@ func (h *cleanHandler) HandleFile(ctx context.Context, fileDeleter *file.Deleter
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *cleanHandler) HandleFolder(ctx context.Context, fileDeleter *file.Deleter, folderID file.FolderID) error {
|
func (h *cleanHandler) HandleFolder(ctx context.Context, fileDeleter *file.Deleter, folderID models.FolderID) error {
|
||||||
return h.deleteRelatedFolderGalleries(ctx, folderID)
|
return h.deleteRelatedFolderGalleries(ctx, folderID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *cleanHandler) handleRelatedScenes(ctx context.Context, fileDeleter *file.Deleter, fileID file.ID) error {
|
func (h *cleanHandler) handleRelatedScenes(ctx context.Context, fileDeleter *file.Deleter, fileID models.FileID) error {
|
||||||
mgr := GetInstance()
|
mgr := GetInstance()
|
||||||
sceneQB := mgr.Database.Scene
|
sceneQB := mgr.Database.Scene
|
||||||
scenes, err := sceneQB.FindByFileID(ctx, fileID)
|
scenes, err := sceneQB.FindByFileID(ctx, fileID)
|
||||||
|
|
@ -313,7 +313,7 @@ func (h *cleanHandler) handleRelatedScenes(ctx context.Context, fileDeleter *fil
|
||||||
}, nil)
|
}, nil)
|
||||||
} else {
|
} else {
|
||||||
// set the primary file to a remaining file
|
// set the primary file to a remaining file
|
||||||
var newPrimaryID file.ID
|
var newPrimaryID models.FileID
|
||||||
for _, f := range scene.Files.List() {
|
for _, f := range scene.Files.List() {
|
||||||
if f.ID != fileID {
|
if f.ID != fileID {
|
||||||
newPrimaryID = f.ID
|
newPrimaryID = f.ID
|
||||||
|
|
@ -332,7 +332,7 @@ func (h *cleanHandler) handleRelatedScenes(ctx context.Context, fileDeleter *fil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *cleanHandler) handleRelatedGalleries(ctx context.Context, fileID file.ID) error {
|
func (h *cleanHandler) handleRelatedGalleries(ctx context.Context, fileID models.FileID) error {
|
||||||
mgr := GetInstance()
|
mgr := GetInstance()
|
||||||
qb := mgr.Database.Gallery
|
qb := mgr.Database.Gallery
|
||||||
galleries, err := qb.FindByFileID(ctx, fileID)
|
galleries, err := qb.FindByFileID(ctx, fileID)
|
||||||
|
|
@ -358,7 +358,7 @@ func (h *cleanHandler) handleRelatedGalleries(ctx context.Context, fileID file.I
|
||||||
}, nil)
|
}, nil)
|
||||||
} else {
|
} else {
|
||||||
// set the primary file to a remaining file
|
// set the primary file to a remaining file
|
||||||
var newPrimaryID file.ID
|
var newPrimaryID models.FileID
|
||||||
for _, f := range g.Files.List() {
|
for _, f := range g.Files.List() {
|
||||||
if f.Base().ID != fileID {
|
if f.Base().ID != fileID {
|
||||||
newPrimaryID = f.Base().ID
|
newPrimaryID = f.Base().ID
|
||||||
|
|
@ -377,7 +377,7 @@ func (h *cleanHandler) handleRelatedGalleries(ctx context.Context, fileID file.I
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *cleanHandler) deleteRelatedFolderGalleries(ctx context.Context, folderID file.FolderID) error {
|
func (h *cleanHandler) deleteRelatedFolderGalleries(ctx context.Context, folderID models.FolderID) error {
|
||||||
mgr := GetInstance()
|
mgr := GetInstance()
|
||||||
qb := mgr.Database.Gallery
|
qb := mgr.Database.Gallery
|
||||||
galleries, err := qb.FindByFolderID(ctx, folderID)
|
galleries, err := qb.FindByFolderID(ctx, folderID)
|
||||||
|
|
@ -401,7 +401,7 @@ func (h *cleanHandler) deleteRelatedFolderGalleries(ctx context.Context, folderI
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *cleanHandler) handleRelatedImages(ctx context.Context, fileDeleter *file.Deleter, fileID file.ID) error {
|
func (h *cleanHandler) handleRelatedImages(ctx context.Context, fileDeleter *file.Deleter, fileID models.FileID) error {
|
||||||
mgr := GetInstance()
|
mgr := GetInstance()
|
||||||
imageQB := mgr.Database.Image
|
imageQB := mgr.Database.Image
|
||||||
images, err := imageQB.FindByFileID(ctx, fileID)
|
images, err := imageQB.FindByFileID(ctx, fileID)
|
||||||
|
|
@ -431,7 +431,7 @@ func (h *cleanHandler) handleRelatedImages(ctx context.Context, fileDeleter *fil
|
||||||
}, nil)
|
}, nil)
|
||||||
} else {
|
} else {
|
||||||
// set the primary file to a remaining file
|
// set the primary file to a remaining file
|
||||||
var newPrimaryID file.ID
|
var newPrimaryID models.FileID
|
||||||
for _, f := range i.Files.List() {
|
for _, f := range i.Files.List() {
|
||||||
if f.Base().ID != fileID {
|
if f.Base().ID != fileID {
|
||||||
newPrimaryID = f.Base().ID
|
newPrimaryID = f.Base().ID
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/manager/config"
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/gallery"
|
"github.com/stashapp/stash/pkg/gallery"
|
||||||
"github.com/stashapp/stash/pkg/image"
|
"github.com/stashapp/stash/pkg/image"
|
||||||
|
|
@ -386,7 +385,7 @@ func (t *ExportTask) ExportScenes(ctx context.Context, workers int, repo Reposit
|
||||||
logger.Infof("[scenes] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
logger.Infof("[scenes] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func exportFile(f file.File, t *ExportTask) {
|
func exportFile(f models.File, t *ExportTask) {
|
||||||
newFileJSON := fileToJSON(f)
|
newFileJSON := fileToJSON(f)
|
||||||
|
|
||||||
fn := newFileJSON.Filename()
|
fn := newFileJSON.Filename()
|
||||||
|
|
@ -396,7 +395,7 @@ func exportFile(f file.File, t *ExportTask) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fileToJSON(f file.File) jsonschema.DirEntry {
|
func fileToJSON(f models.File) jsonschema.DirEntry {
|
||||||
bf := f.Base()
|
bf := f.Base()
|
||||||
|
|
||||||
base := jsonschema.BaseFile{
|
base := jsonschema.BaseFile{
|
||||||
|
|
@ -422,7 +421,7 @@ func fileToJSON(f file.File) jsonschema.DirEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ff := f.(type) {
|
switch ff := f.(type) {
|
||||||
case *file.VideoFile:
|
case *models.VideoFile:
|
||||||
base.Type = jsonschema.DirEntryTypeVideo
|
base.Type = jsonschema.DirEntryTypeVideo
|
||||||
return jsonschema.VideoFile{
|
return jsonschema.VideoFile{
|
||||||
BaseFile: &base,
|
BaseFile: &base,
|
||||||
|
|
@ -437,7 +436,7 @@ func fileToJSON(f file.File) jsonschema.DirEntry {
|
||||||
Interactive: ff.Interactive,
|
Interactive: ff.Interactive,
|
||||||
InteractiveSpeed: ff.InteractiveSpeed,
|
InteractiveSpeed: ff.InteractiveSpeed,
|
||||||
}
|
}
|
||||||
case *file.ImageFile:
|
case *models.ImageFile:
|
||||||
base.Type = jsonschema.DirEntryTypeImage
|
base.Type = jsonschema.DirEntryTypeImage
|
||||||
return jsonschema.ImageFile{
|
return jsonschema.ImageFile{
|
||||||
BaseFile: &base,
|
BaseFile: &base,
|
||||||
|
|
@ -450,7 +449,7 @@ func fileToJSON(f file.File) jsonschema.DirEntry {
|
||||||
return &base
|
return &base
|
||||||
}
|
}
|
||||||
|
|
||||||
func exportFolder(f file.Folder, t *ExportTask) {
|
func exportFolder(f models.Folder, t *ExportTask) {
|
||||||
newFileJSON := folderToJSON(f)
|
newFileJSON := folderToJSON(f)
|
||||||
|
|
||||||
fn := newFileJSON.Filename()
|
fn := newFileJSON.Filename()
|
||||||
|
|
@ -460,7 +459,7 @@ func exportFolder(f file.Folder, t *ExportTask) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func folderToJSON(f file.Folder) jsonschema.DirEntry {
|
func folderToJSON(f models.Folder) jsonschema.DirEntry {
|
||||||
base := jsonschema.BaseDirEntry{
|
base := jsonschema.BaseDirEntry{
|
||||||
Type: jsonschema.DirEntryTypeFolder,
|
Type: jsonschema.DirEntryTypeFolder,
|
||||||
ModTime: json.JSONTime{Time: f.ModTime},
|
ModTime: json.JSONTime{Time: f.ModTime},
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/image"
|
"github.com/stashapp/stash/pkg/image"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
|
@ -44,7 +43,7 @@ func (t *GenerateClipPreviewTask) Start(ctx context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *GenerateClipPreviewTask) required() bool {
|
func (t *GenerateClipPreviewTask) required() bool {
|
||||||
_, ok := t.Image.Files.Primary().(*file.VideoFile)
|
_, ok := t.Image.Files.Primary().(*models.VideoFile)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
|
@ -102,7 +101,7 @@ func (t *GenerateMarkersTask) generateSceneMarkers(ctx context.Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *GenerateMarkersTask) generateMarker(videoFile *file.VideoFile, scene *models.Scene, sceneMarker *models.SceneMarker) {
|
func (t *GenerateMarkersTask) generateMarker(videoFile *models.VideoFile, scene *models.Scene, sceneMarker *models.SceneMarker) {
|
||||||
sceneHash := t.Scene.GetHash(t.fileNamingAlgorithm)
|
sceneHash := t.Scene.GetHash(t.fileNamingAlgorithm)
|
||||||
seconds := int(sceneMarker.Seconds)
|
seconds := int(sceneMarker.Seconds)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/hash/videophash"
|
"github.com/stashapp/stash/pkg/hash/videophash"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
|
@ -12,11 +11,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type GeneratePhashTask struct {
|
type GeneratePhashTask struct {
|
||||||
File *file.VideoFile
|
File *models.VideoFile
|
||||||
Overwrite bool
|
Overwrite bool
|
||||||
fileNamingAlgorithm models.HashAlgorithm
|
fileNamingAlgorithm models.HashAlgorithm
|
||||||
txnManager txn.Manager
|
txnManager txn.Manager
|
||||||
fileUpdater file.Updater
|
fileUpdater models.FileUpdater
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *GeneratePhashTask) GetDescription() string {
|
func (t *GeneratePhashTask) GetDescription() string {
|
||||||
|
|
@ -38,8 +37,8 @@ func (t *GeneratePhashTask) Start(ctx context.Context) {
|
||||||
if err := txn.WithTxn(ctx, t.txnManager, func(ctx context.Context) error {
|
if err := txn.WithTxn(ctx, t.txnManager, func(ctx context.Context) error {
|
||||||
qb := t.fileUpdater
|
qb := t.fileUpdater
|
||||||
hashValue := int64(*hash)
|
hashValue := int64(*hash)
|
||||||
t.File.Fingerprints = t.File.Fingerprints.AppendUnique(file.Fingerprint{
|
t.File.Fingerprints = t.File.Fingerprints.AppendUnique(models.Fingerprint{
|
||||||
Type: file.FingerprintTypePhash,
|
Type: models.FingerprintTypePhash,
|
||||||
Fingerprint: hashValue,
|
Fingerprint: hashValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -54,5 +53,5 @@ func (t *GeneratePhashTask) required() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.File.Fingerprints.Get(file.FingerprintTypePhash) == nil
|
return t.File.Fingerprints.Get(models.FingerprintTypePhash) == nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ func (j *IdentifyJob) identifyScene(ctx context.Context, s *models.Scene, source
|
||||||
SceneReaderUpdater: instance.Repository.Scene,
|
SceneReaderUpdater: instance.Repository.Scene,
|
||||||
StudioReaderWriter: instance.Repository.Studio,
|
StudioReaderWriter: instance.Repository.Studio,
|
||||||
PerformerCreator: instance.Repository.Performer,
|
PerformerCreator: instance.Repository.Performer,
|
||||||
TagCreatorFinder: instance.Repository.Tag,
|
TagFinderCreator: instance.Repository.Tag,
|
||||||
|
|
||||||
DefaultOptions: j.input.Options,
|
DefaultOptions: j.input.Options,
|
||||||
Sources: sources,
|
Sources: sources,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
|
"github.com/stashapp/stash/pkg/file"
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/gallery"
|
"github.com/stashapp/stash/pkg/gallery"
|
||||||
"github.com/stashapp/stash/pkg/image"
|
"github.com/stashapp/stash/pkg/image"
|
||||||
|
|
@ -281,7 +282,7 @@ func (t *ImportTask) ImportStudios(ctx context.Context) {
|
||||||
logger.Info("[studios] import complete")
|
logger.Info("[studios] import complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ImportTask) ImportStudio(ctx context.Context, studioJSON *jsonschema.Studio, pendingParent map[string][]*jsonschema.Studio, readerWriter studio.NameFinderCreatorUpdater) error {
|
func (t *ImportTask) ImportStudio(ctx context.Context, studioJSON *jsonschema.Studio, pendingParent map[string][]*jsonschema.Studio, readerWriter studio.ImporterReaderWriter) error {
|
||||||
importer := &studio.Importer{
|
importer := &studio.Importer{
|
||||||
ReaderWriter: readerWriter,
|
ReaderWriter: readerWriter,
|
||||||
Input: *studioJSON,
|
Input: *studioJSON,
|
||||||
|
|
@ -385,7 +386,7 @@ func (t *ImportTask) ImportFiles(ctx context.Context) {
|
||||||
if err := t.txnManager.WithTxn(ctx, func(ctx context.Context) error {
|
if err := t.txnManager.WithTxn(ctx, func(ctx context.Context) error {
|
||||||
return t.ImportFile(ctx, fileJSON, pendingParent)
|
return t.ImportFile(ctx, fileJSON, pendingParent)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if errors.Is(err, errZipFileNotExist) {
|
if errors.Is(err, file.ErrZipFileNotExist) {
|
||||||
// add to the pending parent list so that it is created after the parent
|
// add to the pending parent list so that it is created after the parent
|
||||||
s := pendingParent[fileJSON.DirEntry().ZipFile]
|
s := pendingParent[fileJSON.DirEntry().ZipFile]
|
||||||
s = append(s, fileJSON)
|
s = append(s, fileJSON)
|
||||||
|
|
@ -421,7 +422,7 @@ func (t *ImportTask) ImportFile(ctx context.Context, fileJSON jsonschema.DirEntr
|
||||||
r := t.txnManager
|
r := t.txnManager
|
||||||
readerWriter := r.File
|
readerWriter := r.File
|
||||||
|
|
||||||
fileImporter := &fileFolderImporter{
|
fileImporter := &file.Importer{
|
||||||
ReaderWriter: readerWriter,
|
ReaderWriter: readerWriter,
|
||||||
FolderStore: r.Folder,
|
FolderStore: r.Folder,
|
||||||
Input: fileJSON,
|
Input: fileJSON,
|
||||||
|
|
@ -569,7 +570,7 @@ func (t *ImportTask) ImportTags(ctx context.Context) {
|
||||||
logger.Info("[tags] import complete")
|
logger.Info("[tags] import complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ImportTask) ImportTag(ctx context.Context, tagJSON *jsonschema.Tag, pendingParent map[string][]*jsonschema.Tag, fail bool, readerWriter tag.NameFinderCreatorUpdater) error {
|
func (t *ImportTask) ImportTag(ctx context.Context, tagJSON *jsonschema.Tag, pendingParent map[string][]*jsonschema.Tag, fail bool, readerWriter tag.ImporterReaderWriter) error {
|
||||||
importer := &tag.Importer{
|
importer := &tag.Importer{
|
||||||
ReaderWriter: readerWriter,
|
ReaderWriter: readerWriter,
|
||||||
Input: *tagJSON,
|
Input: *tagJSON,
|
||||||
|
|
|
||||||
|
|
@ -96,17 +96,17 @@ func newExtensionConfig(c *config.Instance) extensionConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
type fileCounter interface {
|
type fileCounter interface {
|
||||||
CountByFileID(ctx context.Context, fileID file.ID) (int, error)
|
CountByFileID(ctx context.Context, fileID models.FileID) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type galleryFinder interface {
|
type galleryFinder interface {
|
||||||
fileCounter
|
fileCounter
|
||||||
FindByFolderID(ctx context.Context, folderID file.FolderID) ([]*models.Gallery, error)
|
FindByFolderID(ctx context.Context, folderID models.FolderID) ([]*models.Gallery, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sceneFinder interface {
|
type sceneFinder interface {
|
||||||
fileCounter
|
fileCounter
|
||||||
FindByPrimaryFileID(ctx context.Context, fileID file.ID) ([]*models.Scene, error)
|
FindByPrimaryFileID(ctx context.Context, fileID models.FileID) ([]*models.Scene, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handlerRequiredFilter returns true if a File's handler needs to be executed despite the file not being updated.
|
// handlerRequiredFilter returns true if a File's handler needs to be executed despite the file not being updated.
|
||||||
|
|
@ -139,7 +139,7 @@ func newHandlerRequiredFilter(c *config.Instance) *handlerRequiredFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *handlerRequiredFilter) Accept(ctx context.Context, ff file.File) bool {
|
func (f *handlerRequiredFilter) Accept(ctx context.Context, ff models.File) bool {
|
||||||
path := ff.Base().Path
|
path := ff.Base().Path
|
||||||
isVideoFile := useAsVideo(path)
|
isVideoFile := useAsVideo(path)
|
||||||
isImageFile := useAsImage(path)
|
isImageFile := useAsImage(path)
|
||||||
|
|
@ -213,7 +213,7 @@ func (f *handlerRequiredFilter) Accept(ctx context.Context, ff file.File) bool {
|
||||||
|
|
||||||
// clean captions - scene handler handles this as well, but
|
// clean captions - scene handler handles this as well, but
|
||||||
// unchanged files aren't processed by the scene handler
|
// unchanged files aren't processed by the scene handler
|
||||||
videoFile, _ := ff.(*file.VideoFile)
|
videoFile, _ := ff.(*models.VideoFile)
|
||||||
if videoFile != nil {
|
if videoFile != nil {
|
||||||
if err := video.CleanCaptions(ctx, videoFile, f.txnManager, f.CaptionUpdater); err != nil {
|
if err := video.CleanCaptions(ctx, videoFile, f.txnManager, f.CaptionUpdater); err != nil {
|
||||||
logger.Errorf("Error cleaning captions: %v", err)
|
logger.Errorf("Error cleaning captions: %v", err)
|
||||||
|
|
@ -370,7 +370,7 @@ type imageGenerators struct {
|
||||||
progress *job.Progress
|
progress *job.Progress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f file.File) error {
|
func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f models.File) error {
|
||||||
const overwrite = false
|
const overwrite = false
|
||||||
|
|
||||||
progress := g.progress
|
progress := g.progress
|
||||||
|
|
@ -387,12 +387,12 @@ func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f file.
|
||||||
}
|
}
|
||||||
|
|
||||||
// avoid adding a task if the file isn't a video file
|
// avoid adding a task if the file isn't a video file
|
||||||
_, isVideo := f.(*file.VideoFile)
|
_, isVideo := f.(*models.VideoFile)
|
||||||
if isVideo && t.ScanGenerateClipPreviews {
|
if isVideo && t.ScanGenerateClipPreviews {
|
||||||
// this is a bit of a hack: the task requires files to be loaded, but
|
// this is a bit of a hack: the task requires files to be loaded, but
|
||||||
// we don't really need to since we already have the file
|
// we don't really need to since we already have the file
|
||||||
ii := *i
|
ii := *i
|
||||||
ii.Files = models.NewRelatedFiles([]file.File{f})
|
ii.Files = models.NewRelatedFiles([]models.File{f})
|
||||||
|
|
||||||
progress.AddTotal(1)
|
progress.AddTotal(1)
|
||||||
previewsFn := func(ctx context.Context) {
|
previewsFn := func(ctx context.Context) {
|
||||||
|
|
@ -415,7 +415,7 @@ func (g *imageGenerators) Generate(ctx context.Context, i *models.Image, f file.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *imageGenerators) generateThumbnail(ctx context.Context, i *models.Image, f file.File) error {
|
func (g *imageGenerators) generateThumbnail(ctx context.Context, i *models.Image, f models.File) error {
|
||||||
thumbPath := GetInstance().Paths.Generated.GetThumbnailPath(i.Checksum, models.DefaultGthumbWidth)
|
thumbPath := GetInstance().Paths.Generated.GetThumbnailPath(i.Checksum, models.DefaultGthumbWidth)
|
||||||
exists, _ := fsutil.FileExists(thumbPath)
|
exists, _ := fsutil.FileExists(thumbPath)
|
||||||
if exists {
|
if exists {
|
||||||
|
|
@ -424,12 +424,12 @@ func (g *imageGenerators) generateThumbnail(ctx context.Context, i *models.Image
|
||||||
|
|
||||||
path := f.Base().Path
|
path := f.Base().Path
|
||||||
|
|
||||||
asFrame, ok := f.(file.VisualFile)
|
vf, ok := f.(models.VisualFile)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("file %s does not implement Frame", path)
|
return fmt.Errorf("file %s is not a visual file", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
if asFrame.GetHeight() <= models.DefaultGthumbWidth && asFrame.GetWidth() <= models.DefaultGthumbWidth {
|
if vf.GetHeight() <= models.DefaultGthumbWidth && vf.GetWidth() <= models.DefaultGthumbWidth {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -466,7 +466,7 @@ type sceneGenerators struct {
|
||||||
progress *job.Progress
|
progress *job.Progress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *sceneGenerators) Generate(ctx context.Context, s *models.Scene, f *file.VideoFile) error {
|
func (g *sceneGenerators) Generate(ctx context.Context, s *models.Scene, f *models.VideoFile) error {
|
||||||
const overwrite = false
|
const overwrite = false
|
||||||
|
|
||||||
progress := g.progress
|
progress := g.progress
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
|
@ -51,7 +50,7 @@ const (
|
||||||
type StreamType struct {
|
type StreamType struct {
|
||||||
Name string
|
Name string
|
||||||
SegmentType *SegmentType
|
SegmentType *SegmentType
|
||||||
ServeManifest func(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *file.VideoFile, resolution string)
|
ServeManifest func(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *models.VideoFile, resolution string)
|
||||||
Args func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) Args
|
Args func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) Args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,7 +249,7 @@ var ErrInvalidSegment = errors.New("invalid segment")
|
||||||
|
|
||||||
type StreamOptions struct {
|
type StreamOptions struct {
|
||||||
StreamType *StreamType
|
StreamType *StreamType
|
||||||
VideoFile *file.VideoFile
|
VideoFile *models.VideoFile
|
||||||
Resolution string
|
Resolution string
|
||||||
Hash string
|
Hash string
|
||||||
Segment string
|
Segment string
|
||||||
|
|
@ -279,7 +278,7 @@ type waitingSegment struct {
|
||||||
type runningStream struct {
|
type runningStream struct {
|
||||||
dir string
|
dir string
|
||||||
streamType *StreamType
|
streamType *StreamType
|
||||||
vf *file.VideoFile
|
vf *models.VideoFile
|
||||||
maxTranscodeSize int
|
maxTranscodeSize int
|
||||||
outputDir string
|
outputDir string
|
||||||
|
|
||||||
|
|
@ -394,7 +393,7 @@ func (tp *transcodeProcess) checkSegments() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lastSegment(vf *file.VideoFile) int {
|
func lastSegment(vf *models.VideoFile) int {
|
||||||
return int(math.Ceil(vf.Duration/segmentLength)) - 1
|
return int(math.Ceil(vf.Duration/segmentLength)) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -405,7 +404,7 @@ func segmentExists(path string) bool {
|
||||||
|
|
||||||
// serveHLSManifest serves a generated HLS playlist. The URLs for the segments
|
// serveHLSManifest serves a generated HLS playlist. The URLs for the segments
|
||||||
// are of the form {r.URL}/%d.ts{?urlQuery} where %d is the segment index.
|
// are of the form {r.URL}/%d.ts{?urlQuery} where %d is the segment index.
|
||||||
func serveHLSManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *file.VideoFile, resolution string) {
|
func serveHLSManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *models.VideoFile, resolution string) {
|
||||||
if sm.cacheDir == "" {
|
if sm.cacheDir == "" {
|
||||||
logger.Error("[transcode] cannot live transcode with HLS because cache dir is unset")
|
logger.Error("[transcode] cannot live transcode with HLS because cache dir is unset")
|
||||||
http.Error(w, "cannot live transcode with HLS because cache dir is unset", http.StatusServiceUnavailable)
|
http.Error(w, "cannot live transcode with HLS because cache dir is unset", http.StatusServiceUnavailable)
|
||||||
|
|
@ -460,7 +459,7 @@ func serveHLSManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request,
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveDASHManifest serves a generated DASH manifest.
|
// serveDASHManifest serves a generated DASH manifest.
|
||||||
func serveDASHManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *file.VideoFile, resolution string) {
|
func serveDASHManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *models.VideoFile, resolution string) {
|
||||||
if sm.cacheDir == "" {
|
if sm.cacheDir == "" {
|
||||||
logger.Error("[transcode] cannot live transcode with DASH because cache dir is unset")
|
logger.Error("[transcode] cannot live transcode with DASH because cache dir is unset")
|
||||||
http.Error(w, "cannot live transcode files with DASH because cache dir is unset", http.StatusServiceUnavailable)
|
http.Error(w, "cannot live transcode files with DASH because cache dir is unset", http.StatusServiceUnavailable)
|
||||||
|
|
@ -550,7 +549,7 @@ func serveDASHManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request
|
||||||
utils.ServeStaticContent(w, r, buf.Bytes())
|
utils.ServeStaticContent(w, r, buf.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *StreamManager) ServeManifest(w http.ResponseWriter, r *http.Request, streamType *StreamType, vf *file.VideoFile, resolution string) {
|
func (sm *StreamManager) ServeManifest(w http.ResponseWriter, r *http.Request, streamType *StreamType, vf *models.VideoFile, resolution string) {
|
||||||
streamType.ServeManifest(sm, w, r, vf, resolution)
|
streamType.ServeManifest(sm, w, r, vf, resolution)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
|
@ -134,7 +133,7 @@ var (
|
||||||
|
|
||||||
type TranscodeOptions struct {
|
type TranscodeOptions struct {
|
||||||
StreamType StreamFormat
|
StreamType StreamFormat
|
||||||
VideoFile *file.VideoFile
|
VideoFile *models.VideoFile
|
||||||
Resolution string
|
Resolution string
|
||||||
StartTime float64
|
StartTime float64
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,13 @@ import (
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/job"
|
"github.com/stashapp/stash/pkg/job"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cleaner scans through stored file and folder instances and removes those that are no longer present on disk.
|
// Cleaner scans through stored file and folder instances and removes those that are no longer present on disk.
|
||||||
type Cleaner struct {
|
type Cleaner struct {
|
||||||
FS FS
|
FS models.FS
|
||||||
Repository Repository
|
Repository Repository
|
||||||
|
|
||||||
Handlers []CleanHandler
|
Handlers []CleanHandler
|
||||||
|
|
@ -55,44 +56,44 @@ func (s *Cleaner) Clean(ctx context.Context, options CleanOptions, progress *job
|
||||||
}
|
}
|
||||||
|
|
||||||
type fileOrFolder struct {
|
type fileOrFolder struct {
|
||||||
fileID ID
|
fileID models.FileID
|
||||||
folderID FolderID
|
folderID models.FolderID
|
||||||
}
|
}
|
||||||
|
|
||||||
type deleteSet struct {
|
type deleteSet struct {
|
||||||
orderedList []fileOrFolder
|
orderedList []fileOrFolder
|
||||||
fileIDSet map[ID]string
|
fileIDSet map[models.FileID]string
|
||||||
|
|
||||||
folderIDSet map[FolderID]string
|
folderIDSet map[models.FolderID]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDeleteSet() deleteSet {
|
func newDeleteSet() deleteSet {
|
||||||
return deleteSet{
|
return deleteSet{
|
||||||
fileIDSet: make(map[ID]string),
|
fileIDSet: make(map[models.FileID]string),
|
||||||
folderIDSet: make(map[FolderID]string),
|
folderIDSet: make(map[models.FolderID]string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *deleteSet) add(id ID, path string) {
|
func (s *deleteSet) add(id models.FileID, path string) {
|
||||||
if _, ok := s.fileIDSet[id]; !ok {
|
if _, ok := s.fileIDSet[id]; !ok {
|
||||||
s.orderedList = append(s.orderedList, fileOrFolder{fileID: id})
|
s.orderedList = append(s.orderedList, fileOrFolder{fileID: id})
|
||||||
s.fileIDSet[id] = path
|
s.fileIDSet[id] = path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *deleteSet) has(id ID) bool {
|
func (s *deleteSet) has(id models.FileID) bool {
|
||||||
_, ok := s.fileIDSet[id]
|
_, ok := s.fileIDSet[id]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *deleteSet) addFolder(id FolderID, path string) {
|
func (s *deleteSet) addFolder(id models.FolderID, path string) {
|
||||||
if _, ok := s.folderIDSet[id]; !ok {
|
if _, ok := s.folderIDSet[id]; !ok {
|
||||||
s.orderedList = append(s.orderedList, fileOrFolder{folderID: id})
|
s.orderedList = append(s.orderedList, fileOrFolder{folderID: id})
|
||||||
s.folderIDSet[id] = path
|
s.folderIDSet[id] = path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *deleteSet) hasFolder(id FolderID) bool {
|
func (s *deleteSet) hasFolder(id models.FolderID) bool {
|
||||||
_, ok := s.folderIDSet[id]
|
_, ok := s.folderIDSet[id]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +114,7 @@ func (j *cleanJob) execute(ctx context.Context) error {
|
||||||
|
|
||||||
if err := txn.WithReadTxn(ctx, j.Repository, func(ctx context.Context) error {
|
if err := txn.WithReadTxn(ctx, j.Repository, func(ctx context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
fileCount, err = j.Repository.CountAllInPaths(ctx, j.options.Paths)
|
fileCount, err = j.Repository.FileStore.CountAllInPaths(ctx, j.options.Paths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -177,7 +178,7 @@ func (j *cleanJob) assessFiles(ctx context.Context, toDelete *deleteSet) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := j.Repository.FindAllInPaths(ctx, j.options.Paths, batchSize, offset)
|
files, err := j.Repository.FileStore.FindAllInPaths(ctx, j.options.Paths, batchSize, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error querying for files: %w", err)
|
return fmt.Errorf("error querying for files: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -221,9 +222,9 @@ func (j *cleanJob) assessFiles(ctx context.Context, toDelete *deleteSet) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// flagFolderForDelete adds folders to the toDelete set, with the leaf folders added first
|
// flagFolderForDelete adds folders to the toDelete set, with the leaf folders added first
|
||||||
func (j *cleanJob) flagFileForDelete(ctx context.Context, toDelete *deleteSet, f File) error {
|
func (j *cleanJob) flagFileForDelete(ctx context.Context, toDelete *deleteSet, f models.File) error {
|
||||||
// add contained files first
|
// add contained files first
|
||||||
containedFiles, err := j.Repository.FindByZipFileID(ctx, f.Base().ID)
|
containedFiles, err := j.Repository.FileStore.FindByZipFileID(ctx, f.Base().ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error finding contained files for %q: %w", f.Base().Path, err)
|
return fmt.Errorf("error finding contained files for %q: %w", f.Base().Path, err)
|
||||||
}
|
}
|
||||||
|
|
@ -306,7 +307,7 @@ func (j *cleanJob) assessFolders(ctx context.Context, toDelete *deleteSet) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *cleanJob) flagFolderForDelete(ctx context.Context, toDelete *deleteSet, folder *Folder) error {
|
func (j *cleanJob) flagFolderForDelete(ctx context.Context, toDelete *deleteSet, folder *models.Folder) error {
|
||||||
// it is possible that child folders may be included while parent folders are not
|
// it is possible that child folders may be included while parent folders are not
|
||||||
// so we need to check child folders separately
|
// so we need to check child folders separately
|
||||||
toDelete.addFolder(folder.ID, folder.Path)
|
toDelete.addFolder(folder.ID, folder.Path)
|
||||||
|
|
@ -314,7 +315,7 @@ func (j *cleanJob) flagFolderForDelete(ctx context.Context, toDelete *deleteSet,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *cleanJob) shouldClean(ctx context.Context, f File) bool {
|
func (j *cleanJob) shouldClean(ctx context.Context, f models.File) bool {
|
||||||
path := f.Base().Path
|
path := f.Base().Path
|
||||||
|
|
||||||
info, err := f.Base().Info(j.FS)
|
info, err := f.Base().Info(j.FS)
|
||||||
|
|
@ -336,7 +337,7 @@ func (j *cleanJob) shouldClean(ctx context.Context, f File) bool {
|
||||||
return !filter.Accept(ctx, path, info)
|
return !filter.Accept(ctx, path, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *cleanJob) shouldCleanFolder(ctx context.Context, f *Folder) bool {
|
func (j *cleanJob) shouldCleanFolder(ctx context.Context, f *models.Folder) bool {
|
||||||
path := f.Path
|
path := f.Path
|
||||||
|
|
||||||
info, err := f.Info(j.FS)
|
info, err := f.Info(j.FS)
|
||||||
|
|
@ -376,7 +377,7 @@ func (j *cleanJob) shouldCleanFolder(ctx context.Context, f *Folder) bool {
|
||||||
return !filter.Accept(ctx, path, info)
|
return !filter.Accept(ctx, path, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *cleanJob) deleteFile(ctx context.Context, fileID ID, fn string) {
|
func (j *cleanJob) deleteFile(ctx context.Context, fileID models.FileID, fn string) {
|
||||||
// delete associated objects
|
// delete associated objects
|
||||||
fileDeleter := NewDeleter()
|
fileDeleter := NewDeleter()
|
||||||
if err := txn.WithTxn(ctx, j.Repository, func(ctx context.Context) error {
|
if err := txn.WithTxn(ctx, j.Repository, func(ctx context.Context) error {
|
||||||
|
|
@ -386,14 +387,14 @@ func (j *cleanJob) deleteFile(ctx context.Context, fileID ID, fn string) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return j.Repository.Destroy(ctx, fileID)
|
return j.Repository.FileStore.Destroy(ctx, fileID)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.Errorf("Error deleting file %q from database: %s", fn, err.Error())
|
logger.Errorf("Error deleting file %q from database: %s", fn, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *cleanJob) deleteFolder(ctx context.Context, folderID FolderID, fn string) {
|
func (j *cleanJob) deleteFolder(ctx context.Context, folderID models.FolderID, fn string) {
|
||||||
// delete associated objects
|
// delete associated objects
|
||||||
fileDeleter := NewDeleter()
|
fileDeleter := NewDeleter()
|
||||||
if err := txn.WithTxn(ctx, j.Repository, func(ctx context.Context) error {
|
if err := txn.WithTxn(ctx, j.Repository, func(ctx context.Context) error {
|
||||||
|
|
@ -410,7 +411,7 @@ func (j *cleanJob) deleteFolder(ctx context.Context, folderID FolderID, fn strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *cleanJob) fireHandlers(ctx context.Context, fileDeleter *Deleter, fileID ID) error {
|
func (j *cleanJob) fireHandlers(ctx context.Context, fileDeleter *Deleter, fileID models.FileID) error {
|
||||||
for _, h := range j.Handlers {
|
for _, h := range j.Handlers {
|
||||||
if err := h.HandleFile(ctx, fileDeleter, fileID); err != nil {
|
if err := h.HandleFile(ctx, fileDeleter, fileID); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -420,7 +421,7 @@ func (j *cleanJob) fireHandlers(ctx context.Context, fileDeleter *Deleter, fileI
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *cleanJob) fireFolderHandlers(ctx context.Context, fileDeleter *Deleter, folderID FolderID) error {
|
func (j *cleanJob) fireFolderHandlers(ctx context.Context, fileDeleter *Deleter, folderID models.FolderID) error {
|
||||||
for _, h := range j.Handlers {
|
for _, h := range j.Handlers {
|
||||||
if err := h.HandleFolder(ctx, fileDeleter, folderID); err != nil {
|
if err := h.HandleFolder(ctx, fileDeleter, folderID); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -179,7 +180,7 @@ func (d *Deleter) renameForRestore(path string) error {
|
||||||
return d.RenamerRemover.Rename(path+deleteFileSuffix, path)
|
return d.RenamerRemover.Rename(path+deleteFileSuffix, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Destroy(ctx context.Context, destroyer Destroyer, f File, fileDeleter *Deleter, deleteFile bool) error {
|
func Destroy(ctx context.Context, destroyer models.FileDestroyer, f models.File, fileDeleter *Deleter, deleteFile bool) error {
|
||||||
if err := destroyer.Destroy(ctx, f.Base().ID); err != nil {
|
if err := destroyer.Destroy(ctx, f.Base().ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -195,11 +196,11 @@ func Destroy(ctx context.Context, destroyer Destroyer, f File, fileDeleter *Dele
|
||||||
}
|
}
|
||||||
|
|
||||||
type ZipDestroyer struct {
|
type ZipDestroyer struct {
|
||||||
FileDestroyer GetterDestroyer
|
FileDestroyer models.FileFinderDestroyer
|
||||||
FolderDestroyer FolderGetterDestroyer
|
FolderDestroyer models.FolderFinderDestroyer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ZipDestroyer) DestroyZip(ctx context.Context, f File, fileDeleter *Deleter, deleteFile bool) error {
|
func (d *ZipDestroyer) DestroyZip(ctx context.Context, f models.File, fileDeleter *Deleter, deleteFile bool) error {
|
||||||
// destroy contained files
|
// destroy contained files
|
||||||
files, err := d.FileDestroyer.FindByZipFileID(ctx, f.Base().ID)
|
files, err := d.FileDestroyer.FindByZipFileID(ctx, f.Base().ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
226
pkg/file/file.go
226
pkg/file/file.go
|
|
@ -1,225 +1,15 @@
|
||||||
package file
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"context"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ID represents an ID of a file.
|
// Repository provides access to storage methods for files and folders.
|
||||||
type ID int32
|
type Repository struct {
|
||||||
|
txn.Manager
|
||||||
|
txn.DatabaseProvider
|
||||||
|
|
||||||
func (i ID) String() string {
|
FileStore models.FileReaderWriter
|
||||||
return strconv.Itoa(int(i))
|
FolderStore models.FolderReaderWriter
|
||||||
}
|
|
||||||
|
|
||||||
// DirEntry represents a file or directory in the file system.
|
|
||||||
type DirEntry struct {
|
|
||||||
ZipFileID *ID `json:"zip_file_id"`
|
|
||||||
|
|
||||||
// transient - not persisted
|
|
||||||
// only guaranteed to have id, path and basename set
|
|
||||||
ZipFile File
|
|
||||||
|
|
||||||
ModTime time.Time `json:"mod_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *DirEntry) info(fs FS, path string) (fs.FileInfo, error) {
|
|
||||||
if e.ZipFile != nil {
|
|
||||||
zipPath := e.ZipFile.Base().Path
|
|
||||||
zfs, err := fs.OpenZip(zipPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer zfs.Close()
|
|
||||||
fs = zfs
|
|
||||||
}
|
|
||||||
// else assume os file
|
|
||||||
|
|
||||||
ret, err := fs.Lstat(path)
|
|
||||||
return ret, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// File represents a file in the file system.
|
|
||||||
type File interface {
|
|
||||||
Base() *BaseFile
|
|
||||||
SetFingerprints(fp Fingerprints)
|
|
||||||
Open(fs FS) (io.ReadCloser, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BaseFile represents a file in the file system.
|
|
||||||
type BaseFile struct {
|
|
||||||
ID ID `json:"id"`
|
|
||||||
|
|
||||||
DirEntry
|
|
||||||
|
|
||||||
// resolved from parent folder and basename only - not stored in DB
|
|
||||||
Path string `json:"path"`
|
|
||||||
|
|
||||||
Basename string `json:"basename"`
|
|
||||||
ParentFolderID FolderID `json:"parent_folder_id"`
|
|
||||||
|
|
||||||
Fingerprints Fingerprints `json:"fingerprints"`
|
|
||||||
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFingerprints sets the fingerprints of the file.
|
|
||||||
// If a fingerprint of the same type already exists, it is overwritten.
|
|
||||||
func (f *BaseFile) SetFingerprints(fp Fingerprints) {
|
|
||||||
for _, v := range fp {
|
|
||||||
f.SetFingerprint(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFingerprint sets the fingerprint of the file.
|
|
||||||
// If a fingerprint of the same type already exists, it is overwritten.
|
|
||||||
func (f *BaseFile) SetFingerprint(fp Fingerprint) {
|
|
||||||
for i, existing := range f.Fingerprints {
|
|
||||||
if existing.Type == fp.Type {
|
|
||||||
f.Fingerprints[i] = fp
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Fingerprints = append(f.Fingerprints, fp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base is used to fulfil the File interface.
|
|
||||||
func (f *BaseFile) Base() *BaseFile {
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *BaseFile) Open(fs FS) (io.ReadCloser, error) {
|
|
||||||
if f.ZipFile != nil {
|
|
||||||
zipPath := f.ZipFile.Base().Path
|
|
||||||
zfs, err := fs.OpenZip(zipPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return zfs.OpenOnly(f.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fs.Open(f.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *BaseFile) Info(fs FS) (fs.FileInfo, error) {
|
|
||||||
return f.info(fs, f.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *BaseFile) Serve(fs FS, w http.ResponseWriter, r *http.Request) error {
|
|
||||||
reader, err := f.Open(fs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer reader.Close()
|
|
||||||
|
|
||||||
content, ok := reader.(io.ReadSeeker)
|
|
||||||
if !ok {
|
|
||||||
data, err := io.ReadAll(reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
content = bytes.NewReader(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.URL.Query().Has("t") {
|
|
||||||
w.Header().Set("Cache-Control", "private, max-age=31536000, immutable")
|
|
||||||
} else {
|
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
|
||||||
}
|
|
||||||
http.ServeContent(w, r, f.Basename, f.ModTime, content)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Finder interface {
|
|
||||||
Find(ctx context.Context, id ...ID) ([]File, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getter provides methods to find Files.
|
|
||||||
type Getter interface {
|
|
||||||
Finder
|
|
||||||
FindByPath(ctx context.Context, path string) (File, error)
|
|
||||||
FindAllByPath(ctx context.Context, path string) ([]File, error)
|
|
||||||
FindByFingerprint(ctx context.Context, fp Fingerprint) ([]File, error)
|
|
||||||
FindByZipFileID(ctx context.Context, zipFileID ID) ([]File, error)
|
|
||||||
FindAllInPaths(ctx context.Context, p []string, limit, offset int) ([]File, error)
|
|
||||||
FindByFileInfo(ctx context.Context, info fs.FileInfo, size int64) ([]File, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Counter interface {
|
|
||||||
CountAllInPaths(ctx context.Context, p []string) (int, error)
|
|
||||||
CountByFolderID(ctx context.Context, folderID FolderID) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creator provides methods to create Files.
|
|
||||||
type Creator interface {
|
|
||||||
Create(ctx context.Context, f File) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Updater provides methods to update Files.
|
|
||||||
type Updater interface {
|
|
||||||
Update(ctx context.Context, f File) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Destroyer interface {
|
|
||||||
Destroy(ctx context.Context, id ID) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetterUpdater interface {
|
|
||||||
Getter
|
|
||||||
Updater
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetterDestroyer interface {
|
|
||||||
Getter
|
|
||||||
Destroyer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store provides methods to find, create and update Files.
|
|
||||||
type Store interface {
|
|
||||||
Getter
|
|
||||||
Counter
|
|
||||||
Creator
|
|
||||||
Updater
|
|
||||||
Destroyer
|
|
||||||
|
|
||||||
IsPrimary(ctx context.Context, fileID ID) (bool, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decorator wraps the Decorate method to add additional functionality while scanning files.
|
|
||||||
type Decorator interface {
|
|
||||||
Decorate(ctx context.Context, fs FS, f File) (File, error)
|
|
||||||
IsMissingMetadata(ctx context.Context, fs FS, f File) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilteredDecorator struct {
|
|
||||||
Decorator
|
|
||||||
Filter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decorate runs the decorator if the filter accepts the file.
|
|
||||||
func (d *FilteredDecorator) Decorate(ctx context.Context, fs FS, f File) (File, error) {
|
|
||||||
if d.Accept(ctx, f) {
|
|
||||||
return d.Decorator.Decorate(ctx, fs, f)
|
|
||||||
}
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FilteredDecorator) IsMissingMetadata(ctx context.Context, fs FS, f File) bool {
|
|
||||||
if d.Accept(ctx, f) {
|
|
||||||
return d.Decorator.IsMissingMetadata(ctx, fs, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,94 +3,16 @@ package file
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FolderID represents an ID of a folder.
|
|
||||||
type FolderID int32
|
|
||||||
|
|
||||||
// String converts the ID to a string.
|
|
||||||
func (i FolderID) String() string {
|
|
||||||
return strconv.Itoa(int(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Folder represents a folder in the file system.
|
|
||||||
type Folder struct {
|
|
||||||
ID FolderID `json:"id"`
|
|
||||||
DirEntry
|
|
||||||
Path string `json:"path"`
|
|
||||||
ParentFolderID *FolderID `json:"parent_folder_id"`
|
|
||||||
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Folder) Info(fs FS) (fs.FileInfo, error) {
|
|
||||||
return f.info(fs, f.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FolderFinder interface {
|
|
||||||
Find(ctx context.Context, id FolderID) (*Folder, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FolderPathFinder finds Folders by their path.
|
|
||||||
type FolderPathFinder interface {
|
|
||||||
FindByPath(ctx context.Context, path string) (*Folder, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FolderGetter provides methods to find Folders.
|
|
||||||
type FolderGetter interface {
|
|
||||||
FolderFinder
|
|
||||||
FolderPathFinder
|
|
||||||
FindByZipFileID(ctx context.Context, zipFileID ID) ([]*Folder, error)
|
|
||||||
FindAllInPaths(ctx context.Context, p []string, limit, offset int) ([]*Folder, error)
|
|
||||||
FindByParentFolderID(ctx context.Context, parentFolderID FolderID) ([]*Folder, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type FolderCounter interface {
|
|
||||||
CountAllInPaths(ctx context.Context, p []string) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FolderCreator provides methods to create Folders.
|
|
||||||
type FolderCreator interface {
|
|
||||||
Create(ctx context.Context, f *Folder) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type FolderFinderCreator interface {
|
|
||||||
FolderPathFinder
|
|
||||||
FolderCreator
|
|
||||||
}
|
|
||||||
|
|
||||||
// FolderUpdater provides methods to update Folders.
|
|
||||||
type FolderUpdater interface {
|
|
||||||
Update(ctx context.Context, f *Folder) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type FolderDestroyer interface {
|
|
||||||
Destroy(ctx context.Context, id FolderID) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type FolderGetterDestroyer interface {
|
|
||||||
FolderGetter
|
|
||||||
FolderDestroyer
|
|
||||||
}
|
|
||||||
|
|
||||||
// FolderStore provides methods to find, create and update Folders.
|
|
||||||
type FolderStore interface {
|
|
||||||
FolderGetter
|
|
||||||
FolderCounter
|
|
||||||
FolderCreator
|
|
||||||
FolderUpdater
|
|
||||||
FolderDestroyer
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOrCreateFolderHierarchy gets the folder for the given path, or creates a folder hierarchy for the given path if one if no existing folder is found.
|
// GetOrCreateFolderHierarchy gets the folder for the given path, or creates a folder hierarchy for the given path if one if no existing folder is found.
|
||||||
// Does not create any folders in the file system
|
// Does not create any folders in the file system
|
||||||
func GetOrCreateFolderHierarchy(ctx context.Context, fc FolderFinderCreator, path string) (*Folder, error) {
|
func GetOrCreateFolderHierarchy(ctx context.Context, fc models.FolderFinderCreator, path string) (*models.Folder, error) {
|
||||||
// get or create folder hierarchy
|
// get or create folder hierarchy
|
||||||
folder, err := fc.FindByPath(ctx, path)
|
folder, err := fc.FindByPath(ctx, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -106,10 +28,10 @@ func GetOrCreateFolderHierarchy(ctx context.Context, fc FolderFinderCreator, pat
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
folder = &Folder{
|
folder = &models.Folder{
|
||||||
Path: path,
|
Path: path,
|
||||||
ParentFolderID: &parent.ID,
|
ParentFolderID: &parent.ID,
|
||||||
DirEntry: DirEntry{
|
DirEntry: models.DirEntry{
|
||||||
// leave mod time empty for now - it will be updated when the folder is scanned
|
// leave mod time empty for now - it will be updated when the folder is scanned
|
||||||
},
|
},
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
|
|
@ -126,7 +48,7 @@ func GetOrCreateFolderHierarchy(ctx context.Context, fc FolderFinderCreator, pat
|
||||||
|
|
||||||
// TransferZipFolderHierarchy creates the folder hierarchy for zipFileID under newPath, and removes
|
// TransferZipFolderHierarchy creates the folder hierarchy for zipFileID under newPath, and removes
|
||||||
// ZipFileID from folders under oldPath.
|
// ZipFileID from folders under oldPath.
|
||||||
func TransferZipFolderHierarchy(ctx context.Context, folderStore FolderStore, zipFileID ID, oldPath string, newPath string) error {
|
func TransferZipFolderHierarchy(ctx context.Context, folderStore models.FolderReaderWriter, zipFileID models.FileID, oldPath string, newPath string) error {
|
||||||
zipFolders, err := folderStore.FindByZipFileID(ctx, zipFileID)
|
zipFolders, err := folderStore.FindByZipFileID(ctx, zipFileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,28 @@ import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type folderRenameCandidate struct {
|
type folderRenameCandidate struct {
|
||||||
folder *Folder
|
folder *models.Folder
|
||||||
found int
|
found int
|
||||||
files int
|
files int
|
||||||
}
|
}
|
||||||
|
|
||||||
type folderRenameDetector struct {
|
type folderRenameDetector struct {
|
||||||
// candidates is a map of folder id to the number of files that match
|
// candidates is a map of folder id to the number of files that match
|
||||||
candidates map[FolderID]folderRenameCandidate
|
candidates map[models.FolderID]folderRenameCandidate
|
||||||
// rejects is a set of folder ids which were found to still exist
|
// rejects is a set of folder ids which were found to still exist
|
||||||
rejects map[FolderID]struct{}
|
rejects map[models.FolderID]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *folderRenameDetector) isReject(id FolderID) bool {
|
func (d *folderRenameDetector) isReject(id models.FolderID) bool {
|
||||||
_, ok := d.rejects[id]
|
_, ok := d.rejects[id]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *folderRenameDetector) getCandidate(id FolderID) *folderRenameCandidate {
|
func (d *folderRenameDetector) getCandidate(id models.FolderID) *folderRenameCandidate {
|
||||||
c, ok := d.candidates[id]
|
c, ok := d.candidates[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -40,14 +41,14 @@ func (d *folderRenameDetector) setCandidate(c folderRenameCandidate) {
|
||||||
d.candidates[c.folder.ID] = c
|
d.candidates[c.folder.ID] = c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *folderRenameDetector) reject(id FolderID) {
|
func (d *folderRenameDetector) reject(id models.FolderID) {
|
||||||
d.rejects[id] = struct{}{}
|
d.rejects[id] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bestCandidate returns the folder that is the best candidate for a rename.
|
// bestCandidate returns the folder that is the best candidate for a rename.
|
||||||
// This is the folder that has the largest number of its original files that
|
// This is the folder that has the largest number of its original files that
|
||||||
// are still present in the new location.
|
// are still present in the new location.
|
||||||
func (d *folderRenameDetector) bestCandidate() *Folder {
|
func (d *folderRenameDetector) bestCandidate() *models.Folder {
|
||||||
if len(d.candidates) == 0 {
|
if len(d.candidates) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -74,14 +75,14 @@ func (d *folderRenameDetector) bestCandidate() *Folder {
|
||||||
return best.folder
|
return best.folder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) detectFolderMove(ctx context.Context, file scanFile) (*Folder, error) {
|
func (s *scanJob) detectFolderMove(ctx context.Context, file scanFile) (*models.Folder, error) {
|
||||||
// in order for a folder to be considered moved, the existing folder must be
|
// in order for a folder to be considered moved, the existing folder must be
|
||||||
// missing, and the majority of the old folder's files must be present, unchanged,
|
// missing, and the majority of the old folder's files must be present, unchanged,
|
||||||
// in the new folder.
|
// in the new folder.
|
||||||
|
|
||||||
detector := folderRenameDetector{
|
detector := folderRenameDetector{
|
||||||
candidates: make(map[FolderID]folderRenameCandidate),
|
candidates: make(map[models.FolderID]folderRenameCandidate),
|
||||||
rejects: make(map[FolderID]struct{}),
|
rejects: make(map[models.FolderID]struct{}),
|
||||||
}
|
}
|
||||||
// rejects is a set of folder ids which were found to still exist
|
// rejects is a set of folder ids which were found to still exist
|
||||||
|
|
||||||
|
|
@ -117,7 +118,7 @@ func (s *scanJob) detectFolderMove(ctx context.Context, file scanFile) (*Folder,
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the file exists in the database based on basename, size and mod time
|
// check if the file exists in the database based on basename, size and mod time
|
||||||
existing, err := s.Repository.Store.FindByFileInfo(ctx, info, size)
|
existing, err := s.Repository.FileStore.FindByFileInfo(ctx, info, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("checking for existing file %q: %w", path, err)
|
return fmt.Errorf("checking for existing file %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
@ -163,7 +164,7 @@ func (s *scanJob) detectFolderMove(ctx context.Context, file scanFile) (*Folder,
|
||||||
|
|
||||||
// parent folder is missing, possible candidate
|
// parent folder is missing, possible candidate
|
||||||
// count the total number of files in the existing folder
|
// count the total number of files in the existing folder
|
||||||
count, err := s.Repository.Store.CountByFolderID(ctx, parentFolderID)
|
count, err := s.Repository.FileStore.CountByFolderID(ctx, parentFolderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("counting files in folder %d: %w", parentFolderID, err)
|
return fmt.Errorf("counting files in folder %d: %w", parentFolderID, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
package file
|
|
||||||
|
|
||||||
// VisualFile is an interface for files that have a width and height.
|
|
||||||
type VisualFile interface {
|
|
||||||
File
|
|
||||||
GetWidth() int
|
|
||||||
GetHeight() int
|
|
||||||
GetFormat() string
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetMinResolution(f VisualFile) int {
|
|
||||||
w := f.GetWidth()
|
|
||||||
h := f.GetHeight()
|
|
||||||
|
|
||||||
if w < h {
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Opener provides an interface to open a file.
|
// Opener provides an interface to open a file.
|
||||||
|
|
@ -14,7 +15,7 @@ type Opener interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type fsOpener struct {
|
type fsOpener struct {
|
||||||
fs FS
|
fs models.FS
|
||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,15 +23,6 @@ func (o *fsOpener) Open() (io.ReadCloser, error) {
|
||||||
return o.fs.Open(o.name)
|
return o.fs.Open(o.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FS represents a file system.
|
|
||||||
type FS interface {
|
|
||||||
Stat(name string) (fs.FileInfo, error)
|
|
||||||
Lstat(name string) (fs.FileInfo, error)
|
|
||||||
Open(name string) (fs.ReadDirFile, error)
|
|
||||||
OpenZip(name string) (*ZipFS, error)
|
|
||||||
IsPathCaseSensitive(path string) (bool, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OsFS is a file system backed by the OS.
|
// OsFS is a file system backed by the OS.
|
||||||
type OsFS struct{}
|
type OsFS struct{}
|
||||||
|
|
||||||
|
|
@ -66,7 +58,7 @@ func (f *OsFS) Open(name string) (fs.ReadDirFile, error) {
|
||||||
return os.Open(name)
|
return os.Open(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *OsFS) OpenZip(name string) (*ZipFS, error) {
|
func (f *OsFS) OpenZip(name string) (models.ZipFS, error) {
|
||||||
info, err := f.Lstat(name)
|
info, err := f.Lstat(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package file
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PathFilter provides a filter function for paths.
|
// PathFilter provides a filter function for paths.
|
||||||
|
|
@ -18,18 +20,18 @@ func (pff PathFilterFunc) Accept(path string) bool {
|
||||||
|
|
||||||
// Filter provides a filter function for Files.
|
// Filter provides a filter function for Files.
|
||||||
type Filter interface {
|
type Filter interface {
|
||||||
Accept(ctx context.Context, f File) bool
|
Accept(ctx context.Context, f models.File) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilterFunc func(ctx context.Context, f File) bool
|
type FilterFunc func(ctx context.Context, f models.File) bool
|
||||||
|
|
||||||
func (ff FilterFunc) Accept(ctx context.Context, f File) bool {
|
func (ff FilterFunc) Accept(ctx context.Context, f models.File) bool {
|
||||||
return ff(ctx, f)
|
return ff(ctx, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler provides a handler for Files.
|
// Handler provides a handler for Files.
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
Handle(ctx context.Context, f File, oldFile File) error
|
Handle(ctx context.Context, f models.File, oldFile models.File) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilteredHandler is a Handler runs only if the filter accepts the file.
|
// FilteredHandler is a Handler runs only if the filter accepts the file.
|
||||||
|
|
@ -39,7 +41,7 @@ type FilteredHandler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle runs the handler if the filter accepts the file.
|
// Handle runs the handler if the filter accepts the file.
|
||||||
func (h *FilteredHandler) Handle(ctx context.Context, f File, oldFile File) error {
|
func (h *FilteredHandler) Handle(ctx context.Context, f models.File, oldFile models.File) error {
|
||||||
if h.Accept(ctx, f) {
|
if h.Accept(ctx, f) {
|
||||||
return h.Handler.Handle(ctx, f, oldFile)
|
return h.Handler.Handle(ctx, f, oldFile)
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +50,6 @@ func (h *FilteredHandler) Handle(ctx context.Context, f File, oldFile File) erro
|
||||||
|
|
||||||
// CleanHandler provides a handler for cleaning Files and Folders.
|
// CleanHandler provides a handler for cleaning Files and Folders.
|
||||||
type CleanHandler interface {
|
type CleanHandler interface {
|
||||||
HandleFile(ctx context.Context, fileDeleter *Deleter, fileID ID) error
|
HandleFile(ctx context.Context, fileDeleter *Deleter, fileID models.FileID) error
|
||||||
HandleFolder(ctx context.Context, fileDeleter *Deleter, folderID FolderID) error
|
HandleFolder(ctx context.Context, fileDeleter *Deleter, folderID models.FolderID) error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/file"
|
"github.com/stashapp/stash/pkg/file"
|
||||||
"github.com/stashapp/stash/pkg/file/video"
|
"github.com/stashapp/stash/pkg/file/video"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
_ "golang.org/x/image/webp"
|
_ "golang.org/x/image/webp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -21,10 +22,10 @@ type Decorator struct {
|
||||||
FFProbe ffmpeg.FFProbe
|
FFProbe ffmpeg.FFProbe
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decorator) Decorate(ctx context.Context, fs file.FS, f file.File) (file.File, error) {
|
func (d *Decorator) Decorate(ctx context.Context, fs models.FS, f models.File) (models.File, error) {
|
||||||
base := f.Base()
|
base := f.Base()
|
||||||
|
|
||||||
decorateFallback := func() (file.File, error) {
|
decorateFallback := func() (models.File, error) {
|
||||||
r, err := fs.Open(base.Path)
|
r, err := fs.Open(base.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return f, fmt.Errorf("reading image file %q: %w", base.Path, err)
|
return f, fmt.Errorf("reading image file %q: %w", base.Path, err)
|
||||||
|
|
@ -35,7 +36,7 @@ func (d *Decorator) Decorate(ctx context.Context, fs file.FS, f file.File) (file
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return f, fmt.Errorf("decoding image file %q: %w", base.Path, err)
|
return f, fmt.Errorf("decoding image file %q: %w", base.Path, err)
|
||||||
}
|
}
|
||||||
return &file.ImageFile{
|
return &models.ImageFile{
|
||||||
BaseFile: base,
|
BaseFile: base,
|
||||||
Format: format,
|
Format: format,
|
||||||
Width: c.Width,
|
Width: c.Width,
|
||||||
|
|
@ -58,7 +59,7 @@ func (d *Decorator) Decorate(ctx context.Context, fs file.FS, f file.File) (file
|
||||||
|
|
||||||
// Fallback to catch non-animated avif images that FFProbe detects as video files
|
// Fallback to catch non-animated avif images that FFProbe detects as video files
|
||||||
if probe.Bitrate == 0 && probe.VideoCodec == "av1" {
|
if probe.Bitrate == 0 && probe.VideoCodec == "av1" {
|
||||||
return &file.ImageFile{
|
return &models.ImageFile{
|
||||||
BaseFile: base,
|
BaseFile: base,
|
||||||
Format: "avif",
|
Format: "avif",
|
||||||
Width: probe.Width,
|
Width: probe.Width,
|
||||||
|
|
@ -78,7 +79,7 @@ func (d *Decorator) Decorate(ctx context.Context, fs file.FS, f file.File) (file
|
||||||
return videoFileDecorator.Decorate(ctx, fs, f)
|
return videoFileDecorator.Decorate(ctx, fs, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &file.ImageFile{
|
return &models.ImageFile{
|
||||||
BaseFile: base,
|
BaseFile: base,
|
||||||
Format: probe.VideoCodec,
|
Format: probe.VideoCodec,
|
||||||
Width: probe.Width,
|
Width: probe.Width,
|
||||||
|
|
@ -86,14 +87,14 @@ func (d *Decorator) Decorate(ctx context.Context, fs file.FS, f file.File) (file
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decorator) IsMissingMetadata(ctx context.Context, fs file.FS, f file.File) bool {
|
func (d *Decorator) IsMissingMetadata(ctx context.Context, fs models.FS, f models.File) bool {
|
||||||
const (
|
const (
|
||||||
unsetString = "unset"
|
unsetString = "unset"
|
||||||
unsetNumber = -1
|
unsetNumber = -1
|
||||||
)
|
)
|
||||||
|
|
||||||
imf, isImage := f.(*file.ImageFile)
|
imf, isImage := f.(*models.ImageFile)
|
||||||
vf, isVideo := f.(*file.VideoFile)
|
vf, isVideo := f.(*models.VideoFile)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case isImage:
|
case isImage:
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package file
|
|
||||||
|
|
||||||
// ImageFile is an extension of BaseFile to represent image files.
|
|
||||||
type ImageFile struct {
|
|
||||||
*BaseFile
|
|
||||||
Format string `json:"format"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f ImageFile) GetWidth() int {
|
|
||||||
return f.Width
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f ImageFile) GetHeight() int {
|
|
||||||
return f.Height
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f ImageFile) GetFormat() string {
|
|
||||||
return f.Format
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package manager
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -7,24 +7,22 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HACK: this is all here because of an import loop in jsonschema -> models -> file
|
var ErrZipFileNotExist = errors.New("zip file does not exist")
|
||||||
|
|
||||||
var errZipFileNotExist = errors.New("zip file does not exist")
|
type Importer struct {
|
||||||
|
ReaderWriter models.FileFinderCreator
|
||||||
type fileFolderImporter struct {
|
FolderStore models.FolderFinderCreator
|
||||||
ReaderWriter file.Store
|
|
||||||
FolderStore file.FolderStore
|
|
||||||
Input jsonschema.DirEntry
|
Input jsonschema.DirEntry
|
||||||
|
|
||||||
file file.File
|
file models.File
|
||||||
folder *file.Folder
|
folder *models.Folder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) PreImport(ctx context.Context) error {
|
func (i *Importer) PreImport(ctx context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch ff := i.Input.(type) {
|
switch ff := i.Input.(type) {
|
||||||
|
|
@ -37,9 +35,9 @@ func (i *fileFolderImporter) PreImport(ctx context.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) folderJSONToFolder(ctx context.Context, baseJSON *jsonschema.BaseDirEntry) (*file.Folder, error) {
|
func (i *Importer) folderJSONToFolder(ctx context.Context, baseJSON *jsonschema.BaseDirEntry) (*models.Folder, error) {
|
||||||
ret := file.Folder{
|
ret := models.Folder{
|
||||||
DirEntry: file.DirEntry{
|
DirEntry: models.DirEntry{
|
||||||
ModTime: baseJSON.ModTime.GetTime(),
|
ModTime: baseJSON.ModTime.GetTime(),
|
||||||
},
|
},
|
||||||
Path: baseJSON.Path,
|
Path: baseJSON.Path,
|
||||||
|
|
@ -56,14 +54,14 @@ func (i *fileFolderImporter) folderJSONToFolder(ctx context.Context, baseJSON *j
|
||||||
return &ret, nil
|
return &ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) fileJSONToFile(ctx context.Context, fileJSON jsonschema.DirEntry) (file.File, error) {
|
func (i *Importer) fileJSONToFile(ctx context.Context, fileJSON jsonschema.DirEntry) (models.File, error) {
|
||||||
switch ff := fileJSON.(type) {
|
switch ff := fileJSON.(type) {
|
||||||
case *jsonschema.VideoFile:
|
case *jsonschema.VideoFile:
|
||||||
baseFile, err := i.baseFileJSONToBaseFile(ctx, ff.BaseFile)
|
baseFile, err := i.baseFileJSONToBaseFile(ctx, ff.BaseFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &file.VideoFile{
|
return &models.VideoFile{
|
||||||
BaseFile: baseFile,
|
BaseFile: baseFile,
|
||||||
Format: ff.Format,
|
Format: ff.Format,
|
||||||
Width: ff.Width,
|
Width: ff.Width,
|
||||||
|
|
@ -81,7 +79,7 @@ func (i *fileFolderImporter) fileJSONToFile(ctx context.Context, fileJSON jsonsc
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &file.ImageFile{
|
return &models.ImageFile{
|
||||||
BaseFile: baseFile,
|
BaseFile: baseFile,
|
||||||
Format: ff.Format,
|
Format: ff.Format,
|
||||||
Width: ff.Width,
|
Width: ff.Width,
|
||||||
|
|
@ -94,9 +92,9 @@ func (i *fileFolderImporter) fileJSONToFile(ctx context.Context, fileJSON jsonsc
|
||||||
return nil, fmt.Errorf("unknown file type")
|
return nil, fmt.Errorf("unknown file type")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) baseFileJSONToBaseFile(ctx context.Context, baseJSON *jsonschema.BaseFile) (*file.BaseFile, error) {
|
func (i *Importer) baseFileJSONToBaseFile(ctx context.Context, baseJSON *jsonschema.BaseFile) (*models.BaseFile, error) {
|
||||||
baseFile := file.BaseFile{
|
baseFile := models.BaseFile{
|
||||||
DirEntry: file.DirEntry{
|
DirEntry: models.DirEntry{
|
||||||
ModTime: baseJSON.ModTime.GetTime(),
|
ModTime: baseJSON.ModTime.GetTime(),
|
||||||
},
|
},
|
||||||
Basename: filepath.Base(baseJSON.Path),
|
Basename: filepath.Base(baseJSON.Path),
|
||||||
|
|
@ -106,7 +104,7 @@ func (i *fileFolderImporter) baseFileJSONToBaseFile(ctx context.Context, baseJSO
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fp := range baseJSON.Fingerprints {
|
for _, fp := range baseJSON.Fingerprints {
|
||||||
baseFile.Fingerprints = append(baseFile.Fingerprints, file.Fingerprint{
|
baseFile.Fingerprints = append(baseFile.Fingerprints, models.Fingerprint{
|
||||||
Type: fp.Type,
|
Type: fp.Type,
|
||||||
Fingerprint: fp.Fingerprint,
|
Fingerprint: fp.Fingerprint,
|
||||||
})
|
})
|
||||||
|
|
@ -119,7 +117,7 @@ func (i *fileFolderImporter) baseFileJSONToBaseFile(ctx context.Context, baseJSO
|
||||||
return &baseFile, nil
|
return &baseFile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) populateZipFileID(ctx context.Context, f *file.DirEntry) error {
|
func (i *Importer) populateZipFileID(ctx context.Context, f *models.DirEntry) error {
|
||||||
zipFilePath := i.Input.DirEntry().ZipFile
|
zipFilePath := i.Input.DirEntry().ZipFile
|
||||||
if zipFilePath != "" {
|
if zipFilePath != "" {
|
||||||
zf, err := i.ReaderWriter.FindByPath(ctx, zipFilePath)
|
zf, err := i.ReaderWriter.FindByPath(ctx, zipFilePath)
|
||||||
|
|
@ -128,7 +126,7 @@ func (i *fileFolderImporter) populateZipFileID(ctx context.Context, f *file.DirE
|
||||||
}
|
}
|
||||||
|
|
||||||
if zf == nil {
|
if zf == nil {
|
||||||
return errZipFileNotExist
|
return ErrZipFileNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
id := zf.Base().ID
|
id := zf.Base().ID
|
||||||
|
|
@ -138,15 +136,15 @@ func (i *fileFolderImporter) populateZipFileID(ctx context.Context, f *file.DirE
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) PostImport(ctx context.Context, id int) error {
|
func (i *Importer) PostImport(ctx context.Context, id int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) Name() string {
|
func (i *Importer) Name() string {
|
||||||
return i.Input.DirEntry().Path
|
return i.Input.DirEntry().Path
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) FindExistingID(ctx context.Context) (*int, error) {
|
func (i *Importer) FindExistingID(ctx context.Context) (*int, error) {
|
||||||
path := i.Input.DirEntry().Path
|
path := i.Input.DirEntry().Path
|
||||||
existing, err := i.ReaderWriter.FindByPath(ctx, path)
|
existing, err := i.ReaderWriter.FindByPath(ctx, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -161,7 +159,7 @@ func (i *fileFolderImporter) FindExistingID(ctx context.Context) (*int, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) createFolderHierarchy(ctx context.Context, p string) (*file.Folder, error) {
|
func (i *Importer) createFolderHierarchy(ctx context.Context, p string) (*models.Folder, error) {
|
||||||
parentPath := filepath.Dir(p)
|
parentPath := filepath.Dir(p)
|
||||||
|
|
||||||
if parentPath == p {
|
if parentPath == p {
|
||||||
|
|
@ -177,7 +175,7 @@ func (i *fileFolderImporter) createFolderHierarchy(ctx context.Context, p string
|
||||||
return i.getOrCreateFolder(ctx, p, parent)
|
return i.getOrCreateFolder(ctx, p, parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) getOrCreateFolder(ctx context.Context, path string, parent *file.Folder) (*file.Folder, error) {
|
func (i *Importer) getOrCreateFolder(ctx context.Context, path string, parent *models.Folder) (*models.Folder, error) {
|
||||||
folder, err := i.FolderStore.FindByPath(ctx, path)
|
folder, err := i.FolderStore.FindByPath(ctx, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -189,7 +187,7 @@ func (i *fileFolderImporter) getOrCreateFolder(ctx context.Context, path string,
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
folder = &file.Folder{
|
folder = &models.Folder{
|
||||||
Path: path,
|
Path: path,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
|
|
@ -207,7 +205,7 @@ func (i *fileFolderImporter) getOrCreateFolder(ctx context.Context, path string,
|
||||||
return folder, nil
|
return folder, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) Create(ctx context.Context) (*int, error) {
|
func (i *Importer) Create(ctx context.Context) (*int, error) {
|
||||||
// create folder hierarchy and set parent folder id
|
// create folder hierarchy and set parent folder id
|
||||||
path := i.Input.DirEntry().Path
|
path := i.Input.DirEntry().Path
|
||||||
path = filepath.Dir(path)
|
path = filepath.Dir(path)
|
||||||
|
|
@ -223,7 +221,7 @@ func (i *fileFolderImporter) Create(ctx context.Context) (*int, error) {
|
||||||
return i.createFile(ctx, folder)
|
return i.createFile(ctx, folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) createFile(ctx context.Context, parentFolder *file.Folder) (*int, error) {
|
func (i *Importer) createFile(ctx context.Context, parentFolder *models.Folder) (*int, error) {
|
||||||
if parentFolder != nil {
|
if parentFolder != nil {
|
||||||
i.file.Base().ParentFolderID = parentFolder.ID
|
i.file.Base().ParentFolderID = parentFolder.ID
|
||||||
}
|
}
|
||||||
|
|
@ -236,7 +234,7 @@ func (i *fileFolderImporter) createFile(ctx context.Context, parentFolder *file.
|
||||||
return &id, nil
|
return &id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) createFolder(ctx context.Context, parentFolder *file.Folder) (*int, error) {
|
func (i *Importer) createFolder(ctx context.Context, parentFolder *models.Folder) (*int, error) {
|
||||||
if parentFolder != nil {
|
if parentFolder != nil {
|
||||||
i.folder.ParentFolderID = &parentFolder.ID
|
i.folder.ParentFolderID = &parentFolder.ID
|
||||||
}
|
}
|
||||||
|
|
@ -249,7 +247,7 @@ func (i *fileFolderImporter) createFolder(ctx context.Context, parentFolder *fil
|
||||||
return &id, nil
|
return &id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *fileFolderImporter) Update(ctx context.Context, id int) error {
|
func (i *Importer) Update(ctx context.Context, id int) error {
|
||||||
// update not supported
|
// update not supported
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -40,14 +41,14 @@ func (r folderCreatorStatRenamerImpl) Mkdir(name string, perm os.FileMode) error
|
||||||
|
|
||||||
type Mover struct {
|
type Mover struct {
|
||||||
Renamer DirMakerStatRenamer
|
Renamer DirMakerStatRenamer
|
||||||
Files GetterUpdater
|
Files models.FileFinderUpdater
|
||||||
Folders FolderStore
|
Folders models.FolderReaderWriter
|
||||||
|
|
||||||
moved map[string]string
|
moved map[string]string
|
||||||
foldersCreated []string
|
foldersCreated []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMover(fileStore GetterUpdater, folderStore FolderStore) *Mover {
|
func NewMover(fileStore models.FileFinderUpdater, folderStore models.FolderReaderWriter) *Mover {
|
||||||
return &Mover{
|
return &Mover{
|
||||||
Files: fileStore,
|
Files: fileStore,
|
||||||
Folders: folderStore,
|
Folders: folderStore,
|
||||||
|
|
@ -60,7 +61,7 @@ func NewMover(fileStore GetterUpdater, folderStore FolderStore) *Mover {
|
||||||
|
|
||||||
// Move moves the file to the given folder and basename. If basename is empty, then the existing basename is used.
|
// Move moves the file to the given folder and basename. If basename is empty, then the existing basename is used.
|
||||||
// Assumes that the parent folder exists in the filesystem.
|
// Assumes that the parent folder exists in the filesystem.
|
||||||
func (m *Mover) Move(ctx context.Context, f File, folder *Folder, basename string) error {
|
func (m *Mover) Move(ctx context.Context, f models.File, folder *models.Folder, basename string) error {
|
||||||
fBase := f.Base()
|
fBase := f.Base()
|
||||||
|
|
||||||
// don't allow moving files in zip files
|
// don't allow moving files in zip files
|
||||||
|
|
|
||||||
130
pkg/file/scan.go
130
pkg/file/scan.go
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
"github.com/remeh/sizedwaitgroup"
|
"github.com/remeh/sizedwaitgroup"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -24,15 +25,6 @@ const (
|
||||||
maxRetries = -1
|
maxRetries = -1
|
||||||
)
|
)
|
||||||
|
|
||||||
// Repository provides access to storage methods for files and folders.
|
|
||||||
type Repository struct {
|
|
||||||
txn.Manager
|
|
||||||
txn.DatabaseProvider
|
|
||||||
Store
|
|
||||||
|
|
||||||
FolderStore FolderStore
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scanner scans files into the database.
|
// Scanner scans files into the database.
|
||||||
//
|
//
|
||||||
// The scan process works using two goroutines. The first walks through the provided paths
|
// The scan process works using two goroutines. The first walks through the provided paths
|
||||||
|
|
@ -59,7 +51,7 @@ type Repository struct {
|
||||||
// If the file is not a renamed file, then the decorators are fired and the file is created, then
|
// If the file is not a renamed file, then the decorators are fired and the file is created, then
|
||||||
// the applicable handlers are fired.
|
// the applicable handlers are fired.
|
||||||
type Scanner struct {
|
type Scanner struct {
|
||||||
FS FS
|
FS models.FS
|
||||||
Repository Repository
|
Repository Repository
|
||||||
FingerprintCalculator FingerprintCalculator
|
FingerprintCalculator FingerprintCalculator
|
||||||
|
|
||||||
|
|
@ -67,6 +59,38 @@ type Scanner struct {
|
||||||
FileDecorators []Decorator
|
FileDecorators []Decorator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FingerprintCalculator calculates a fingerprint for the provided file.
|
||||||
|
type FingerprintCalculator interface {
|
||||||
|
CalculateFingerprints(f *models.BaseFile, o Opener, useExisting bool) ([]models.Fingerprint, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decorator wraps the Decorate method to add additional functionality while scanning files.
|
||||||
|
type Decorator interface {
|
||||||
|
Decorate(ctx context.Context, fs models.FS, f models.File) (models.File, error)
|
||||||
|
IsMissingMetadata(ctx context.Context, fs models.FS, f models.File) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilteredDecorator struct {
|
||||||
|
Decorator
|
||||||
|
Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decorate runs the decorator if the filter accepts the file.
|
||||||
|
func (d *FilteredDecorator) Decorate(ctx context.Context, fs models.FS, f models.File) (models.File, error) {
|
||||||
|
if d.Accept(ctx, f) {
|
||||||
|
return d.Decorator.Decorate(ctx, fs, f)
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FilteredDecorator) IsMissingMetadata(ctx context.Context, fs models.FS, f models.File) bool {
|
||||||
|
if d.Accept(ctx, f) {
|
||||||
|
return d.Decorator.IsMissingMetadata(ctx, fs, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// ProgressReporter is used to report progress of the scan.
|
// ProgressReporter is used to report progress of the scan.
|
||||||
type ProgressReporter interface {
|
type ProgressReporter interface {
|
||||||
AddTotal(total int)
|
AddTotal(total int)
|
||||||
|
|
@ -129,8 +153,8 @@ func (s *Scanner) Scan(ctx context.Context, handlers []Handler, options ScanOpti
|
||||||
}
|
}
|
||||||
|
|
||||||
type scanFile struct {
|
type scanFile struct {
|
||||||
*BaseFile
|
*models.BaseFile
|
||||||
fs FS
|
fs models.FS
|
||||||
info fs.FileInfo
|
info fs.FileInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,7 +222,7 @@ func (s *scanJob) queueFiles(ctx context.Context, paths []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) queueFileFunc(ctx context.Context, f FS, zipFile *scanFile) fs.WalkDirFunc {
|
func (s *scanJob) queueFileFunc(ctx context.Context, f models.FS, zipFile *scanFile) fs.WalkDirFunc {
|
||||||
return func(path string, d fs.DirEntry, err error) error {
|
return func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// don't let errors prevent scanning
|
// don't let errors prevent scanning
|
||||||
|
|
@ -229,8 +253,8 @@ func (s *scanJob) queueFileFunc(ctx context.Context, f FS, zipFile *scanFile) fs
|
||||||
}
|
}
|
||||||
|
|
||||||
ff := scanFile{
|
ff := scanFile{
|
||||||
BaseFile: &BaseFile{
|
BaseFile: &models.BaseFile{
|
||||||
DirEntry: DirEntry{
|
DirEntry: models.DirEntry{
|
||||||
ModTime: modTime(info),
|
ModTime: modTime(info),
|
||||||
},
|
},
|
||||||
Path: path,
|
Path: path,
|
||||||
|
|
@ -286,7 +310,7 @@ func (s *scanJob) queueFileFunc(ctx context.Context, f FS, zipFile *scanFile) fs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFileSize(f FS, path string, info fs.FileInfo) (int64, error) {
|
func getFileSize(f models.FS, path string, info fs.FileInfo) (int64, error) {
|
||||||
// #2196/#3042 - replace size with target size if file is a symlink
|
// #2196/#3042 - replace size with target size if file is a symlink
|
||||||
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
|
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||||
targetInfo, err := f.Stat(path)
|
targetInfo, err := f.Stat(path)
|
||||||
|
|
@ -408,10 +432,10 @@ func (s *scanJob) processQueueItem(ctx context.Context, f scanFile) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) getFolderID(ctx context.Context, path string) (*FolderID, error) {
|
func (s *scanJob) getFolderID(ctx context.Context, path string) (*models.FolderID, error) {
|
||||||
// check the folder cache first
|
// check the folder cache first
|
||||||
if f, ok := s.folderPathToID.Load(path); ok {
|
if f, ok := s.folderPathToID.Load(path); ok {
|
||||||
v := f.(FolderID)
|
v := f.(models.FolderID)
|
||||||
return &v, nil
|
return &v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -428,7 +452,7 @@ func (s *scanJob) getFolderID(ctx context.Context, path string) (*FolderID, erro
|
||||||
return &ret.ID, nil
|
return &ret.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) getZipFileID(ctx context.Context, zipFile *scanFile) (*ID, error) {
|
func (s *scanJob) getZipFileID(ctx context.Context, zipFile *scanFile) (*models.FileID, error) {
|
||||||
if zipFile == nil {
|
if zipFile == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -441,11 +465,11 @@ func (s *scanJob) getZipFileID(ctx context.Context, zipFile *scanFile) (*ID, err
|
||||||
|
|
||||||
// check the folder cache first
|
// check the folder cache first
|
||||||
if f, ok := s.zipPathToID.Load(path); ok {
|
if f, ok := s.zipPathToID.Load(path); ok {
|
||||||
v := f.(ID)
|
v := f.(models.FileID)
|
||||||
return &v, nil
|
return &v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ret, err := s.Repository.FindByPath(ctx, path)
|
ret, err := s.Repository.FileStore.FindByPath(ctx, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting zip file ID for %q: %w", path, err)
|
return nil, fmt.Errorf("getting zip file ID for %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
@ -489,7 +513,7 @@ func (s *scanJob) handleFolder(ctx context.Context, file scanFile) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) onNewFolder(ctx context.Context, file scanFile) (*Folder, error) {
|
func (s *scanJob) onNewFolder(ctx context.Context, file scanFile) (*models.Folder, error) {
|
||||||
renamed, err := s.handleFolderRename(ctx, file)
|
renamed, err := s.handleFolderRename(ctx, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -501,7 +525,7 @@ func (s *scanJob) onNewFolder(ctx context.Context, file scanFile) (*Folder, erro
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
toCreate := &Folder{
|
toCreate := &models.Folder{
|
||||||
DirEntry: file.DirEntry,
|
DirEntry: file.DirEntry,
|
||||||
Path: file.Path,
|
Path: file.Path,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
|
|
@ -536,7 +560,7 @@ func (s *scanJob) onNewFolder(ctx context.Context, file scanFile) (*Folder, erro
|
||||||
return toCreate, nil
|
return toCreate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) handleFolderRename(ctx context.Context, file scanFile) (*Folder, error) {
|
func (s *scanJob) handleFolderRename(ctx context.Context, file scanFile) (*models.Folder, error) {
|
||||||
// ignore folders in zip files
|
// ignore folders in zip files
|
||||||
if file.ZipFileID != nil {
|
if file.ZipFileID != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
@ -572,7 +596,7 @@ func (s *scanJob) handleFolderRename(ctx context.Context, file scanFile) (*Folde
|
||||||
return renamedFrom, nil
|
return renamedFrom, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) onExistingFolder(ctx context.Context, f scanFile, existing *Folder) (*Folder, error) {
|
func (s *scanJob) onExistingFolder(ctx context.Context, f scanFile, existing *models.Folder) (*models.Folder, error) {
|
||||||
update := false
|
update := false
|
||||||
|
|
||||||
// update if mod time is changed
|
// update if mod time is changed
|
||||||
|
|
@ -613,12 +637,12 @@ func modTime(info fs.FileInfo) time.Time {
|
||||||
func (s *scanJob) handleFile(ctx context.Context, f scanFile) error {
|
func (s *scanJob) handleFile(ctx context.Context, f scanFile) error {
|
||||||
defer s.incrementProgress(f)
|
defer s.incrementProgress(f)
|
||||||
|
|
||||||
var ff File
|
var ff models.File
|
||||||
// don't use a transaction to check if new or existing
|
// don't use a transaction to check if new or existing
|
||||||
if err := s.withDB(ctx, func(ctx context.Context) error {
|
if err := s.withDB(ctx, func(ctx context.Context) error {
|
||||||
// determine if file already exists in data store
|
// determine if file already exists in data store
|
||||||
var err error
|
var err error
|
||||||
ff, err = s.Repository.FindByPath(ctx, f.Path)
|
ff, err = s.Repository.FileStore.FindByPath(ctx, f.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("checking for existing file %q: %w", f.Path, err)
|
return fmt.Errorf("checking for existing file %q: %w", f.Path, err)
|
||||||
}
|
}
|
||||||
|
|
@ -661,7 +685,7 @@ func (s *scanJob) isZipFile(path string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) onNewFile(ctx context.Context, f scanFile) (File, error) {
|
func (s *scanJob) onNewFile(ctx context.Context, f scanFile) (models.File, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
baseFile := f.BaseFile
|
baseFile := f.BaseFile
|
||||||
|
|
@ -716,7 +740,7 @@ func (s *scanJob) onNewFile(ctx context.Context, f scanFile) (File, error) {
|
||||||
|
|
||||||
// if not renamed, queue file for creation
|
// if not renamed, queue file for creation
|
||||||
if err := s.withTxn(ctx, func(ctx context.Context) error {
|
if err := s.withTxn(ctx, func(ctx context.Context) error {
|
||||||
if err := s.Repository.Create(ctx, file); err != nil {
|
if err := s.Repository.FileStore.Create(ctx, file); err != nil {
|
||||||
return fmt.Errorf("creating file %q: %w", path, err)
|
return fmt.Errorf("creating file %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -732,7 +756,7 @@ func (s *scanJob) onNewFile(ctx context.Context, f scanFile) (File, error) {
|
||||||
return file, nil
|
return file, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) fireDecorators(ctx context.Context, fs FS, f File) (File, error) {
|
func (s *scanJob) fireDecorators(ctx context.Context, fs models.FS, f models.File) (models.File, error) {
|
||||||
for _, h := range s.FileDecorators {
|
for _, h := range s.FileDecorators {
|
||||||
var err error
|
var err error
|
||||||
f, err = h.Decorate(ctx, fs, f)
|
f, err = h.Decorate(ctx, fs, f)
|
||||||
|
|
@ -744,7 +768,7 @@ func (s *scanJob) fireDecorators(ctx context.Context, fs FS, f File) (File, erro
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) fireHandlers(ctx context.Context, f File, oldFile File) error {
|
func (s *scanJob) fireHandlers(ctx context.Context, f models.File, oldFile models.File) error {
|
||||||
for _, h := range s.handlers {
|
for _, h := range s.handlers {
|
||||||
if err := h.Handle(ctx, f, oldFile); err != nil {
|
if err := h.Handle(ctx, f, oldFile); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -754,7 +778,7 @@ func (s *scanJob) fireHandlers(ctx context.Context, f File, oldFile File) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) calculateFingerprints(fs FS, f *BaseFile, path string, useExisting bool) (Fingerprints, error) {
|
func (s *scanJob) calculateFingerprints(fs models.FS, f *models.BaseFile, path string, useExisting bool) (models.Fingerprints, error) {
|
||||||
// only log if we're (re)calculating fingerprints
|
// only log if we're (re)calculating fingerprints
|
||||||
if !useExisting {
|
if !useExisting {
|
||||||
logger.Infof("Calculating fingerprints for %s ...", path)
|
logger.Infof("Calculating fingerprints for %s ...", path)
|
||||||
|
|
@ -772,7 +796,7 @@ func (s *scanJob) calculateFingerprints(fs FS, f *BaseFile, path string, useExis
|
||||||
return fp, nil
|
return fp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendFileUnique(v []File, toAdd []File) []File {
|
func appendFileUnique(v []models.File, toAdd []models.File) []models.File {
|
||||||
for _, f := range toAdd {
|
for _, f := range toAdd {
|
||||||
found := false
|
found := false
|
||||||
id := f.Base().ID
|
id := f.Base().ID
|
||||||
|
|
@ -791,7 +815,7 @@ func appendFileUnique(v []File, toAdd []File) []File {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) getFileFS(f *BaseFile) (FS, error) {
|
func (s *scanJob) getFileFS(f *models.BaseFile) (models.FS, error) {
|
||||||
if f.ZipFile == nil {
|
if f.ZipFile == nil {
|
||||||
return s.FS, nil
|
return s.FS, nil
|
||||||
}
|
}
|
||||||
|
|
@ -805,11 +829,11 @@ func (s *scanJob) getFileFS(f *BaseFile) (FS, error) {
|
||||||
return fs.OpenZip(zipPath)
|
return fs.OpenZip(zipPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) handleRename(ctx context.Context, f File, fp []Fingerprint) (File, error) {
|
func (s *scanJob) handleRename(ctx context.Context, f models.File, fp []models.Fingerprint) (models.File, error) {
|
||||||
var others []File
|
var others []models.File
|
||||||
|
|
||||||
for _, tfp := range fp {
|
for _, tfp := range fp {
|
||||||
thisOthers, err := s.Repository.FindByFingerprint(ctx, tfp)
|
thisOthers, err := s.Repository.FileStore.FindByFingerprint(ctx, tfp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("getting files by fingerprint %v: %w", tfp, err)
|
return nil, fmt.Errorf("getting files by fingerprint %v: %w", tfp, err)
|
||||||
}
|
}
|
||||||
|
|
@ -817,7 +841,7 @@ func (s *scanJob) handleRename(ctx context.Context, f File, fp []Fingerprint) (F
|
||||||
others = appendFileUnique(others, thisOthers)
|
others = appendFileUnique(others, thisOthers)
|
||||||
}
|
}
|
||||||
|
|
||||||
var missing []File
|
var missing []models.File
|
||||||
|
|
||||||
fZipID := f.Base().ZipFileID
|
fZipID := f.Base().ZipFileID
|
||||||
for _, other := range others {
|
for _, other := range others {
|
||||||
|
|
@ -867,7 +891,7 @@ func (s *scanJob) handleRename(ctx context.Context, f File, fp []Fingerprint) (F
|
||||||
fBase.Fingerprints = otherBase.Fingerprints
|
fBase.Fingerprints = otherBase.Fingerprints
|
||||||
|
|
||||||
if err := s.withTxn(ctx, func(ctx context.Context) error {
|
if err := s.withTxn(ctx, func(ctx context.Context) error {
|
||||||
if err := s.Repository.Update(ctx, f); err != nil {
|
if err := s.Repository.FileStore.Update(ctx, f); err != nil {
|
||||||
return fmt.Errorf("updating file for rename %q: %w", fBase.Path, err)
|
return fmt.Errorf("updating file for rename %q: %w", fBase.Path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -889,7 +913,7 @@ func (s *scanJob) handleRename(ctx context.Context, f File, fp []Fingerprint) (F
|
||||||
return f, nil
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) isHandlerRequired(ctx context.Context, f File) bool {
|
func (s *scanJob) isHandlerRequired(ctx context.Context, f models.File) bool {
|
||||||
accept := len(s.options.HandlerRequiredFilters) == 0
|
accept := len(s.options.HandlerRequiredFilters) == 0
|
||||||
for _, filter := range s.options.HandlerRequiredFilters {
|
for _, filter := range s.options.HandlerRequiredFilters {
|
||||||
// accept if any filter accepts the file
|
// accept if any filter accepts the file
|
||||||
|
|
@ -910,7 +934,7 @@ func (s *scanJob) isHandlerRequired(ctx context.Context, f File) bool {
|
||||||
// - file size
|
// - file size
|
||||||
// - image format, width or height
|
// - image format, width or height
|
||||||
// - video codec, audio codec, format, width, height, framerate or bitrate
|
// - video codec, audio codec, format, width, height, framerate or bitrate
|
||||||
func (s *scanJob) isMissingMetadata(ctx context.Context, f scanFile, existing File) bool {
|
func (s *scanJob) isMissingMetadata(ctx context.Context, f scanFile, existing models.File) bool {
|
||||||
for _, h := range s.FileDecorators {
|
for _, h := range s.FileDecorators {
|
||||||
if h.IsMissingMetadata(ctx, f.fs, existing) {
|
if h.IsMissingMetadata(ctx, f.fs, existing) {
|
||||||
return true
|
return true
|
||||||
|
|
@ -920,7 +944,7 @@ func (s *scanJob) isMissingMetadata(ctx context.Context, f scanFile, existing Fi
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) setMissingMetadata(ctx context.Context, f scanFile, existing File) (File, error) {
|
func (s *scanJob) setMissingMetadata(ctx context.Context, f scanFile, existing models.File) (models.File, error) {
|
||||||
path := existing.Base().Path
|
path := existing.Base().Path
|
||||||
logger.Infof("Updating metadata for %s", path)
|
logger.Infof("Updating metadata for %s", path)
|
||||||
|
|
||||||
|
|
@ -934,7 +958,7 @@ func (s *scanJob) setMissingMetadata(ctx context.Context, f scanFile, existing F
|
||||||
|
|
||||||
// queue file for update
|
// queue file for update
|
||||||
if err := s.withTxn(ctx, func(ctx context.Context) error {
|
if err := s.withTxn(ctx, func(ctx context.Context) error {
|
||||||
if err := s.Repository.Update(ctx, existing); err != nil {
|
if err := s.Repository.FileStore.Update(ctx, existing); err != nil {
|
||||||
return fmt.Errorf("updating file %q: %w", path, err)
|
return fmt.Errorf("updating file %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -946,7 +970,7 @@ func (s *scanJob) setMissingMetadata(ctx context.Context, f scanFile, existing F
|
||||||
return existing, nil
|
return existing, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) setMissingFingerprints(ctx context.Context, f scanFile, existing File) (File, error) {
|
func (s *scanJob) setMissingFingerprints(ctx context.Context, f scanFile, existing models.File) (models.File, error) {
|
||||||
const useExisting = true
|
const useExisting = true
|
||||||
fp, err := s.calculateFingerprints(f.fs, existing.Base(), f.Path, useExisting)
|
fp, err := s.calculateFingerprints(f.fs, existing.Base(), f.Path, useExisting)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -957,7 +981,7 @@ func (s *scanJob) setMissingFingerprints(ctx context.Context, f scanFile, existi
|
||||||
existing.SetFingerprints(fp)
|
existing.SetFingerprints(fp)
|
||||||
|
|
||||||
if err := s.withTxn(ctx, func(ctx context.Context) error {
|
if err := s.withTxn(ctx, func(ctx context.Context) error {
|
||||||
if err := s.Repository.Update(ctx, existing); err != nil {
|
if err := s.Repository.FileStore.Update(ctx, existing); err != nil {
|
||||||
return fmt.Errorf("updating file %q: %w", f.Path, err)
|
return fmt.Errorf("updating file %q: %w", f.Path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -971,7 +995,7 @@ func (s *scanJob) setMissingFingerprints(ctx context.Context, f scanFile, existi
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns a file only if it was updated
|
// returns a file only if it was updated
|
||||||
func (s *scanJob) onExistingFile(ctx context.Context, f scanFile, existing File) (File, error) {
|
func (s *scanJob) onExistingFile(ctx context.Context, f scanFile, existing models.File) (models.File, error) {
|
||||||
base := existing.Base()
|
base := existing.Base()
|
||||||
path := base.Path
|
path := base.Path
|
||||||
|
|
||||||
|
|
@ -1006,7 +1030,7 @@ func (s *scanJob) onExistingFile(ctx context.Context, f scanFile, existing File)
|
||||||
|
|
||||||
// queue file for update
|
// queue file for update
|
||||||
if err := s.withTxn(ctx, func(ctx context.Context) error {
|
if err := s.withTxn(ctx, func(ctx context.Context) error {
|
||||||
if err := s.Repository.Update(ctx, existing); err != nil {
|
if err := s.Repository.FileStore.Update(ctx, existing); err != nil {
|
||||||
return fmt.Errorf("updating file %q: %w", path, err)
|
return fmt.Errorf("updating file %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1022,21 +1046,21 @@ func (s *scanJob) onExistingFile(ctx context.Context, f scanFile, existing File)
|
||||||
return existing, nil
|
return existing, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *scanJob) removeOutdatedFingerprints(existing File, fp Fingerprints) {
|
func (s *scanJob) removeOutdatedFingerprints(existing models.File, fp models.Fingerprints) {
|
||||||
// HACK - if no MD5 fingerprint was returned, and the oshash is changed
|
// HACK - if no MD5 fingerprint was returned, and the oshash is changed
|
||||||
// then remove the MD5 fingerprint
|
// then remove the MD5 fingerprint
|
||||||
oshash := fp.For(FingerprintTypeOshash)
|
oshash := fp.For(models.FingerprintTypeOshash)
|
||||||
if oshash == nil {
|
if oshash == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
existingOshash := existing.Base().Fingerprints.For(FingerprintTypeOshash)
|
existingOshash := existing.Base().Fingerprints.For(models.FingerprintTypeOshash)
|
||||||
if existingOshash == nil || *existingOshash == *oshash {
|
if existingOshash == nil || *existingOshash == *oshash {
|
||||||
// missing oshash or same oshash - nothing to do
|
// missing oshash or same oshash - nothing to do
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
md5 := fp.For(FingerprintTypeMD5)
|
md5 := fp.For(models.FingerprintTypeMD5)
|
||||||
|
|
||||||
if md5 != nil {
|
if md5 != nil {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
|
|
@ -1045,11 +1069,11 @@ func (s *scanJob) removeOutdatedFingerprints(existing File, fp Fingerprints) {
|
||||||
|
|
||||||
// oshash has changed, MD5 is missing - remove MD5 from the existing fingerprints
|
// oshash has changed, MD5 is missing - remove MD5 from the existing fingerprints
|
||||||
logger.Infof("Removing outdated checksum from %s", existing.Base().Path)
|
logger.Infof("Removing outdated checksum from %s", existing.Base().Path)
|
||||||
existing.Base().Fingerprints.Remove(FingerprintTypeMD5)
|
existing.Base().Fingerprints.Remove(models.FingerprintTypeMD5)
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns a file only if it was updated
|
// returns a file only if it was updated
|
||||||
func (s *scanJob) onUnchangedFile(ctx context.Context, f scanFile, existing File) (File, error) {
|
func (s *scanJob) onUnchangedFile(ctx context.Context, f scanFile, existing models.File) (models.File, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
isMissingMetdata := s.isMissingMetadata(ctx, f, existing)
|
isMissingMetdata := s.isMissingMetadata(ctx, f, existing)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/asticode/go-astisub"
|
"github.com/asticode/go-astisub"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/txn"
|
"github.com/stashapp/stash/pkg/txn"
|
||||||
|
|
@ -87,12 +86,12 @@ func getCaptionsLangFromPath(captionPath string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type CaptionUpdater interface {
|
type CaptionUpdater interface {
|
||||||
GetCaptions(ctx context.Context, fileID file.ID) ([]*models.VideoCaption, error)
|
GetCaptions(ctx context.Context, fileID models.FileID) ([]*models.VideoCaption, error)
|
||||||
UpdateCaptions(ctx context.Context, fileID file.ID, captions []*models.VideoCaption) error
|
UpdateCaptions(ctx context.Context, fileID models.FileID, captions []*models.VideoCaption) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// associates captions to scene/s with the same basename
|
// associates captions to scene/s with the same basename
|
||||||
func AssociateCaptions(ctx context.Context, captionPath string, txnMgr txn.Manager, fqb file.Getter, w CaptionUpdater) {
|
func AssociateCaptions(ctx context.Context, captionPath string, txnMgr txn.Manager, fqb models.FileFinder, w CaptionUpdater) {
|
||||||
captionLang := getCaptionsLangFromPath(captionPath)
|
captionLang := getCaptionsLangFromPath(captionPath)
|
||||||
|
|
||||||
captionPrefix := getCaptionPrefix(captionPath)
|
captionPrefix := getCaptionPrefix(captionPath)
|
||||||
|
|
@ -108,7 +107,7 @@ func AssociateCaptions(ctx context.Context, captionPath string, txnMgr txn.Manag
|
||||||
// found some files
|
// found some files
|
||||||
// filter out non video files
|
// filter out non video files
|
||||||
switch f.(type) {
|
switch f.(type) {
|
||||||
case *file.VideoFile:
|
case *models.VideoFile:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
|
|
@ -143,7 +142,7 @@ func AssociateCaptions(ctx context.Context, captionPath string, txnMgr txn.Manag
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanCaptions removes non existent/accessible language codes from captions
|
// CleanCaptions removes non existent/accessible language codes from captions
|
||||||
func CleanCaptions(ctx context.Context, f *file.VideoFile, txnMgr txn.Manager, w CaptionUpdater) error {
|
func CleanCaptions(ctx context.Context, f *models.VideoFile, txnMgr txn.Manager, w CaptionUpdater) error {
|
||||||
captions, err := w.GetCaptions(ctx, f.ID)
|
captions, err := w.GetCaptions(ctx, f.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getting captions for file %s: %w", f.Path, err)
|
return fmt.Errorf("getting captions for file %s: %w", f.Path, err)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
"github.com/stashapp/stash/pkg/file"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Decorator adds video specific fields to a File.
|
// Decorator adds video specific fields to a File.
|
||||||
|
|
@ -14,7 +15,7 @@ type Decorator struct {
|
||||||
FFProbe ffmpeg.FFProbe
|
FFProbe ffmpeg.FFProbe
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decorator) Decorate(ctx context.Context, fs file.FS, f file.File) (file.File, error) {
|
func (d *Decorator) Decorate(ctx context.Context, fs models.FS, f models.File) (models.File, error) {
|
||||||
if d.FFProbe == "" {
|
if d.FFProbe == "" {
|
||||||
return f, errors.New("ffprobe not configured")
|
return f, errors.New("ffprobe not configured")
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +43,7 @@ func (d *Decorator) Decorate(ctx context.Context, fs file.FS, f file.File) (file
|
||||||
interactive = true
|
interactive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return &file.VideoFile{
|
return &models.VideoFile{
|
||||||
BaseFile: base,
|
BaseFile: base,
|
||||||
Format: string(container),
|
Format: string(container),
|
||||||
VideoCodec: videoFile.VideoCodec,
|
VideoCodec: videoFile.VideoCodec,
|
||||||
|
|
@ -56,13 +57,13 @@ func (d *Decorator) Decorate(ctx context.Context, fs file.FS, f file.File) (file
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Decorator) IsMissingMetadata(ctx context.Context, fs file.FS, f file.File) bool {
|
func (d *Decorator) IsMissingMetadata(ctx context.Context, fs models.FS, f models.File) bool {
|
||||||
const (
|
const (
|
||||||
unsetString = "unset"
|
unsetString = "unset"
|
||||||
unsetNumber = -1
|
unsetNumber = -1
|
||||||
)
|
)
|
||||||
|
|
||||||
vf, ok := f.(*file.VideoFile)
|
vf, ok := f.(*models.VideoFile)
|
||||||
if !ok {
|
if !ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package file
|
|
||||||
|
|
||||||
// VideoFile is an extension of BaseFile to represent video files.
|
|
||||||
type VideoFile struct {
|
|
||||||
*BaseFile
|
|
||||||
Format string `json:"format"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
Duration float64 `json:"duration"`
|
|
||||||
VideoCodec string `json:"video_codec"`
|
|
||||||
AudioCodec string `json:"audio_codec"`
|
|
||||||
FrameRate float64 `json:"frame_rate"`
|
|
||||||
BitRate int64 `json:"bitrate"`
|
|
||||||
|
|
||||||
Interactive bool `json:"interactive"`
|
|
||||||
InteractiveSpeed *int `json:"interactive_speed"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f VideoFile) GetWidth() int {
|
|
||||||
return f.Width
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f VideoFile) GetHeight() int {
|
|
||||||
return f.Height
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f VideoFile) GetFormat() string {
|
|
||||||
return f.Format
|
|
||||||
}
|
|
||||||
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Modified from github.com/facebookgo/symwalk
|
// Modified from github.com/facebookgo/symwalk
|
||||||
|
|
@ -48,7 +50,7 @@ import (
|
||||||
//
|
//
|
||||||
// Note that symwalk.Walk does not terminate if there are any non-terminating loops in
|
// Note that symwalk.Walk does not terminate if there are any non-terminating loops in
|
||||||
// the file structure.
|
// the file structure.
|
||||||
func walkSym(f FS, filename string, linkDirname string, walkFn fs.WalkDirFunc) error {
|
func walkSym(f models.FS, filename string, linkDirname string, walkFn fs.WalkDirFunc) error {
|
||||||
symWalkFunc := func(path string, info fs.DirEntry, err error) error {
|
symWalkFunc := func(path string, info fs.DirEntry, err error) error {
|
||||||
|
|
||||||
if fname, err := filepath.Rel(filename, path); err == nil {
|
if fname, err := filepath.Rel(filename, path); err == nil {
|
||||||
|
|
@ -80,7 +82,7 @@ func walkSym(f FS, filename string, linkDirname string, walkFn fs.WalkDirFunc) e
|
||||||
}
|
}
|
||||||
|
|
||||||
// symWalk extends filepath.Walk to also follow symlinks
|
// symWalk extends filepath.Walk to also follow symlinks
|
||||||
func symWalk(fs FS, path string, walkFn fs.WalkDirFunc) error {
|
func symWalk(fs models.FS, path string, walkFn fs.WalkDirFunc) error {
|
||||||
return walkSym(fs, path, path, walkFn)
|
return walkSym(fs, path, path, walkFn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +95,7 @@ func (d *statDirEntry) IsDir() bool { return d.info.IsDir() }
|
||||||
func (d *statDirEntry) Type() fs.FileMode { return d.info.Mode().Type() }
|
func (d *statDirEntry) Type() fs.FileMode { return d.info.Mode().Type() }
|
||||||
func (d *statDirEntry) Info() (fs.FileInfo, error) { return d.info, nil }
|
func (d *statDirEntry) Info() (fs.FileInfo, error) { return d.info, nil }
|
||||||
|
|
||||||
func fsWalk(f FS, root string, fn fs.WalkDirFunc) error {
|
func fsWalk(f models.FS, root string, fn fs.WalkDirFunc) error {
|
||||||
info, err := f.Lstat(root)
|
info, err := f.Lstat(root)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fn(root, nil, err)
|
err = fn(root, nil, err)
|
||||||
|
|
@ -106,7 +108,7 @@ func fsWalk(f FS, root string, fn fs.WalkDirFunc) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func walkDir(f FS, path string, d fs.DirEntry, walkDirFn fs.WalkDirFunc) error {
|
func walkDir(f models.FS, path string, d fs.DirEntry, walkDirFn fs.WalkDirFunc) error {
|
||||||
if err := walkDirFn(path, d, nil); err != nil || !d.IsDir() {
|
if err := walkDirFn(path, d, nil); err != nil || !d.IsDir() {
|
||||||
if errors.Is(err, fs.SkipDir) && d.IsDir() {
|
if errors.Is(err, fs.SkipDir) && d.IsDir() {
|
||||||
// Successfully skipped directory.
|
// Successfully skipped directory.
|
||||||
|
|
@ -143,7 +145,7 @@ func walkDir(f FS, path string, d fs.DirEntry, walkDirFn fs.WalkDirFunc) error {
|
||||||
|
|
||||||
// readDir reads the directory named by dirname and returns
|
// readDir reads the directory named by dirname and returns
|
||||||
// a sorted list of directory entries.
|
// a sorted list of directory entries.
|
||||||
func readDir(fs FS, dirname string) ([]fs.DirEntry, error) {
|
func readDir(fs models.FS, dirname string) ([]fs.DirEntry, error) {
|
||||||
f, err := fs.Open(dirname)
|
f, err := fs.Open(dirname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/xWTF/chardet"
|
"github.com/xWTF/chardet"
|
||||||
|
|
||||||
"golang.org/x/net/html/charset"
|
"golang.org/x/net/html/charset"
|
||||||
|
|
@ -22,14 +23,14 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ZipFS is a file system backed by a zip file.
|
// ZipFS is a file system backed by a zip file.
|
||||||
type ZipFS struct {
|
type zipFS struct {
|
||||||
*zip.Reader
|
*zip.Reader
|
||||||
zipFileCloser io.Closer
|
zipFileCloser io.Closer
|
||||||
zipInfo fs.FileInfo
|
zipInfo fs.FileInfo
|
||||||
zipPath string
|
zipPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newZipFS(fs FS, path string, info fs.FileInfo) (*ZipFS, error) {
|
func newZipFS(fs models.FS, path string, info fs.FileInfo) (*zipFS, error) {
|
||||||
reader, err := fs.Open(path)
|
reader, err := fs.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -85,7 +86,7 @@ func newZipFS(fs FS, path string, info fs.FileInfo) (*ZipFS, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ZipFS{
|
return &zipFS{
|
||||||
Reader: zipReader,
|
Reader: zipReader,
|
||||||
zipFileCloser: reader,
|
zipFileCloser: reader,
|
||||||
zipInfo: info,
|
zipInfo: info,
|
||||||
|
|
@ -93,7 +94,7 @@ func newZipFS(fs FS, path string, info fs.FileInfo) (*ZipFS, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *ZipFS) rel(name string) (string, error) {
|
func (f *zipFS) rel(name string) (string, error) {
|
||||||
if f.zipPath == name {
|
if f.zipPath == name {
|
||||||
return ".", nil
|
return ".", nil
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +111,7 @@ func (f *ZipFS) rel(name string) (string, error) {
|
||||||
return relName, nil
|
return relName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *ZipFS) Stat(name string) (fs.FileInfo, error) {
|
func (f *zipFS) Stat(name string) (fs.FileInfo, error) {
|
||||||
reader, err := f.Open(name)
|
reader, err := f.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -120,15 +121,15 @@ func (f *ZipFS) Stat(name string) (fs.FileInfo, error) {
|
||||||
return reader.Stat()
|
return reader.Stat()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *ZipFS) Lstat(name string) (fs.FileInfo, error) {
|
func (f *zipFS) Lstat(name string) (fs.FileInfo, error) {
|
||||||
return f.Stat(name)
|
return f.Stat(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *ZipFS) OpenZip(name string) (*ZipFS, error) {
|
func (f *zipFS) OpenZip(name string) (models.ZipFS, error) {
|
||||||
return nil, errZipFSOpenZip
|
return nil, errZipFSOpenZip
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *ZipFS) IsPathCaseSensitive(path string) (bool, error) {
|
func (f *zipFS) IsPathCaseSensitive(path string) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +146,7 @@ func (f *zipReadDirFile) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||||
return asReadDirFile.ReadDir(n)
|
return asReadDirFile.ReadDir(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *ZipFS) Open(name string) (fs.ReadDirFile, error) {
|
func (f *zipFS) Open(name string) (fs.ReadDirFile, error) {
|
||||||
relName, err := f.rel(name)
|
relName, err := f.rel(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -161,12 +162,12 @@ func (f *ZipFS) Open(name string) (fs.ReadDirFile, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *ZipFS) Close() error {
|
func (f *zipFS) Close() error {
|
||||||
return f.zipFileCloser.Close()
|
return f.zipFileCloser.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// openOnly returns a ReadCloser where calling Close will close the zip fs as well.
|
// openOnly returns a ReadCloser where calling Close will close the zip fs as well.
|
||||||
func (f *ZipFS) OpenOnly(name string) (io.ReadCloser, error) {
|
func (f *zipFS) OpenOnly(name string) (io.ReadCloser, error) {
|
||||||
r, err := f.Open(name)
|
r, err := f.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,14 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChapterCreatorUpdater interface {
|
type ChapterImporterReaderWriter interface {
|
||||||
Create(ctx context.Context, newGalleryChapter *models.GalleryChapter) error
|
models.GalleryChapterCreatorUpdater
|
||||||
Update(ctx context.Context, updatedGalleryChapter *models.GalleryChapter) error
|
|
||||||
FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error)
|
FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChapterImporter struct {
|
type ChapterImporter struct {
|
||||||
GalleryID int
|
GalleryID int
|
||||||
ReaderWriter ChapterCreatorUpdater
|
ReaderWriter ChapterImporterReaderWriter
|
||||||
Input jsonschema.GalleryChapter
|
Input jsonschema.GalleryChapter
|
||||||
MissingRefBehaviour models.ImportMissingRefEnum
|
MissingRefBehaviour models.ImportMissingRefEnum
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,7 @@ func (s *Service) Destroy(ctx context.Context, i *models.Gallery, fileDeleter *i
|
||||||
return imgsDestroyed, nil
|
return imgsDestroyed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChapterDestroyer interface {
|
func DestroyChapter(ctx context.Context, galleryChapter *models.GalleryChapter, qb models.GalleryChapterDestroyer) error {
|
||||||
FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error)
|
|
||||||
Destroy(ctx context.Context, id int) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func DestroyChapter(ctx context.Context, galleryChapter *models.GalleryChapter, qb ChapterDestroyer) error {
|
|
||||||
return qb.Destroy(ctx, galleryChapter.ID)
|
return qb.Destroy(ctx, galleryChapter.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,8 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/json"
|
"github.com/stashapp/stash/pkg/models/json"
|
||||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||||
"github.com/stashapp/stash/pkg/studio"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChapterFinder interface {
|
|
||||||
FindByGalleryID(ctx context.Context, galleryID int) ([]*models.GalleryChapter, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToBasicJSON converts a gallery object into its JSON object equivalent. It
|
// ToBasicJSON converts a gallery object into its JSON object equivalent. It
|
||||||
// does not convert the relationships to other objects.
|
// does not convert the relationships to other objects.
|
||||||
func ToBasicJSON(gallery *models.Gallery) (*jsonschema.Gallery, error) {
|
func ToBasicJSON(gallery *models.Gallery) (*jsonschema.Gallery, error) {
|
||||||
|
|
@ -48,7 +43,7 @@ func ToBasicJSON(gallery *models.Gallery) (*jsonschema.Gallery, error) {
|
||||||
|
|
||||||
// GetStudioName returns the name of the provided gallery's studio. It returns an
|
// GetStudioName returns the name of the provided gallery's studio. It returns an
|
||||||
// empty string if there is no studio assigned to the gallery.
|
// empty string if there is no studio assigned to the gallery.
|
||||||
func GetStudioName(ctx context.Context, reader studio.Finder, gallery *models.Gallery) (string, error) {
|
func GetStudioName(ctx context.Context, reader models.StudioGetter, gallery *models.Gallery) (string, error) {
|
||||||
if gallery.StudioID != nil {
|
if gallery.StudioID != nil {
|
||||||
studio, err := reader.Find(ctx, *gallery.StudioID)
|
studio, err := reader.Find(ctx, *gallery.StudioID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -65,7 +60,7 @@ func GetStudioName(ctx context.Context, reader studio.Finder, gallery *models.Ga
|
||||||
|
|
||||||
// GetGalleryChaptersJSON returns a slice of GalleryChapter JSON representation
|
// GetGalleryChaptersJSON returns a slice of GalleryChapter JSON representation
|
||||||
// objects corresponding to the provided gallery's chapters.
|
// objects corresponding to the provided gallery's chapters.
|
||||||
func GetGalleryChaptersJSON(ctx context.Context, chapterReader ChapterFinder, gallery *models.Gallery) ([]jsonschema.GalleryChapter, error) {
|
func GetGalleryChaptersJSON(ctx context.Context, chapterReader models.GalleryChapterFinder, gallery *models.Gallery) ([]jsonschema.GalleryChapter, error) {
|
||||||
galleryChapters, err := chapterReader.FindByGalleryID(ctx, gallery.ID)
|
galleryChapters, err := chapterReader.FindByGalleryID(ctx, gallery.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting gallery chapters: %v", err)
|
return nil, fmt.Errorf("error getting gallery chapters: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package gallery
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/json"
|
"github.com/stashapp/stash/pkg/models/json"
|
||||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||||
|
|
@ -50,8 +49,8 @@ var (
|
||||||
func createFullGallery(id int) models.Gallery {
|
func createFullGallery(id int) models.Gallery {
|
||||||
return models.Gallery{
|
return models.Gallery{
|
||||||
ID: id,
|
ID: id,
|
||||||
Files: models.NewRelatedFiles([]file.File{
|
Files: models.NewRelatedFiles([]models.File{
|
||||||
&file.BaseFile{
|
&models.BaseFile{
|
||||||
Path: path,
|
Path: path,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
@ -69,8 +68,8 @@ func createFullGallery(id int) models.Gallery {
|
||||||
func createEmptyGallery(id int) models.Gallery {
|
func createEmptyGallery(id int) models.Gallery {
|
||||||
return models.Gallery{
|
return models.Gallery{
|
||||||
ID: id,
|
ID: id,
|
||||||
Files: models.NewRelatedFiles([]file.File{
|
Files: models.NewRelatedFiles([]models.File{
|
||||||
&file.BaseFile{
|
&models.BaseFile{
|
||||||
Path: path,
|
Path: path,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,25 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||||
"github.com/stashapp/stash/pkg/performer"
|
|
||||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
"github.com/stashapp/stash/pkg/studio"
|
|
||||||
"github.com/stashapp/stash/pkg/tag"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ImporterReaderWriter interface {
|
||||||
|
models.GalleryCreatorUpdater
|
||||||
|
FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Gallery, error)
|
||||||
|
FindByFolderID(ctx context.Context, folderID models.FolderID) ([]*models.Gallery, error)
|
||||||
|
FindUserGalleryByTitle(ctx context.Context, title string) ([]*models.Gallery, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Importer struct {
|
type Importer struct {
|
||||||
ReaderWriter FullCreatorUpdater
|
ReaderWriter ImporterReaderWriter
|
||||||
StudioWriter studio.NameFinderCreator
|
StudioWriter models.StudioFinderCreator
|
||||||
PerformerWriter performer.NameFinderCreator
|
PerformerWriter models.PerformerFinderCreator
|
||||||
TagWriter tag.NameFinderCreator
|
TagWriter models.TagFinderCreator
|
||||||
FileFinder file.Getter
|
FileFinder models.FileFinder
|
||||||
FolderFinder file.FolderGetter
|
FolderFinder models.FolderFinder
|
||||||
Input jsonschema.Gallery
|
Input jsonschema.Gallery
|
||||||
MissingRefBehaviour models.ImportMissingRefEnum
|
MissingRefBehaviour models.ImportMissingRefEnum
|
||||||
|
|
||||||
|
|
@ -28,11 +31,6 @@ type Importer struct {
|
||||||
gallery models.Gallery
|
gallery models.Gallery
|
||||||
}
|
}
|
||||||
|
|
||||||
type FullCreatorUpdater interface {
|
|
||||||
FinderCreatorUpdater
|
|
||||||
Update(ctx context.Context, updatedGallery *models.Gallery) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Importer) PreImport(ctx context.Context) error {
|
func (i *Importer) PreImport(ctx context.Context) error {
|
||||||
i.gallery = i.galleryJSONToGallery(i.Input)
|
i.gallery = i.galleryJSONToGallery(i.Input)
|
||||||
|
|
||||||
|
|
@ -251,7 +249,7 @@ func (i *Importer) createTags(ctx context.Context, names []string) ([]*models.Ta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Importer) populateFilesFolder(ctx context.Context) error {
|
func (i *Importer) populateFilesFolder(ctx context.Context) error {
|
||||||
files := make([]file.File, 0)
|
files := make([]models.File, 0)
|
||||||
|
|
||||||
for _, ref := range i.Input.ZipFiles {
|
for _, ref := range i.Input.ZipFiles {
|
||||||
path := ref
|
path := ref
|
||||||
|
|
@ -340,7 +338,7 @@ func (i *Importer) FindExistingID(ctx context.Context) (*int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Importer) Create(ctx context.Context) (*int, error) {
|
func (i *Importer) Create(ctx context.Context) (*int, error) {
|
||||||
var fileIDs []file.ID
|
var fileIDs []models.FileID
|
||||||
for _, f := range i.gallery.Files.List() {
|
for _, f := range i.gallery.Files.List() {
|
||||||
fileIDs = append(fileIDs, f.Base().ID)
|
fileIDs = append(fileIDs, f.Base().ID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/json"
|
"github.com/stashapp/stash/pkg/models/json"
|
||||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||||
|
|
@ -68,7 +67,7 @@ func TestImporterPreImport(t *testing.T) {
|
||||||
Rating: &rating,
|
Rating: &rating,
|
||||||
Organized: organized,
|
Organized: organized,
|
||||||
URL: url,
|
URL: url,
|
||||||
Files: models.NewRelatedFiles([]file.File{}),
|
Files: models.NewRelatedFiles([]models.File{}),
|
||||||
TagIDs: models.NewRelatedIDs([]int{}),
|
TagIDs: models.NewRelatedIDs([]int{}),
|
||||||
PerformerIDs: models.NewRelatedIDs([]int{}),
|
PerformerIDs: models.NewRelatedIDs([]int{}),
|
||||||
CreatedAt: createdAt,
|
CreatedAt: createdAt,
|
||||||
|
|
|
||||||
|
|
@ -4,27 +4,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Queryer interface {
|
func CountByPerformerID(ctx context.Context, r models.GalleryQueryer, id int) (int, error) {
|
||||||
Query(ctx context.Context, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CountQueryer interface {
|
|
||||||
QueryCount(ctx context.Context, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Finder interface {
|
|
||||||
FindByPath(ctx context.Context, p string) ([]*models.Gallery, error)
|
|
||||||
FindUserGalleryByTitle(ctx context.Context, title string) ([]*models.Gallery, error)
|
|
||||||
FindByFolderID(ctx context.Context, folderID file.FolderID) ([]*models.Gallery, error)
|
|
||||||
FindByFileID(ctx context.Context, fileID file.ID) ([]*models.Gallery, error)
|
|
||||||
FindByFingerprints(ctx context.Context, fp []file.Fingerprint) ([]*models.Gallery, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CountByPerformerID(ctx context.Context, r CountQueryer, id int) (int, error) {
|
|
||||||
filter := &models.GalleryFilterType{
|
filter := &models.GalleryFilterType{
|
||||||
Performers: &models.MultiCriterionInput{
|
Performers: &models.MultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(id)},
|
Value: []string{strconv.Itoa(id)},
|
||||||
|
|
@ -35,7 +18,7 @@ func CountByPerformerID(ctx context.Context, r CountQueryer, id int) (int, error
|
||||||
return r.QueryCount(ctx, filter, nil)
|
return r.QueryCount(ctx, filter, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountByStudioID(ctx context.Context, r CountQueryer, id int, depth *int) (int, error) {
|
func CountByStudioID(ctx context.Context, r models.GalleryQueryer, id int, depth *int) (int, error) {
|
||||||
filter := &models.GalleryFilterType{
|
filter := &models.GalleryFilterType{
|
||||||
Studios: &models.HierarchicalMultiCriterionInput{
|
Studios: &models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(id)},
|
Value: []string{strconv.Itoa(id)},
|
||||||
|
|
@ -47,7 +30,7 @@ func CountByStudioID(ctx context.Context, r CountQueryer, id int, depth *int) (i
|
||||||
return r.QueryCount(ctx, filter, nil)
|
return r.QueryCount(ctx, filter, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountByTagID(ctx context.Context, r CountQueryer, id int, depth *int) (int, error) {
|
func CountByTagID(ctx context.Context, r models.GalleryQueryer, id int, depth *int) (int, error) {
|
||||||
filter := &models.GalleryFilterType{
|
filter := &models.GalleryFilterType{
|
||||||
Tags: &models.HierarchicalMultiCriterionInput{
|
Tags: &models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(id)},
|
Value: []string{strconv.Itoa(id)},
|
||||||
|
|
|
||||||
|
|
@ -7,39 +7,40 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/plugin"
|
"github.com/stashapp/stash/pkg/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FinderCreatorUpdater interface {
|
type ScanCreatorUpdater interface {
|
||||||
Finder
|
FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Gallery, error)
|
||||||
Create(ctx context.Context, newGallery *models.Gallery, fileIDs []file.ID) error
|
FindByFingerprints(ctx context.Context, fp []models.Fingerprint) ([]*models.Gallery, error)
|
||||||
|
GetFiles(ctx context.Context, relatedID int) ([]models.File, error)
|
||||||
|
|
||||||
|
Create(ctx context.Context, newGallery *models.Gallery, fileIDs []models.FileID) error
|
||||||
UpdatePartial(ctx context.Context, id int, updatedGallery models.GalleryPartial) (*models.Gallery, error)
|
UpdatePartial(ctx context.Context, id int, updatedGallery models.GalleryPartial) (*models.Gallery, error)
|
||||||
AddFileID(ctx context.Context, id int, fileID file.ID) error
|
AddFileID(ctx context.Context, id int, fileID models.FileID) error
|
||||||
models.FileLoader
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SceneFinderUpdater interface {
|
type ScanSceneFinderUpdater interface {
|
||||||
FindByPath(ctx context.Context, p string) ([]*models.Scene, error)
|
FindByPath(ctx context.Context, p string) ([]*models.Scene, error)
|
||||||
Update(ctx context.Context, updatedScene *models.Scene) error
|
Update(ctx context.Context, updatedScene *models.Scene) error
|
||||||
AddGalleryIDs(ctx context.Context, sceneID int, galleryIDs []int) error
|
AddGalleryIDs(ctx context.Context, sceneID int, galleryIDs []int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageFinderUpdater interface {
|
type ScanImageFinderUpdater interface {
|
||||||
FindByZipFileID(ctx context.Context, zipFileID file.ID) ([]*models.Image, error)
|
FindByZipFileID(ctx context.Context, zipFileID models.FileID) ([]*models.Image, error)
|
||||||
UpdatePartial(ctx context.Context, id int, partial models.ImagePartial) (*models.Image, error)
|
UpdatePartial(ctx context.Context, id int, partial models.ImagePartial) (*models.Image, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScanHandler struct {
|
type ScanHandler struct {
|
||||||
CreatorUpdater FullCreatorUpdater
|
CreatorUpdater ScanCreatorUpdater
|
||||||
SceneFinderUpdater SceneFinderUpdater
|
SceneFinderUpdater ScanSceneFinderUpdater
|
||||||
ImageFinderUpdater ImageFinderUpdater
|
ImageFinderUpdater ScanImageFinderUpdater
|
||||||
PluginCache *plugin.Cache
|
PluginCache *plugin.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File) error {
|
func (h *ScanHandler) Handle(ctx context.Context, f models.File, oldFile models.File) error {
|
||||||
baseFile := f.Base()
|
baseFile := f.Base()
|
||||||
|
|
||||||
// try to match the file to a gallery
|
// try to match the file to a gallery
|
||||||
|
|
@ -83,7 +84,7 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File
|
||||||
|
|
||||||
logger.Infof("%s doesn't exist. Creating new gallery...", f.Base().Path)
|
logger.Infof("%s doesn't exist. Creating new gallery...", f.Base().Path)
|
||||||
|
|
||||||
if err := h.CreatorUpdater.Create(ctx, newGallery, []file.ID{baseFile.ID}); err != nil {
|
if err := h.CreatorUpdater.Create(ctx, newGallery, []models.FileID{baseFile.ID}); err != nil {
|
||||||
return fmt.Errorf("creating new gallery: %w", err)
|
return fmt.Errorf("creating new gallery: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,7 +113,7 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.Gallery, f file.File, updateExisting bool) error {
|
func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.Gallery, f models.File, updateExisting bool) error {
|
||||||
for _, i := range existing {
|
for _, i := range existing {
|
||||||
if err := i.LoadFiles(ctx, h.CreatorUpdater); err != nil {
|
if err := i.LoadFiles(ctx, h.CreatorUpdater); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -146,7 +147,7 @@ func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScanHandler) associateScene(ctx context.Context, existing []*models.Gallery, f file.File) error {
|
func (h *ScanHandler) associateScene(ctx context.Context, existing []*models.Gallery, f models.File) error {
|
||||||
galleryIDs := make([]int, len(existing))
|
galleryIDs := make([]int, len(existing))
|
||||||
for i, g := range existing {
|
for i, g := range existing {
|
||||||
galleryIDs[i] = g.ID
|
galleryIDs[i] = g.ID
|
||||||
|
|
|
||||||
|
|
@ -3,50 +3,25 @@ package gallery
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/image"
|
"github.com/stashapp/stash/pkg/image"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FinderByFile interface {
|
|
||||||
FindByFileID(ctx context.Context, fileID file.ID) ([]*models.Gallery, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Repository interface {
|
|
||||||
models.GalleryFinder
|
|
||||||
FinderByFile
|
|
||||||
Destroy(ctx context.Context, id int) error
|
|
||||||
models.FileLoader
|
|
||||||
ImageUpdater
|
|
||||||
PartialUpdater
|
|
||||||
}
|
|
||||||
|
|
||||||
type PartialUpdater interface {
|
|
||||||
UpdatePartial(ctx context.Context, id int, updatedGallery models.GalleryPartial) (*models.Gallery, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImageFinder interface {
|
type ImageFinder interface {
|
||||||
FindByFolderID(ctx context.Context, folder file.FolderID) ([]*models.Image, error)
|
FindByFolderID(ctx context.Context, folder models.FolderID) ([]*models.Image, error)
|
||||||
FindByZipFileID(ctx context.Context, zipFileID file.ID) ([]*models.Image, error)
|
FindByZipFileID(ctx context.Context, zipFileID models.FileID) ([]*models.Image, error)
|
||||||
models.GalleryIDLoader
|
models.GalleryIDLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageService interface {
|
type ImageService interface {
|
||||||
Destroy(ctx context.Context, i *models.Image, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) error
|
Destroy(ctx context.Context, i *models.Image, fileDeleter *image.FileDeleter, deleteGenerated, deleteFile bool) error
|
||||||
DestroyZipImages(ctx context.Context, zipFile file.File, fileDeleter *image.FileDeleter, deleteGenerated bool) ([]*models.Image, error)
|
DestroyZipImages(ctx context.Context, zipFile models.File, fileDeleter *image.FileDeleter, deleteGenerated bool) ([]*models.Image, error)
|
||||||
}
|
|
||||||
|
|
||||||
type ChapterRepository interface {
|
|
||||||
ChapterFinder
|
|
||||||
ChapterDestroyer
|
|
||||||
|
|
||||||
Update(ctx context.Context, updatedObject models.GalleryChapter) (*models.GalleryChapter, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
Repository Repository
|
Repository models.GalleryReaderWriter
|
||||||
ImageFinder ImageFinder
|
ImageFinder ImageFinder
|
||||||
ImageService ImageService
|
ImageService ImageService
|
||||||
File file.Store
|
File models.FileReaderWriter
|
||||||
Folder file.FolderStore
|
Folder models.FolderReaderWriter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ func (s *Service) RemoveImages(ctx context.Context, g *models.Gallery, toRemove
|
||||||
return s.Updated(ctx, g.ID)
|
return s.Updated(ctx, g.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddPerformer(ctx context.Context, qb PartialUpdater, o *models.Gallery, performerID int) error {
|
func AddPerformer(ctx context.Context, qb models.GalleryUpdater, o *models.Gallery, performerID int) error {
|
||||||
_, err := qb.UpdatePartial(ctx, o.ID, models.GalleryPartial{
|
_, err := qb.UpdatePartial(ctx, o.ID, models.GalleryPartial{
|
||||||
PerformerIDs: &models.UpdateIDs{
|
PerformerIDs: &models.UpdateIDs{
|
||||||
IDs: []int{performerID},
|
IDs: []int{performerID},
|
||||||
|
|
@ -64,7 +64,7 @@ func AddPerformer(ctx context.Context, qb PartialUpdater, o *models.Gallery, per
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddTag(ctx context.Context, qb PartialUpdater, o *models.Gallery, tagID int) error {
|
func AddTag(ctx context.Context, qb models.GalleryUpdater, o *models.Gallery, tagID int) error {
|
||||||
_, err := qb.UpdatePartial(ctx, o.ID, models.GalleryPartial{
|
_, err := qb.UpdatePartial(ctx, o.ID, models.GalleryPartial{
|
||||||
TagIDs: &models.UpdateIDs{
|
TagIDs: &models.UpdateIDs{
|
||||||
IDs: []int{tagID},
|
IDs: []int{tagID},
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import (
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg/transcoder"
|
"github.com/stashapp/stash/pkg/ffmpeg/transcoder"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -23,7 +23,7 @@ const (
|
||||||
rows = 5
|
rows = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
func Generate(encoder *ffmpeg.FFMpeg, videoFile *file.VideoFile) (*uint64, error) {
|
func Generate(encoder *ffmpeg.FFMpeg, videoFile *models.VideoFile) (*uint64, error) {
|
||||||
sprite, err := generateSprite(encoder, videoFile)
|
sprite, err := generateSprite(encoder, videoFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -76,7 +76,7 @@ func combineImages(images []image.Image) image.Image {
|
||||||
return montage
|
return montage
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateSprite(encoder *ffmpeg.FFMpeg, videoFile *file.VideoFile) (image.Image, error) {
|
func generateSprite(encoder *ffmpeg.FFMpeg, videoFile *models.VideoFile) (image.Image, error) {
|
||||||
logger.Infof("[generator] generating phash sprite for %s", videoFile.Path)
|
logger.Infof("[generator] generating phash sprite for %s", videoFile.Path)
|
||||||
|
|
||||||
// Generate sprite image offset by 5% on each end to avoid intro/outros
|
// Generate sprite image offset by 5% on each end to avoid intro/outros
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,6 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/models/paths"
|
"github.com/stashapp/stash/pkg/models/paths"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Destroyer interface {
|
|
||||||
Destroy(ctx context.Context, id int) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileDeleter is an extension of file.Deleter that handles deletion of image files.
|
// FileDeleter is an extension of file.Deleter that handles deletion of image files.
|
||||||
type FileDeleter struct {
|
type FileDeleter struct {
|
||||||
*file.Deleter
|
*file.Deleter
|
||||||
|
|
@ -45,7 +41,7 @@ func (s *Service) Destroy(ctx context.Context, i *models.Image, fileDeleter *Fil
|
||||||
|
|
||||||
// DestroyZipImages destroys all images in zip, optionally marking the files and generated files for deletion.
|
// DestroyZipImages destroys all images in zip, optionally marking the files and generated files for deletion.
|
||||||
// Returns a slice of images that were destroyed.
|
// Returns a slice of images that were destroyed.
|
||||||
func (s *Service) DestroyZipImages(ctx context.Context, zipFile file.File, fileDeleter *FileDeleter, deleteGenerated bool) ([]*models.Image, error) {
|
func (s *Service) DestroyZipImages(ctx context.Context, zipFile models.File, fileDeleter *FileDeleter, deleteGenerated bool) ([]*models.Image, error) {
|
||||||
var imgsDestroyed []*models.Image
|
var imgsDestroyed []*models.Image
|
||||||
|
|
||||||
imgs, err := s.Repository.FindByZipFileID(ctx, zipFile.Base().ID)
|
imgs, err := s.Repository.FindByZipFileID(ctx, zipFile.Base().ID)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/json"
|
"github.com/stashapp/stash/pkg/models/json"
|
||||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||||
"github.com/stashapp/stash/pkg/studio"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToBasicJSON converts a image object into its JSON object equivalent. It
|
// ToBasicJSON converts a image object into its JSON object equivalent. It
|
||||||
|
|
@ -53,7 +52,7 @@ func ToBasicJSON(image *models.Image) *jsonschema.Image {
|
||||||
|
|
||||||
// GetStudioName returns the name of the provided image's studio. It returns an
|
// GetStudioName returns the name of the provided image's studio. It returns an
|
||||||
// empty string if there is no studio assigned to the image.
|
// empty string if there is no studio assigned to the image.
|
||||||
func GetStudioName(ctx context.Context, reader studio.Finder, image *models.Image) (string, error) {
|
func GetStudioName(ctx context.Context, reader models.StudioGetter, image *models.Image) (string, error) {
|
||||||
if image.StudioID != nil {
|
if image.StudioID != nil {
|
||||||
studio, err := reader.Find(ctx, *image.StudioID)
|
studio, err := reader.Find(ctx, *image.StudioID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package image
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/json"
|
"github.com/stashapp/stash/pkg/models/json"
|
||||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||||
|
|
@ -45,8 +44,8 @@ var (
|
||||||
func createFullImage(id int) models.Image {
|
func createFullImage(id int) models.Image {
|
||||||
return models.Image{
|
return models.Image{
|
||||||
ID: id,
|
ID: id,
|
||||||
Files: models.NewRelatedFiles([]file.File{
|
Files: models.NewRelatedFiles([]models.File{
|
||||||
&file.BaseFile{
|
&models.BaseFile{
|
||||||
Path: path,
|
Path: path,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||||
"github.com/stashapp/stash/pkg/performer"
|
|
||||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
"github.com/stashapp/stash/pkg/studio"
|
|
||||||
"github.com/stashapp/stash/pkg/tag"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type GalleryFinder interface {
|
type GalleryFinder interface {
|
||||||
|
|
@ -19,18 +15,18 @@ type GalleryFinder interface {
|
||||||
FindUserGalleryByTitle(ctx context.Context, title string) ([]*models.Gallery, error)
|
FindUserGalleryByTitle(ctx context.Context, title string) ([]*models.Gallery, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FullCreatorUpdater interface {
|
type ImporterReaderWriter interface {
|
||||||
FinderCreatorUpdater
|
models.ImageCreatorUpdater
|
||||||
Update(ctx context.Context, updatedImage *models.Image) error
|
FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Image, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Importer struct {
|
type Importer struct {
|
||||||
ReaderWriter FullCreatorUpdater
|
ReaderWriter ImporterReaderWriter
|
||||||
FileFinder file.Getter
|
FileFinder models.FileFinder
|
||||||
StudioWriter studio.NameFinderCreator
|
StudioWriter models.StudioFinderCreator
|
||||||
GalleryFinder GalleryFinder
|
GalleryFinder GalleryFinder
|
||||||
PerformerWriter performer.NameFinderCreator
|
PerformerWriter models.PerformerFinderCreator
|
||||||
TagWriter tag.NameFinderCreator
|
TagWriter models.TagFinderCreator
|
||||||
Input jsonschema.Image
|
Input jsonschema.Image
|
||||||
MissingRefBehaviour models.ImportMissingRefEnum
|
MissingRefBehaviour models.ImportMissingRefEnum
|
||||||
|
|
||||||
|
|
@ -99,7 +95,7 @@ func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Importer) populateFiles(ctx context.Context) error {
|
func (i *Importer) populateFiles(ctx context.Context) error {
|
||||||
files := make([]file.File, 0)
|
files := make([]models.File, 0)
|
||||||
|
|
||||||
for _, ref := range i.Input.Files {
|
for _, ref := range i.Input.Files {
|
||||||
path := ref
|
path := ref
|
||||||
|
|
@ -330,7 +326,7 @@ func (i *Importer) FindExistingID(ctx context.Context) (*int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Importer) Create(ctx context.Context) (*int, error) {
|
func (i *Importer) Create(ctx context.Context) (*int, error) {
|
||||||
var fileIDs []file.ID
|
var fileIDs []models.FileID
|
||||||
for _, f := range i.image.Files.List() {
|
for _, f := range i.image.Files.List() {
|
||||||
fileIDs = append(fileIDs, f.Base().ID)
|
fileIDs = append(fileIDs, f.Base().ID)
|
||||||
}
|
}
|
||||||
|
|
@ -360,7 +356,7 @@ func (i *Importer) Update(ctx context.Context, id int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func importTags(ctx context.Context, tagWriter tag.NameFinderCreator, names []string, missingRefBehaviour models.ImportMissingRefEnum) ([]*models.Tag, error) {
|
func importTags(ctx context.Context, tagWriter models.TagFinderCreator, names []string, missingRefBehaviour models.ImportMissingRefEnum) ([]*models.Tag, error) {
|
||||||
tags, err := tagWriter.FindByNames(ctx, names, false)
|
tags, err := tagWriter.FindByNames(ctx, names, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -395,7 +391,7 @@ func importTags(ctx context.Context, tagWriter tag.NameFinderCreator, names []st
|
||||||
return tags, nil
|
return tags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTags(ctx context.Context, tagWriter tag.NameFinderCreator, names []string) ([]*models.Tag, error) {
|
func createTags(ctx context.Context, tagWriter models.TagCreator, names []string) ([]*models.Tag, error) {
|
||||||
var ret []*models.Tag
|
var ret []*models.Tag
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
newTag := models.NewTag(name)
|
newTag := models.NewTag(name)
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,6 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Queryer interface {
|
|
||||||
Query(ctx context.Context, options models.ImageQueryOptions) (*models.ImageQueryResult, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CountQueryer interface {
|
|
||||||
QueryCount(ctx context.Context, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryOptions returns a ImageQueryResult populated with the provided filters.
|
// QueryOptions returns a ImageQueryResult populated with the provided filters.
|
||||||
func QueryOptions(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType, count bool) models.ImageQueryOptions {
|
func QueryOptions(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType, count bool) models.ImageQueryOptions {
|
||||||
return models.ImageQueryOptions{
|
return models.ImageQueryOptions{
|
||||||
|
|
@ -27,7 +19,7 @@ func QueryOptions(imageFilter *models.ImageFilterType, findFilter *models.FindFi
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query queries for images using the provided filters.
|
// Query queries for images using the provided filters.
|
||||||
func Query(ctx context.Context, qb Queryer, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, error) {
|
func Query(ctx context.Context, qb models.ImageQueryer, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, error) {
|
||||||
result, err := qb.Query(ctx, QueryOptions(imageFilter, findFilter, false))
|
result, err := qb.Query(ctx, QueryOptions(imageFilter, findFilter, false))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -41,7 +33,7 @@ func Query(ctx context.Context, qb Queryer, imageFilter *models.ImageFilterType,
|
||||||
return images, nil
|
return images, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountByPerformerID(ctx context.Context, r CountQueryer, id int) (int, error) {
|
func CountByPerformerID(ctx context.Context, r models.ImageQueryer, id int) (int, error) {
|
||||||
filter := &models.ImageFilterType{
|
filter := &models.ImageFilterType{
|
||||||
Performers: &models.MultiCriterionInput{
|
Performers: &models.MultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(id)},
|
Value: []string{strconv.Itoa(id)},
|
||||||
|
|
@ -52,7 +44,7 @@ func CountByPerformerID(ctx context.Context, r CountQueryer, id int) (int, error
|
||||||
return r.QueryCount(ctx, filter, nil)
|
return r.QueryCount(ctx, filter, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountByStudioID(ctx context.Context, r CountQueryer, id int, depth *int) (int, error) {
|
func CountByStudioID(ctx context.Context, r models.ImageQueryer, id int, depth *int) (int, error) {
|
||||||
filter := &models.ImageFilterType{
|
filter := &models.ImageFilterType{
|
||||||
Studios: &models.HierarchicalMultiCriterionInput{
|
Studios: &models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(id)},
|
Value: []string{strconv.Itoa(id)},
|
||||||
|
|
@ -64,7 +56,7 @@ func CountByStudioID(ctx context.Context, r CountQueryer, id int, depth *int) (i
|
||||||
return r.QueryCount(ctx, filter, nil)
|
return r.QueryCount(ctx, filter, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountByTagID(ctx context.Context, r CountQueryer, id int, depth *int) (int, error) {
|
func CountByTagID(ctx context.Context, r models.ImageQueryer, id int, depth *int) (int, error) {
|
||||||
filter := &models.ImageFilterType{
|
filter := &models.ImageFilterType{
|
||||||
Tags: &models.HierarchicalMultiCriterionInput{
|
Tags: &models.HierarchicalMultiCriterionInput{
|
||||||
Value: []string{strconv.Itoa(id)},
|
Value: []string{strconv.Itoa(id)},
|
||||||
|
|
@ -76,7 +68,7 @@ func CountByTagID(ctx context.Context, r CountQueryer, id int, depth *int) (int,
|
||||||
return r.QueryCount(ctx, filter, nil)
|
return r.QueryCount(ctx, filter, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindByGalleryID(ctx context.Context, r Queryer, galleryID int, sortBy string, sortDir models.SortDirectionEnum) ([]*models.Image, error) {
|
func FindByGalleryID(ctx context.Context, r models.ImageQueryer, galleryID int, sortBy string, sortDir models.SortDirectionEnum) ([]*models.Image, error) {
|
||||||
perPage := -1
|
perPage := -1
|
||||||
|
|
||||||
findFilter := models.FindFilterType{
|
findFilter := models.FindFilterType{
|
||||||
|
|
@ -99,7 +91,7 @@ func FindByGalleryID(ctx context.Context, r Queryer, galleryID int, sortBy strin
|
||||||
}, &findFilter)
|
}, &findFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindGalleryCover(ctx context.Context, r Queryer, galleryID int, galleryCoverRegex string) (*models.Image, error) {
|
func FindGalleryCover(ctx context.Context, r models.ImageQueryer, galleryID int, galleryCoverRegex string) (*models.Image, error) {
|
||||||
const useCoverJpg = true
|
const useCoverJpg = true
|
||||||
img, err := findGalleryCover(ctx, r, galleryID, useCoverJpg, galleryCoverRegex)
|
img, err := findGalleryCover(ctx, r, galleryID, useCoverJpg, galleryCoverRegex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -114,7 +106,7 @@ func FindGalleryCover(ctx context.Context, r Queryer, galleryID int, galleryCove
|
||||||
return findGalleryCover(ctx, r, galleryID, !useCoverJpg, galleryCoverRegex)
|
return findGalleryCover(ctx, r, galleryID, !useCoverJpg, galleryCoverRegex)
|
||||||
}
|
}
|
||||||
|
|
||||||
func findGalleryCover(ctx context.Context, r Queryer, galleryID int, useCoverJpg bool, galleryCoverRegex string) (*models.Image, error) {
|
func findGalleryCover(ctx context.Context, r models.ImageQueryer, galleryID int, useCoverJpg bool, galleryCoverRegex string) (*models.Image, error) {
|
||||||
// try to find cover.jpg in the gallery
|
// try to find cover.jpg in the gallery
|
||||||
perPage := 1
|
perPage := 1
|
||||||
sortBy := "path"
|
sortBy := "path"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/paths"
|
"github.com/stashapp/stash/pkg/models/paths"
|
||||||
|
|
@ -21,21 +20,22 @@ var (
|
||||||
ErrNotImageFile = errors.New("not an image file")
|
ErrNotImageFile = errors.New("not an image file")
|
||||||
)
|
)
|
||||||
|
|
||||||
type FinderCreatorUpdater interface {
|
type ScanCreatorUpdater interface {
|
||||||
FindByFileID(ctx context.Context, fileID file.ID) ([]*models.Image, error)
|
FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Image, error)
|
||||||
FindByFolderID(ctx context.Context, folderID file.FolderID) ([]*models.Image, error)
|
FindByFolderID(ctx context.Context, folderID models.FolderID) ([]*models.Image, error)
|
||||||
FindByFingerprints(ctx context.Context, fp []file.Fingerprint) ([]*models.Image, error)
|
FindByFingerprints(ctx context.Context, fp []models.Fingerprint) ([]*models.Image, error)
|
||||||
|
GetFiles(ctx context.Context, relatedID int) ([]models.File, error)
|
||||||
|
GetGalleryIDs(ctx context.Context, relatedID int) ([]int, error)
|
||||||
|
|
||||||
Create(ctx context.Context, newImage *models.ImageCreateInput) error
|
Create(ctx context.Context, newImage *models.ImageCreateInput) error
|
||||||
UpdatePartial(ctx context.Context, id int, updatedImage models.ImagePartial) (*models.Image, error)
|
UpdatePartial(ctx context.Context, id int, updatedImage models.ImagePartial) (*models.Image, error)
|
||||||
AddFileID(ctx context.Context, id int, fileID file.ID) error
|
AddFileID(ctx context.Context, id int, fileID models.FileID) error
|
||||||
models.GalleryIDLoader
|
|
||||||
models.FileLoader
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GalleryFinderCreator interface {
|
type GalleryFinderCreator interface {
|
||||||
FindByFileID(ctx context.Context, fileID file.ID) ([]*models.Gallery, error)
|
FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Gallery, error)
|
||||||
FindByFolderID(ctx context.Context, folderID file.FolderID) ([]*models.Gallery, error)
|
FindByFolderID(ctx context.Context, folderID models.FolderID) ([]*models.Gallery, error)
|
||||||
Create(ctx context.Context, newObject *models.Gallery, fileIDs []file.ID) error
|
Create(ctx context.Context, newObject *models.Gallery, fileIDs []models.FileID) error
|
||||||
UpdatePartial(ctx context.Context, id int, updatedGallery models.GalleryPartial) (*models.Gallery, error)
|
UpdatePartial(ctx context.Context, id int, updatedGallery models.GalleryPartial) (*models.Gallery, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,11 +44,11 @@ type ScanConfig interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScanGenerator interface {
|
type ScanGenerator interface {
|
||||||
Generate(ctx context.Context, i *models.Image, f file.File) error
|
Generate(ctx context.Context, i *models.Image, f models.File) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScanHandler struct {
|
type ScanHandler struct {
|
||||||
CreatorUpdater FinderCreatorUpdater
|
CreatorUpdater ScanCreatorUpdater
|
||||||
GalleryFinder GalleryFinderCreator
|
GalleryFinder GalleryFinderCreator
|
||||||
|
|
||||||
ScanGenerator ScanGenerator
|
ScanGenerator ScanGenerator
|
||||||
|
|
@ -80,7 +80,7 @@ func (h *ScanHandler) validate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File) error {
|
func (h *ScanHandler) Handle(ctx context.Context, f models.File, oldFile models.File) error {
|
||||||
if err := h.validate(); err != nil {
|
if err := h.validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +130,7 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File
|
||||||
|
|
||||||
if err := h.CreatorUpdater.Create(ctx, &models.ImageCreateInput{
|
if err := h.CreatorUpdater.Create(ctx, &models.ImageCreateInput{
|
||||||
Image: newImage,
|
Image: newImage,
|
||||||
FileIDs: []file.ID{imageFile.ID},
|
FileIDs: []models.FileID{imageFile.ID},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("creating new image: %w", err)
|
return fmt.Errorf("creating new image: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -151,8 +151,8 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File
|
||||||
|
|
||||||
// remove the old thumbnail if the checksum changed - we'll regenerate it
|
// remove the old thumbnail if the checksum changed - we'll regenerate it
|
||||||
if oldFile != nil {
|
if oldFile != nil {
|
||||||
oldHash := oldFile.Base().Fingerprints.GetString(file.FingerprintTypeMD5)
|
oldHash := oldFile.Base().Fingerprints.GetString(models.FingerprintTypeMD5)
|
||||||
newHash := f.Base().Fingerprints.GetString(file.FingerprintTypeMD5)
|
newHash := f.Base().Fingerprints.GetString(models.FingerprintTypeMD5)
|
||||||
|
|
||||||
if oldHash != "" && newHash != "" && oldHash != newHash {
|
if oldHash != "" && newHash != "" && oldHash != newHash {
|
||||||
// remove cache dir of gallery
|
// remove cache dir of gallery
|
||||||
|
|
@ -173,7 +173,7 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File, oldFile file.File
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.Image, f *file.BaseFile, updateExisting bool) error {
|
func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.Image, f *models.BaseFile, updateExisting bool) error {
|
||||||
for _, i := range existing {
|
for _, i := range existing {
|
||||||
if err := i.LoadFiles(ctx, h.CreatorUpdater); err != nil {
|
if err := i.LoadFiles(ctx, h.CreatorUpdater); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -239,7 +239,7 @@ func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScanHandler) getOrCreateFolderBasedGallery(ctx context.Context, f file.File) (*models.Gallery, error) {
|
func (h *ScanHandler) getOrCreateFolderBasedGallery(ctx context.Context, f models.File) (*models.Gallery, error) {
|
||||||
folderID := f.Base().ParentFolderID
|
folderID := f.Base().ParentFolderID
|
||||||
g, err := h.GalleryFinder.FindByFolderID(ctx, folderID)
|
g, err := h.GalleryFinder.FindByFolderID(ctx, folderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -299,7 +299,7 @@ func (h *ScanHandler) associateFolderImages(ctx context.Context, g *models.Galle
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScanHandler) getOrCreateZipBasedGallery(ctx context.Context, zipFile file.File) (*models.Gallery, error) {
|
func (h *ScanHandler) getOrCreateZipBasedGallery(ctx context.Context, zipFile models.File) (*models.Gallery, error) {
|
||||||
g, err := h.GalleryFinder.FindByFileID(ctx, zipFile.Base().ID)
|
g, err := h.GalleryFinder.FindByFileID(ctx, zipFile.Base().ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("finding zip based gallery: %w", err)
|
return nil, fmt.Errorf("finding zip based gallery: %w", err)
|
||||||
|
|
@ -319,7 +319,7 @@ func (h *ScanHandler) getOrCreateZipBasedGallery(ctx context.Context, zipFile fi
|
||||||
|
|
||||||
logger.Infof("%s doesn't exist. Creating new gallery...", zipFile.Base().Path)
|
logger.Infof("%s doesn't exist. Creating new gallery...", zipFile.Base().Path)
|
||||||
|
|
||||||
if err := h.GalleryFinder.Create(ctx, newGallery, []file.ID{zipFile.Base().ID}); err != nil {
|
if err := h.GalleryFinder.Create(ctx, newGallery, []models.FileID{zipFile.Base().ID}); err != nil {
|
||||||
return nil, fmt.Errorf("creating zip-based gallery: %w", err)
|
return nil, fmt.Errorf("creating zip-based gallery: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,7 +328,7 @@ func (h *ScanHandler) getOrCreateZipBasedGallery(ctx context.Context, zipFile fi
|
||||||
return newGallery, nil
|
return newGallery, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScanHandler) getOrCreateGallery(ctx context.Context, f file.File) (*models.Gallery, error) {
|
func (h *ScanHandler) getOrCreateGallery(ctx context.Context, f models.File) (*models.Gallery, error) {
|
||||||
// don't create folder-based galleries for files in zip file
|
// don't create folder-based galleries for files in zip file
|
||||||
if f.Base().ZipFile != nil {
|
if f.Base().ZipFile != nil {
|
||||||
return h.getOrCreateZipBasedGallery(ctx, f.Base().ZipFile)
|
return h.getOrCreateZipBasedGallery(ctx, f.Base().ZipFile)
|
||||||
|
|
@ -357,7 +357,7 @@ func (h *ScanHandler) getOrCreateGallery(ctx context.Context, f file.File) (*mod
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *ScanHandler) getGalleryToAssociate(ctx context.Context, newImage *models.Image, f file.File) (*models.Gallery, error) {
|
func (h *ScanHandler) getGalleryToAssociate(ctx context.Context, newImage *models.Image, f models.File) (*models.Gallery, error) {
|
||||||
g, err := h.getOrCreateGallery(ctx, f)
|
g, err := h.getOrCreateGallery(ctx, f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,10 @@
|
||||||
package image
|
package image
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FinderByFile interface {
|
|
||||||
FindByFileID(ctx context.Context, fileID file.ID) ([]*models.Image, error)
|
|
||||||
FindByZipFileID(ctx context.Context, zipFileID file.ID) ([]*models.Image, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Repository interface {
|
|
||||||
FinderByFile
|
|
||||||
Destroyer
|
|
||||||
models.FileLoader
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
File file.Store
|
File models.FileReaderWriter
|
||||||
Repository Repository
|
Repository models.ImageReaderWriter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/ffmpeg/transcoder"
|
"github.com/stashapp/stash/pkg/ffmpeg/transcoder"
|
||||||
"github.com/stashapp/stash/pkg/file"
|
"github.com/stashapp/stash/pkg/file"
|
||||||
"github.com/stashapp/stash/pkg/fsutil"
|
"github.com/stashapp/stash/pkg/fsutil"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ffmpegImageQuality = 5
|
const ffmpegImageQuality = 5
|
||||||
|
|
@ -68,7 +69,7 @@ func NewThumbnailEncoder(ffmpegEncoder *ffmpeg.FFMpeg, ffProbe ffmpeg.FFProbe, c
|
||||||
// the provided max size. It resizes based on the largest X/Y direction.
|
// 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
|
// It returns nil and an error if an error occurs reading, decoding or encoding
|
||||||
// the image, or if the image is not suitable for thumbnails.
|
// the image, or if the image is not suitable for thumbnails.
|
||||||
func (e *ThumbnailEncoder) GetThumbnail(f file.File, maxSize int) ([]byte, error) {
|
func (e *ThumbnailEncoder) GetThumbnail(f models.File, maxSize int) ([]byte, error) {
|
||||||
reader, err := f.Open(&file.OsFS{})
|
reader, err := f.Open(&file.OsFS{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -82,7 +83,7 @@ func (e *ThumbnailEncoder) GetThumbnail(f file.File, maxSize int) ([]byte, error
|
||||||
|
|
||||||
data := buf.Bytes()
|
data := buf.Bytes()
|
||||||
|
|
||||||
if imageFile, ok := f.(*file.ImageFile); ok {
|
if imageFile, ok := f.(*models.ImageFile); ok {
|
||||||
format := imageFile.Format
|
format := imageFile.Format
|
||||||
animated := imageFile.Format == formatGif
|
animated := imageFile.Format == formatGif
|
||||||
|
|
||||||
|
|
@ -98,7 +99,7 @@ func (e *ThumbnailEncoder) GetThumbnail(f file.File, maxSize int) ([]byte, error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Videofiles can only be thumbnailed with ffmpeg
|
// Videofiles can only be thumbnailed with ffmpeg
|
||||||
if _, ok := f.(*file.VideoFile); ok {
|
if _, ok := f.(*models.VideoFile); ok {
|
||||||
return e.ffmpegImageThumbnail(buf, maxSize)
|
return e.ffmpegImageThumbnail(buf, maxSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,7 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PartialUpdater interface {
|
func AddPerformer(ctx context.Context, qb models.ImageUpdater, i *models.Image, performerID int) error {
|
||||||
UpdatePartial(ctx context.Context, id int, partial models.ImagePartial) (*models.Image, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddPerformer(ctx context.Context, qb PartialUpdater, i *models.Image, performerID int) error {
|
|
||||||
_, err := qb.UpdatePartial(ctx, i.ID, models.ImagePartial{
|
_, err := qb.UpdatePartial(ctx, i.ID, models.ImagePartial{
|
||||||
PerformerIDs: &models.UpdateIDs{
|
PerformerIDs: &models.UpdateIDs{
|
||||||
IDs: []int{performerID},
|
IDs: []int{performerID},
|
||||||
|
|
@ -21,7 +17,7 @@ func AddPerformer(ctx context.Context, qb PartialUpdater, i *models.Image, perfo
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddTag(ctx context.Context, qb PartialUpdater, i *models.Image, tagID int) error {
|
func AddTag(ctx context.Context, qb models.ImageUpdater, i *models.Image, tagID int) error {
|
||||||
_, err := qb.UpdatePartial(ctx, i.ID, models.ImagePartial{
|
_, err := qb.UpdatePartial(ctx, i.ID, models.ImagePartial{
|
||||||
TagIDs: &models.UpdateIDs{
|
TagIDs: &models.UpdateIDs{
|
||||||
IDs: []int{tagID},
|
IDs: []int{tagID},
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ type Cache struct {
|
||||||
// against. This means that performers with single-letter words in their names could potentially
|
// against. This means that performers with single-letter words in their names could potentially
|
||||||
// be missed.
|
// be missed.
|
||||||
// This query is expensive, so it's queried once and cached, if the cache if provided.
|
// This query is expensive, so it's queried once and cached, if the cache if provided.
|
||||||
func getSingleLetterPerformers(ctx context.Context, c *Cache, reader PerformerAutoTagQueryer) ([]*models.Performer, error) {
|
func getSingleLetterPerformers(ctx context.Context, c *Cache, reader models.PerformerAutoTagQueryer) ([]*models.Performer, error) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
c = &Cache{}
|
c = &Cache{}
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ func getSingleLetterPerformers(ctx context.Context, c *Cache, reader PerformerAu
|
||||||
|
|
||||||
// getSingleLetterStudios returns all studios with names that start with single character words.
|
// getSingleLetterStudios returns all studios with names that start with single character words.
|
||||||
// See getSingleLetterPerformers for details.
|
// See getSingleLetterPerformers for details.
|
||||||
func getSingleLetterStudios(ctx context.Context, c *Cache, reader StudioAutoTagQueryer) ([]*models.Studio, error) {
|
func getSingleLetterStudios(ctx context.Context, c *Cache, reader models.StudioAutoTagQueryer) ([]*models.Studio, error) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
c = &Cache{}
|
c = &Cache{}
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +86,7 @@ func getSingleLetterStudios(ctx context.Context, c *Cache, reader StudioAutoTagQ
|
||||||
|
|
||||||
// getSingleLetterTags returns all tags with names that start with single character words.
|
// getSingleLetterTags returns all tags with names that start with single character words.
|
||||||
// See getSingleLetterPerformers for details.
|
// See getSingleLetterPerformers for details.
|
||||||
func getSingleLetterTags(ctx context.Context, c *Cache, reader TagAutoTagQueryer) ([]*models.Tag, error) {
|
func getSingleLetterTags(ctx context.Context, c *Cache, reader models.TagAutoTagQueryer) ([]*models.Tag, error) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
c = &Cache{}
|
c = &Cache{}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
"github.com/stashapp/stash/pkg/scene"
|
||||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
"github.com/stashapp/stash/pkg/studio"
|
|
||||||
"github.com/stashapp/stash/pkg/tag"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -28,24 +26,6 @@ const (
|
||||||
|
|
||||||
var separatorRE = regexp.MustCompile(separatorPattern)
|
var separatorRE = regexp.MustCompile(separatorPattern)
|
||||||
|
|
||||||
type PerformerAutoTagQueryer interface {
|
|
||||||
Query(ctx context.Context, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error)
|
|
||||||
QueryForAutoTag(ctx context.Context, words []string) ([]*models.Performer, error)
|
|
||||||
models.AliasLoader
|
|
||||||
}
|
|
||||||
|
|
||||||
type StudioAutoTagQueryer interface {
|
|
||||||
QueryForAutoTag(ctx context.Context, words []string) ([]*models.Studio, error)
|
|
||||||
studio.Queryer
|
|
||||||
GetAliases(ctx context.Context, studioID int) ([]string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TagAutoTagQueryer interface {
|
|
||||||
QueryForAutoTag(ctx context.Context, words []string) ([]*models.Tag, error)
|
|
||||||
tag.Queryer
|
|
||||||
GetAliases(ctx context.Context, tagID int) ([]string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPathQueryRegex(name string) string {
|
func getPathQueryRegex(name string) string {
|
||||||
// escape specific regex characters
|
// escape specific regex characters
|
||||||
name = regexp.QuoteMeta(name)
|
name = regexp.QuoteMeta(name)
|
||||||
|
|
@ -146,7 +126,7 @@ func regexpMatchesPath(r *regexp.Regexp, path string) int {
|
||||||
return found[len(found)-1][0]
|
return found[len(found)-1][0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPerformers(ctx context.Context, words []string, performerReader PerformerAutoTagQueryer, cache *Cache) ([]*models.Performer, error) {
|
func getPerformers(ctx context.Context, words []string, performerReader models.PerformerAutoTagQueryer, cache *Cache) ([]*models.Performer, error) {
|
||||||
performers, err := performerReader.QueryForAutoTag(ctx, words)
|
performers, err := performerReader.QueryForAutoTag(ctx, words)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -160,7 +140,7 @@ func getPerformers(ctx context.Context, words []string, performerReader Performe
|
||||||
return append(performers, swPerformers...), nil
|
return append(performers, swPerformers...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PathToPerformers(ctx context.Context, path string, reader PerformerAutoTagQueryer, cache *Cache, trimExt bool) ([]*models.Performer, error) {
|
func PathToPerformers(ctx context.Context, path string, reader models.PerformerAutoTagQueryer, cache *Cache, trimExt bool) ([]*models.Performer, error) {
|
||||||
words := getPathWords(path, trimExt)
|
words := getPathWords(path, trimExt)
|
||||||
|
|
||||||
performers, err := getPerformers(ctx, words, reader, cache)
|
performers, err := getPerformers(ctx, words, reader, cache)
|
||||||
|
|
@ -198,7 +178,7 @@ func PathToPerformers(ctx context.Context, path string, reader PerformerAutoTagQ
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStudios(ctx context.Context, words []string, reader StudioAutoTagQueryer, cache *Cache) ([]*models.Studio, error) {
|
func getStudios(ctx context.Context, words []string, reader models.StudioAutoTagQueryer, cache *Cache) ([]*models.Studio, error) {
|
||||||
studios, err := reader.QueryForAutoTag(ctx, words)
|
studios, err := reader.QueryForAutoTag(ctx, words)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -215,7 +195,7 @@ func getStudios(ctx context.Context, words []string, reader StudioAutoTagQueryer
|
||||||
// PathToStudio returns the Studio that matches the given path.
|
// PathToStudio returns the Studio that matches the given path.
|
||||||
// Where multiple matching studios are found, the one that matches the latest
|
// Where multiple matching studios are found, the one that matches the latest
|
||||||
// position in the path is returned.
|
// position in the path is returned.
|
||||||
func PathToStudio(ctx context.Context, path string, reader StudioAutoTagQueryer, cache *Cache, trimExt bool) (*models.Studio, error) {
|
func PathToStudio(ctx context.Context, path string, reader models.StudioAutoTagQueryer, cache *Cache, trimExt bool) (*models.Studio, error) {
|
||||||
words := getPathWords(path, trimExt)
|
words := getPathWords(path, trimExt)
|
||||||
candidates, err := getStudios(ctx, words, reader, cache)
|
candidates, err := getStudios(ctx, words, reader, cache)
|
||||||
|
|
||||||
|
|
@ -249,7 +229,7 @@ func PathToStudio(ctx context.Context, path string, reader StudioAutoTagQueryer,
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTags(ctx context.Context, words []string, reader TagAutoTagQueryer, cache *Cache) ([]*models.Tag, error) {
|
func getTags(ctx context.Context, words []string, reader models.TagAutoTagQueryer, cache *Cache) ([]*models.Tag, error) {
|
||||||
tags, err := reader.QueryForAutoTag(ctx, words)
|
tags, err := reader.QueryForAutoTag(ctx, words)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -263,7 +243,7 @@ func getTags(ctx context.Context, words []string, reader TagAutoTagQueryer, cach
|
||||||
return append(tags, swTags...), nil
|
return append(tags, swTags...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PathToTags(ctx context.Context, path string, reader TagAutoTagQueryer, cache *Cache, trimExt bool) ([]*models.Tag, error) {
|
func PathToTags(ctx context.Context, path string, reader models.TagAutoTagQueryer, cache *Cache, trimExt bool) ([]*models.Tag, error) {
|
||||||
words := getPathWords(path, trimExt)
|
words := getPathWords(path, trimExt)
|
||||||
tags, err := getTags(ctx, words, reader, cache)
|
tags, err := getTags(ctx, words, reader, cache)
|
||||||
|
|
||||||
|
|
@ -299,7 +279,7 @@ func PathToTags(ctx context.Context, path string, reader TagAutoTagQueryer, cach
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PathToScenesFn(ctx context.Context, name string, paths []string, sceneReader scene.Queryer, fn func(ctx context.Context, scene *models.Scene) error) error {
|
func PathToScenesFn(ctx context.Context, name string, paths []string, sceneReader models.SceneQueryer, fn func(ctx context.Context, scene *models.Scene) error) error {
|
||||||
regex := getPathQueryRegex(name)
|
regex := getPathQueryRegex(name)
|
||||||
organized := false
|
organized := false
|
||||||
filter := models.SceneFilterType{
|
filter := models.SceneFilterType{
|
||||||
|
|
@ -358,7 +338,7 @@ func PathToScenesFn(ctx context.Context, name string, paths []string, sceneReade
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PathToImagesFn(ctx context.Context, name string, paths []string, imageReader image.Queryer, fn func(ctx context.Context, scene *models.Image) error) error {
|
func PathToImagesFn(ctx context.Context, name string, paths []string, imageReader models.ImageQueryer, fn func(ctx context.Context, scene *models.Image) error) error {
|
||||||
regex := getPathQueryRegex(name)
|
regex := getPathQueryRegex(name)
|
||||||
organized := false
|
organized := false
|
||||||
filter := models.ImageFilterType{
|
filter := models.ImageFilterType{
|
||||||
|
|
@ -417,7 +397,7 @@ func PathToImagesFn(ctx context.Context, name string, paths []string, imageReade
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func PathToGalleriesFn(ctx context.Context, name string, paths []string, galleryReader gallery.Queryer, fn func(ctx context.Context, scene *models.Gallery) error) error {
|
func PathToGalleriesFn(ctx context.Context, name string, paths []string, galleryReader models.GalleryQueryer, fn func(ctx context.Context, scene *models.Gallery) error) error {
|
||||||
regex := getPathQueryRegex(name)
|
regex := getPathQueryRegex(name)
|
||||||
organized := false
|
organized := false
|
||||||
filter := models.GalleryFilterType{
|
filter := models.GalleryFilterType{
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ func ScrapedPerformer(ctx context.Context, qb PerformerFinder, p *models.Scraped
|
||||||
}
|
}
|
||||||
|
|
||||||
type StudioFinder interface {
|
type StudioFinder interface {
|
||||||
studio.Queryer
|
models.StudioQueryer
|
||||||
FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Studio, error)
|
FindByStashID(ctx context.Context, stashID models.StashID) ([]*models.Studio, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,7 +134,7 @@ func ScrapedMovie(ctx context.Context, qb MovieNamesFinder, m *models.ScrapedMov
|
||||||
|
|
||||||
// ScrapedTag matches the provided tag with the tags
|
// ScrapedTag matches the provided tag with the tags
|
||||||
// in the database and sets the ID field if one is found.
|
// in the database and sets the ID field if one is found.
|
||||||
func ScrapedTag(ctx context.Context, qb tag.Queryer, s *models.ScrapedTag) error {
|
func ScrapedTag(ctx context.Context, qb models.TagQueryer, s *models.ScrapedTag) error {
|
||||||
if s.StoredID != nil {
|
if s.StoredID != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/file"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileQueryOptions struct {
|
type FileQueryOptions struct {
|
||||||
|
|
@ -57,24 +55,24 @@ func PathsFileFilter(paths []string) *FileFilterType {
|
||||||
type FileQueryResult struct {
|
type FileQueryResult struct {
|
||||||
// can't use QueryResult because id type is wrong
|
// can't use QueryResult because id type is wrong
|
||||||
|
|
||||||
IDs []file.ID
|
IDs []FileID
|
||||||
Count int
|
Count int
|
||||||
|
|
||||||
finder file.Finder
|
getter FileGetter
|
||||||
files []file.File
|
files []File
|
||||||
resolveErr error
|
resolveErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileQueryResult(finder file.Finder) *FileQueryResult {
|
func NewFileQueryResult(fileGetter FileGetter) *FileQueryResult {
|
||||||
return &FileQueryResult{
|
return &FileQueryResult{
|
||||||
finder: finder,
|
getter: fileGetter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FileQueryResult) Resolve(ctx context.Context) ([]file.File, error) {
|
func (r *FileQueryResult) Resolve(ctx context.Context) ([]File, error) {
|
||||||
// cache results
|
// cache results
|
||||||
if r.files == nil && r.resolveErr == nil {
|
if r.files == nil && r.resolveErr == nil {
|
||||||
r.files, r.resolveErr = r.finder.Find(ctx, r.IDs...)
|
r.files, r.resolveErr = r.getter.Find(ctx, r.IDs...)
|
||||||
}
|
}
|
||||||
return r.files, r.resolveErr
|
return r.files, r.resolveErr
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
package file
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
FingerprintTypeOshash = "oshash"
|
FingerprintTypeOshash = "oshash"
|
||||||
|
|
@ -12,6 +17,15 @@ type Fingerprint struct {
|
||||||
Fingerprint interface{}
|
Fingerprint interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fingerprint) Value() string {
|
||||||
|
switch v := f.Fingerprint.(type) {
|
||||||
|
case int64:
|
||||||
|
return strconv.FormatUint(uint64(v), 16)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", f.Fingerprint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Fingerprints []Fingerprint
|
type Fingerprints []Fingerprint
|
||||||
|
|
||||||
func (f *Fingerprints) Remove(type_ string) {
|
func (f *Fingerprints) Remove(type_ string) {
|
||||||
|
|
@ -114,8 +128,3 @@ func (f Fingerprints) AppendUnique(o Fingerprint) Fingerprints {
|
||||||
|
|
||||||
return append(f, o)
|
return append(f, o)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FingerprintCalculator calculates a fingerprint for the provided file.
|
|
||||||
type FingerprintCalculator interface {
|
|
||||||
CalculateFingerprints(f *BaseFile, o Opener, useExisting bool) ([]Fingerprint, error)
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package file
|
package models
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue