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:
DingDongSoLong4 2023-09-01 02:39:29 +02:00 committed by GitHub
parent 20520a58b4
commit c364346a59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
185 changed files with 3840 additions and 2559 deletions

4
.mockery.yml Normal file
View file

@ -0,0 +1,4 @@
dir: ./pkg/models
name: ".*ReaderWriter"
outpkg: mocks
output: ./pkg/models/mocks

View file

@ -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

View file

@ -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,
} }

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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 {

View file

@ -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) {

View file

@ -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)),
ModTime: f.ModTime,
Size: f.Size,
Width: f.Width,
Height: f.Height,
CreatedAt: f.CreatedAt,
UpdatedAt: f.UpdatedAt,
Fingerprints: resolveFingerprints(f.Base()),
} }
return vf, nil
if f.ZipFileID != nil {
zipFileID := strconv.Itoa(int(*f.ZipFileID))
ret.ZipFileID = &zipFileID
}
return ret
} }
func (r *imageResolver) getPrimaryFile(ctx context.Context, obj *models.Image) (file.VisualFile, error) { func (r *imageResolver) getPrimaryFile(ctx context.Context, obj *models.Image) (models.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

View file

@ -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)),
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()),
} }
return vf, nil
if f.ZipFileID != nil {
zipFileID := strconv.Itoa(int(*f.ZipFileID))
ret.ZipFileID = &zipFileID
}
return ret
} }
func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (*file.VideoFile, error) { func (r *sceneResolver) getPrimaryFile(ctx context.Context, obj *models.Scene) (*models.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
} }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)
} }

View file

@ -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 {

View file

@ -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)
} }

View file

@ -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)
} }

View file

@ -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)
} }

View file

@ -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)
} }

View file

@ -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,

View file

@ -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 {

View file

@ -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) {

View file

@ -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) {

View file

@ -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())
} }

View file

@ -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 {

View file

@ -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) {

View file

@ -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)

View file

@ -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 {

View file

@ -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)

View file

@ -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
} }

View file

@ -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 {

View file

@ -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{

View file

@ -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

View file

@ -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 {

View file

@ -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{},

View file

@ -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
} }

View file

@ -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)
} }

View file

@ -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),
} }

View file

@ -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 {

View file

@ -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{

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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},

View file

@ -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
} }

View file

@ -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)

View file

@ -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
} }

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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)
} }

View file

@ -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
} }

View file

@ -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

View file

@ -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 {

View file

@ -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
} }

View file

@ -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

View file

@ -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)
} }

View file

@ -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
}

View file

@ -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

View file

@ -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
} }

View file

@ -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:

View file

@ -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
}

View file

@ -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
} }

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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
} }

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)
} }

View file

@ -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)

View file

@ -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,
}, },
}), }),

View file

@ -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)
} }

View file

@ -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,

View file

@ -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)},

View file

@ -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

View file

@ -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
} }

View file

@ -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},

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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,
}, },
}), }),

View file

@ -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)

View file

@ -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"

View file

@ -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

View file

@ -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
} }

View file

@ -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)
} }

View file

@ -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},

View file

@ -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{}
} }

View file

@ -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{

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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)
}

View file

@ -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