mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 00:13:46 +01:00
Manager refactor, part 1 (#4298)
* Move BackupDatabase and AnonymiseDatabase to internal/manager * Rename config.Instance to config.Config * Rename FFMPEG * Rework manager and initialization process * Fix Makefile * Tweak phasher * Fix config races * Fix setup error not clearing
This commit is contained in:
parent
fc1fc20df4
commit
b78771dbcd
45 changed files with 1230 additions and 1213 deletions
26
Makefile
26
Makefile
|
|
@ -9,9 +9,11 @@ endif
|
|||
ifdef IS_WIN_SHELL
|
||||
RM := del /s /q
|
||||
RMDIR := rmdir /s /q
|
||||
NOOP := @@
|
||||
else
|
||||
RM := rm -f
|
||||
RMDIR := rm -rf
|
||||
NOOP := @:
|
||||
endif
|
||||
|
||||
# set LDFLAGS environment variable to any extra ldflags required
|
||||
|
|
@ -54,38 +56,36 @@ release: pre-ui generate ui build-release
|
|||
# for a static-pie release build: `make flags-static-pie flags-release stash`
|
||||
# for a static windows debug build: `make flags-static-windows stash`
|
||||
|
||||
# shell noop: prevents "nothing to be done" warnings
|
||||
.PHONY: flags
|
||||
flags:
|
||||
ifdef IS_WIN_SHELL
|
||||
@@
|
||||
else
|
||||
@:
|
||||
endif
|
||||
# $(NOOP) prevents "nothing to be done" warnings
|
||||
|
||||
.PHONY: flags-release
|
||||
flags-release: flags
|
||||
flags-release:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -s -w)
|
||||
$(eval GO_BUILD_FLAGS += -trimpath)
|
||||
|
||||
.PHONY: flags-pie
|
||||
flags-pie: flags
|
||||
flags-pie:
|
||||
$(NOOP)
|
||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||
|
||||
.PHONY: flags-static
|
||||
flags-static: flags
|
||||
flags-static:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -extldflags=-static)
|
||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
|
||||
|
||||
.PHONY: flags-static-pie
|
||||
flags-static-pie: flags
|
||||
flags-static-pie:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -extldflags=-static-pie)
|
||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo netgo)
|
||||
|
||||
# identical to flags-static-pie, but excluding netgo, which is not needed on windows
|
||||
.PHONY: flags-static-windows
|
||||
flags-static-windows: flags
|
||||
flags-static-windows:
|
||||
$(NOOP)
|
||||
$(eval LDFLAGS += -extldflags=-static-pie)
|
||||
$(eval GO_BUILD_FLAGS += -buildmode=pie)
|
||||
$(eval GO_BUILD_TAGS += sqlite_omit_load_extension osusergo)
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ On Windows or macOS, running the app might present a security prompt since the b
|
|||
|
||||
On Windows, bypass this by clicking "more info" and then the "run anyway" button. On macOS, Control+Click the app, click "Open", and then "Open" again.
|
||||
|
||||
#### FFMPEG
|
||||
Stash requires ffmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager.
|
||||
#### FFmpeg
|
||||
Stash requires FFmpeg. If you don't have it installed, Stash will download a copy for you. It is recommended that Linux users install `ffmpeg` from their distro's package manager.
|
||||
|
||||
# Usage
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
|
|
@ -66,13 +65,13 @@ func main() {
|
|||
}
|
||||
|
||||
if len(args) > 1 {
|
||||
fmt.Fprintln(os.Stderr, "Files will be processed sequentially! Consier using GNU Parallel.")
|
||||
fmt.Fprintln(os.Stderr, "Files will be processed sequentially! If required, use e.g. GNU Parallel to run concurrently.")
|
||||
fmt.Fprintf(os.Stderr, "Example: parallel %v ::: *.mp4\n", os.Args[0])
|
||||
}
|
||||
|
||||
ffmpegPath, ffprobePath := ffmpeg.GetPaths(nil)
|
||||
encoder := ffmpeg.NewEncoder(ffmpegPath)
|
||||
encoder.InitHWSupport(context.TODO())
|
||||
// don't need to InitHWSupport, phashing doesn't use hw acceleration
|
||||
ffprobe := ffmpeg.FFProbe(ffprobePath)
|
||||
|
||||
for _, item := range args {
|
||||
|
|
|
|||
|
|
@ -2,67 +2,154 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/stashapp/stash/internal/api"
|
||||
"github.com/stashapp/stash/internal/build"
|
||||
"github.com/stashapp/stash/internal/desktop"
|
||||
"github.com/stashapp/stash/internal/log"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/ui"
|
||||
|
||||
_ "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
)
|
||||
|
||||
var exitCode = 0
|
||||
|
||||
func main() {
|
||||
defer recoverPanic()
|
||||
|
||||
_, err := manager.Initialize()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer recoverPanic()
|
||||
if err := api.Start(); err != nil {
|
||||
handleError(err)
|
||||
} else {
|
||||
manager.GetInstance().Shutdown(0)
|
||||
defer func() {
|
||||
if exitCode != 0 {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
}()
|
||||
|
||||
go handleSignals()
|
||||
desktop.Start(manager.GetInstance(), &ui.FaviconProvider)
|
||||
defer recoverPanic()
|
||||
|
||||
blockForever()
|
||||
helpFlag := false
|
||||
pflag.BoolVarP(&helpFlag, "help", "h", false, "show this help text and exit")
|
||||
|
||||
versionFlag := false
|
||||
pflag.BoolVarP(&versionFlag, "version", "v", false, "show version number and exit")
|
||||
|
||||
cpuProfilePath := ""
|
||||
pflag.StringVar(&cpuProfilePath, "cpuprofile", "", "write cpu profile to file")
|
||||
|
||||
pflag.Parse()
|
||||
|
||||
if helpFlag {
|
||||
pflag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
if versionFlag {
|
||||
fmt.Println(build.VersionString())
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := config.Initialize()
|
||||
if err != nil {
|
||||
exitError(fmt.Errorf("config initialization error: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
l := initLog(cfg)
|
||||
|
||||
if cpuProfilePath != "" {
|
||||
if err := initProfiling(cpuProfilePath); err != nil {
|
||||
exitError(err)
|
||||
return
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
mgr, err := manager.Initialize(cfg, l)
|
||||
if err != nil {
|
||||
exitError(fmt.Errorf("manager initialization error: %w", err))
|
||||
return
|
||||
}
|
||||
defer mgr.Shutdown()
|
||||
|
||||
server, err := api.Initialize()
|
||||
if err != nil {
|
||||
exitError(fmt.Errorf("api initialization error: %w", err))
|
||||
return
|
||||
}
|
||||
defer server.Shutdown()
|
||||
|
||||
exit := make(chan int)
|
||||
|
||||
go func() {
|
||||
err := server.Start()
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
exitError(fmt.Errorf("http server error: %w", err))
|
||||
exit <- 1
|
||||
}
|
||||
}()
|
||||
|
||||
go handleSignals(exit)
|
||||
desktop.Start(exit, &ui.FaviconProvider)
|
||||
|
||||
exitCode = <-exit
|
||||
}
|
||||
|
||||
func initLog(cfg *config.Config) *log.Logger {
|
||||
l := log.NewLogger()
|
||||
l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel())
|
||||
logger.Logger = l
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func initProfiling(path string) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create CPU profile file: %v", err)
|
||||
}
|
||||
|
||||
if err = pprof.StartCPUProfile(f); err != nil {
|
||||
return fmt.Errorf("could not start CPU profiling: %v", err)
|
||||
}
|
||||
|
||||
logger.Infof("profiling to %s", path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func recoverPanic() {
|
||||
if p := recover(); p != nil {
|
||||
handleError(fmt.Errorf("Panic: %v", p))
|
||||
if err := recover(); err != nil {
|
||||
exitCode = 1
|
||||
logger.Errorf("panic: %v\n%s", err, debug.Stack())
|
||||
if desktop.IsDesktop() {
|
||||
desktop.FatalError(fmt.Errorf("Panic: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleError(err error) {
|
||||
func exitError(err error) {
|
||||
exitCode = 1
|
||||
logger.Error(err)
|
||||
if desktop.IsDesktop() {
|
||||
desktop.FatalError(err)
|
||||
manager.GetInstance().Shutdown(0)
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleSignals() {
|
||||
func handleSignals(exit chan<- int) {
|
||||
// handle signals
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
<-signals
|
||||
manager.GetInstance().Shutdown(0)
|
||||
}
|
||||
|
||||
func blockForever() {
|
||||
select {}
|
||||
exit <- 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -505,19 +505,10 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAIn
|
|||
c.Set(config.DLNAVideoSortOrder, input.VideoSortOrder)
|
||||
}
|
||||
|
||||
currentDLNAEnabled := c.GetDLNADefaultEnabled()
|
||||
if input.Enabled != nil && *input.Enabled != currentDLNAEnabled {
|
||||
refresh := false
|
||||
if input.Enabled != nil {
|
||||
c.Set(config.DLNADefaultEnabled, *input.Enabled)
|
||||
|
||||
// start/stop the DLNA service as needed
|
||||
dlnaService := manager.GetInstance().DLNAService
|
||||
if !*input.Enabled && dlnaService.IsRunning() {
|
||||
dlnaService.Stop(nil)
|
||||
} else if *input.Enabled && !dlnaService.IsRunning() {
|
||||
if err := dlnaService.Start(nil); err != nil {
|
||||
logger.Warnf("error starting DLNA service: %v", err)
|
||||
}
|
||||
}
|
||||
refresh = true
|
||||
}
|
||||
|
||||
if input.Interfaces != nil {
|
||||
|
|
@ -528,6 +519,10 @@ func (r *mutationResolver) ConfigureDlna(ctx context.Context, input ConfigDLNAIn
|
|||
return makeConfigDLNAResult(), err
|
||||
}
|
||||
|
||||
if refresh {
|
||||
manager.GetInstance().RefreshDLNA()
|
||||
}
|
||||
|
||||
return makeConfigDLNAResult(), nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ package api
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -12,7 +10,6 @@ import (
|
|||
"github.com/stashapp/stash/internal/identify"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
|
|
@ -110,31 +107,10 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
|||
// if download is true, then backup to temporary file and return a link
|
||||
download := input.Download != nil && *input.Download
|
||||
mgr := manager.GetInstance()
|
||||
database := mgr.Database
|
||||
var backupPath string
|
||||
if download {
|
||||
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
|
||||
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
|
||||
}
|
||||
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "backup*.sqlite")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backupPath = f.Name()
|
||||
f.Close()
|
||||
} else {
|
||||
backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault()
|
||||
if backupDirectoryPath != "" {
|
||||
if err := fsutil.EnsureDir(backupDirectoryPath); err != nil {
|
||||
return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err)
|
||||
}
|
||||
}
|
||||
backupPath = database.DatabaseBackupPath(backupDirectoryPath)
|
||||
}
|
||||
|
||||
err := database.Backup(backupPath)
|
||||
backupPath, backupName, err := mgr.BackupDatabase(download)
|
||||
if err != nil {
|
||||
logger.Errorf("Error backing up database: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -147,8 +123,7 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
|||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
|
||||
fn := filepath.Base(database.DatabaseBackupPath(""))
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + backupName
|
||||
return &ret, nil
|
||||
} else {
|
||||
logger.Infof("Successfully backed up database to: %s", backupPath)
|
||||
|
|
@ -158,33 +133,11 @@ func (r *mutationResolver) BackupDatabase(ctx context.Context, input BackupDatab
|
|||
}
|
||||
|
||||
func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input AnonymiseDatabaseInput) (*string, error) {
|
||||
// if download is true, then backup to temporary file and return a link
|
||||
// if download is true, then save to temporary file and return a link
|
||||
download := input.Download != nil && *input.Download
|
||||
mgr := manager.GetInstance()
|
||||
database := mgr.Database
|
||||
var outPath string
|
||||
if download {
|
||||
if err := fsutil.EnsureDir(mgr.Paths.Generated.Downloads); err != nil {
|
||||
return nil, fmt.Errorf("could not create backup directory %v: %w", mgr.Paths.Generated.Downloads, err)
|
||||
}
|
||||
f, err := os.CreateTemp(mgr.Paths.Generated.Downloads, "anonymous*.sqlite")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outPath = f.Name()
|
||||
f.Close()
|
||||
} else {
|
||||
backupDirectoryPath := mgr.Config.GetBackupDirectoryPathOrDefault()
|
||||
if backupDirectoryPath != "" {
|
||||
if err := fsutil.EnsureDir(backupDirectoryPath); err != nil {
|
||||
return nil, fmt.Errorf("could not create backup directory %v: %w", backupDirectoryPath, err)
|
||||
}
|
||||
}
|
||||
outPath = database.AnonymousDatabasePath(backupDirectoryPath)
|
||||
}
|
||||
|
||||
err := database.Anonymise(outPath)
|
||||
outPath, outName, err := mgr.AnonymiseDatabase(download)
|
||||
if err != nil {
|
||||
logger.Errorf("Error anonymising database: %v", err)
|
||||
return nil, err
|
||||
|
|
@ -199,8 +152,7 @@ func (r *mutationResolver) AnonymiseDatabase(ctx context.Context, input Anonymis
|
|||
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
|
||||
fn := filepath.Base(database.DatabaseBackupPath(""))
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + fn
|
||||
ret := baseURL + "/downloads/" + downloadHash + "/" + outName
|
||||
return &ret, nil
|
||||
} else {
|
||||
logger.Infof("Successfully anonymised database to: %s", outPath)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/task"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
|
|
@ -14,13 +13,9 @@ func refreshPackageType(typeArg PackageType) {
|
|||
mgr := manager.GetInstance()
|
||||
|
||||
if typeArg == PackageTypePlugin {
|
||||
if err := mgr.PluginCache.LoadPlugins(); err != nil {
|
||||
logger.Errorf("Error reading plugin configs: %v", err)
|
||||
}
|
||||
mgr.RefreshPluginCache()
|
||||
} else if typeArg == PackageTypeScraper {
|
||||
if err := mgr.ScraperCache.ReloadScrapers(); err != nil {
|
||||
logger.Errorf("Error reading scraper configs: %v", err)
|
||||
}
|
||||
mgr.RefreshScraperCache()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
|
@ -17,11 +16,7 @@ func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, t
|
|||
}
|
||||
|
||||
func (r *mutationResolver) ReloadPlugins(ctx context.Context) (bool, error) {
|
||||
err := manager.GetInstance().PluginCache.LoadPlugins()
|
||||
if err != nil {
|
||||
logger.Errorf("Error reading plugin configs: %v", err)
|
||||
}
|
||||
|
||||
manager.GetInstance().RefreshPluginCache()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,6 @@ import (
|
|||
)
|
||||
|
||||
func (r *mutationResolver) ReloadScrapers(ctx context.Context) (bool, error) {
|
||||
err := manager.GetInstance().ScraperCache.ReloadScrapers()
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
manager.GetInstance().RefreshScraperCache()
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,6 @@ import (
|
|||
|
||||
type downloadsRoutes struct{}
|
||||
|
||||
func getDownloadsRoutes() chi.Router {
|
||||
return downloadsRoutes{}.Routes()
|
||||
}
|
||||
|
||||
func (rs downloadsRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
|
|
|
|||
|
|
@ -31,14 +31,6 @@ type imageRoutes struct {
|
|||
fileGetter models.FileGetter
|
||||
}
|
||||
|
||||
func getImageRoutes(repo models.Repository) chi.Router {
|
||||
return imageRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
imageFinder: repo.Image,
|
||||
fileGetter: repo.File,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (rs imageRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
|
|
@ -76,7 +68,7 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
|
|||
Preset: manager.GetInstance().Config.GetPreviewPreset().String(),
|
||||
}
|
||||
|
||||
encoder := image.NewThumbnailEncoder(manager.GetInstance().FFMPEG, manager.GetInstance().FFProbe, clipPreviewOptions)
|
||||
encoder := image.NewThumbnailEncoder(manager.GetInstance().FFMpeg, manager.GetInstance().FFProbe, clipPreviewOptions)
|
||||
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
|
||||
if err != nil {
|
||||
// don't log for unsupported image format
|
||||
|
|
|
|||
|
|
@ -25,13 +25,6 @@ type movieRoutes struct {
|
|||
movieFinder MovieFinder
|
||||
}
|
||||
|
||||
func getMovieRoutes(repo models.Repository) chi.Router {
|
||||
return movieRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
movieFinder: repo.Movie,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (rs movieRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
|
|
|
|||
|
|
@ -23,13 +23,6 @@ type performerRoutes struct {
|
|||
performerFinder PerformerFinder
|
||||
}
|
||||
|
||||
func getPerformerRoutes(repo models.Repository) chi.Router {
|
||||
return performerRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
performerFinder: repo.Performer,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (rs performerRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,6 @@ type pluginRoutes struct {
|
|||
pluginCache *plugin.Cache
|
||||
}
|
||||
|
||||
func getPluginRoutes(pluginCache *plugin.Cache) chi.Router {
|
||||
return pluginRoutes{
|
||||
pluginCache: pluginCache,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (rs pluginRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
|
|
|
|||
|
|
@ -51,17 +51,6 @@ type sceneRoutes struct {
|
|||
tagFinder SceneMarkerTagFinder
|
||||
}
|
||||
|
||||
func getSceneRoutes(repo models.Repository) chi.Router {
|
||||
return sceneRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
sceneFinder: repo.Scene,
|
||||
fileGetter: repo.File,
|
||||
captionFinder: repo.File,
|
||||
sceneMarkerFinder: repo.SceneMarker,
|
||||
tagFinder: repo.Tag,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
|
|
|
|||
|
|
@ -24,13 +24,6 @@ type studioRoutes struct {
|
|||
studioFinder StudioFinder
|
||||
}
|
||||
|
||||
func getStudioRoutes(repo models.Repository) chi.Router {
|
||||
return studioRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
studioFinder: repo.Studio,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (rs studioRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
|
|
|
|||
|
|
@ -24,13 +24,6 @@ type tagRoutes struct {
|
|||
tagFinder TagFinder
|
||||
}
|
||||
|
||||
func getTagRoutes(repo models.Repository) chi.Router {
|
||||
return tagRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
tagFinder: repo.Tag,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (rs tagRoutes) Routes() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
|
|
|
|||
|
|
@ -46,25 +46,65 @@ const (
|
|||
playgroundEndpoint = "/playground"
|
||||
)
|
||||
|
||||
var uiBox = ui.UIBox
|
||||
var loginUIBox = ui.LoginUIBox
|
||||
type Server struct {
|
||||
http.Server
|
||||
displayAddress string
|
||||
|
||||
func Start() error {
|
||||
c := config.GetInstance()
|
||||
manager *manager.Manager
|
||||
}
|
||||
|
||||
initCustomPerformerImages(c.GetCustomPerformerImageLocation())
|
||||
// Called at startup
|
||||
func Initialize() (*Server, error) {
|
||||
mgr := manager.GetInstance()
|
||||
cfg := mgr.Config
|
||||
|
||||
initCustomPerformerImages(cfg.GetCustomPerformerImageLocation())
|
||||
|
||||
displayHost := cfg.GetHost()
|
||||
if displayHost == "0.0.0.0" {
|
||||
displayHost = "localhost"
|
||||
}
|
||||
displayAddress := displayHost + ":" + strconv.Itoa(cfg.GetPort())
|
||||
|
||||
address := cfg.GetHost() + ":" + strconv.Itoa(cfg.GetPort())
|
||||
tlsConfig, err := makeTLSConfig(cfg)
|
||||
if err != nil {
|
||||
// assume we don't want to start with a broken TLS configuration
|
||||
return nil, fmt.Errorf("error loading TLS config: %v", err)
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
displayAddress = "https://" + displayAddress + "/"
|
||||
} else {
|
||||
displayAddress = "http://" + displayAddress + "/"
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
server := &Server{
|
||||
Server: http.Server{
|
||||
Addr: address,
|
||||
Handler: r,
|
||||
TLSConfig: tlsConfig,
|
||||
// disable http/2 support by default
|
||||
// when http/2 is enabled, we are unable to hijack and close
|
||||
// the connection/request. This is necessary to stop running
|
||||
// streams when deleting a scene file.
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
},
|
||||
displayAddress: displayAddress,
|
||||
manager: mgr,
|
||||
}
|
||||
|
||||
r.Use(middleware.Heartbeat("/healthz"))
|
||||
r.Use(cors.AllowAll().Handler)
|
||||
r.Use(authenticateHandler())
|
||||
visitedPluginHandler := manager.GetInstance().SessionStore.VisitedPluginHandler()
|
||||
visitedPluginHandler := mgr.SessionStore.VisitedPluginHandler()
|
||||
r.Use(visitedPluginHandler)
|
||||
|
||||
r.Use(middleware.Recoverer)
|
||||
|
||||
if c.GetLogAccess() {
|
||||
if cfg.GetLogAccess() {
|
||||
httpLogger := httplog.NewLogger("Stash", httplog.Options{
|
||||
Concise: true,
|
||||
})
|
||||
|
|
@ -83,7 +123,7 @@ func Start() error {
|
|||
return errors.New(message)
|
||||
}
|
||||
|
||||
repo := manager.GetInstance().Repository
|
||||
repo := mgr.Repository
|
||||
|
||||
dataloaders := loaders.Middleware{
|
||||
Repository: repo,
|
||||
|
|
@ -91,10 +131,10 @@ func Start() error {
|
|||
|
||||
r.Use(dataloaders.Middleware)
|
||||
|
||||
pluginCache := manager.GetInstance().PluginCache
|
||||
sceneService := manager.GetInstance().SceneService
|
||||
imageService := manager.GetInstance().ImageService
|
||||
galleryService := manager.GetInstance().GalleryService
|
||||
pluginCache := mgr.PluginCache
|
||||
sceneService := mgr.SceneService
|
||||
imageService := mgr.ImageService
|
||||
galleryService := mgr.GalleryService
|
||||
resolver := &Resolver{
|
||||
repository: repo,
|
||||
sceneService: sceneService,
|
||||
|
|
@ -117,7 +157,7 @@ func Start() error {
|
|||
gqlSrv.AddTransport(gqlTransport.GET{})
|
||||
gqlSrv.AddTransport(gqlTransport.POST{})
|
||||
gqlSrv.AddTransport(gqlTransport.MultipartForm{
|
||||
MaxUploadSize: c.GetMaxUploadSize(),
|
||||
MaxUploadSize: cfg.GetMaxUploadSize(),
|
||||
})
|
||||
|
||||
gqlSrv.SetQueryCache(gqlLru.New(1000))
|
||||
|
|
@ -134,7 +174,7 @@ func Start() error {
|
|||
// chain the visited plugin handler
|
||||
// also requires the dataloader middleware
|
||||
gqlHandler := visitedPluginHandler(dataloaders.Middleware(http.HandlerFunc(gqlHandlerFunc)))
|
||||
manager.GetInstance().PluginCache.RegisterGQLHandler(gqlHandler)
|
||||
pluginCache.RegisterGQLHandler(gqlHandler)
|
||||
|
||||
r.HandleFunc(gqlEndpoint, gqlHandlerFunc)
|
||||
r.HandleFunc(playgroundEndpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -143,23 +183,23 @@ func Start() error {
|
|||
gqlPlayground.Handler("GraphQL playground", endpoint)(w, r)
|
||||
})
|
||||
|
||||
r.Mount("/performer", getPerformerRoutes(repo))
|
||||
r.Mount("/scene", getSceneRoutes(repo))
|
||||
r.Mount("/image", getImageRoutes(repo))
|
||||
r.Mount("/studio", getStudioRoutes(repo))
|
||||
r.Mount("/movie", getMovieRoutes(repo))
|
||||
r.Mount("/tag", getTagRoutes(repo))
|
||||
r.Mount("/downloads", getDownloadsRoutes())
|
||||
r.Mount("/plugin", getPluginRoutes(pluginCache))
|
||||
r.Mount("/performer", server.getPerformerRoutes())
|
||||
r.Mount("/scene", server.getSceneRoutes())
|
||||
r.Mount("/image", server.getImageRoutes())
|
||||
r.Mount("/studio", server.getStudioRoutes())
|
||||
r.Mount("/movie", server.getMovieRoutes())
|
||||
r.Mount("/tag", server.getTagRoutes())
|
||||
r.Mount("/downloads", server.getDownloadsRoutes())
|
||||
r.Mount("/plugin", server.getPluginRoutes())
|
||||
|
||||
r.HandleFunc("/css", cssHandler(c))
|
||||
r.HandleFunc("/javascript", javascriptHandler(c))
|
||||
r.HandleFunc("/customlocales", customLocalesHandler(c))
|
||||
r.HandleFunc("/css", cssHandler(cfg))
|
||||
r.HandleFunc("/javascript", javascriptHandler(cfg))
|
||||
r.HandleFunc("/customlocales", customLocalesHandler(cfg))
|
||||
|
||||
staticLoginUI := statigz.FileServer(loginUIBox.(fs.ReadDirFS))
|
||||
staticLoginUI := statigz.FileServer(ui.LoginUIBox.(fs.ReadDirFS))
|
||||
|
||||
r.Get(loginEndpoint, handleLogin(loginUIBox))
|
||||
r.Post(loginEndpoint, handleLoginPost(loginUIBox))
|
||||
r.Get(loginEndpoint, handleLogin())
|
||||
r.Post(loginEndpoint, handleLoginPost())
|
||||
r.Get(logoutEndpoint, handleLogout())
|
||||
r.HandleFunc(loginEndpoint+"/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, loginEndpoint)
|
||||
|
|
@ -168,13 +208,13 @@ func Start() error {
|
|||
})
|
||||
|
||||
// Serve static folders
|
||||
customServedFolders := c.GetCustomServedFolders()
|
||||
customServedFolders := cfg.GetCustomServedFolders()
|
||||
if customServedFolders != nil {
|
||||
r.Mount("/custom", getCustomRoutes(customServedFolders))
|
||||
}
|
||||
|
||||
customUILocation := c.GetCustomUILocation()
|
||||
staticUI := statigz.FileServer(uiBox.(fs.ReadDirFS))
|
||||
customUILocation := cfg.GetCustomUILocation()
|
||||
staticUI := statigz.FileServer(ui.UIBox.(fs.ReadDirFS))
|
||||
|
||||
// Serve the web app
|
||||
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -190,8 +230,8 @@ func Start() error {
|
|||
}
|
||||
|
||||
if ext == ".html" || ext == "" {
|
||||
themeColor := c.GetThemeColor()
|
||||
data, err := fs.ReadFile(uiBox, "index.html")
|
||||
themeColor := cfg.GetThemeColor()
|
||||
data, err := fs.ReadFile(ui.UIBox, "index.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
@ -217,51 +257,91 @@ func Start() error {
|
|||
}
|
||||
})
|
||||
|
||||
displayHost := c.GetHost()
|
||||
if displayHost == "0.0.0.0" {
|
||||
displayHost = "localhost"
|
||||
}
|
||||
displayAddress := displayHost + ":" + strconv.Itoa(c.GetPort())
|
||||
|
||||
address := c.GetHost() + ":" + strconv.Itoa(c.GetPort())
|
||||
tlsConfig, err := makeTLSConfig(c)
|
||||
if err != nil {
|
||||
// assume we don't want to start with a broken TLS configuration
|
||||
panic(fmt.Errorf("error loading TLS config: %v", err))
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: address,
|
||||
Handler: r,
|
||||
TLSConfig: tlsConfig,
|
||||
// disable http/2 support by default
|
||||
// when http/2 is enabled, we are unable to hijack and close
|
||||
// the connection/request. This is necessary to stop running
|
||||
// streams when deleting a scene file.
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
}
|
||||
|
||||
logger.Infof("stash version: %s\n", build.VersionString())
|
||||
logger.Infof("stash version: %s", build.VersionString())
|
||||
go printLatestVersion(context.TODO())
|
||||
logger.Infof("stash is listening on " + address)
|
||||
if tlsConfig != nil {
|
||||
displayAddress = "https://" + displayAddress + "/"
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
logger.Infof("stash is listening on " + s.Addr)
|
||||
logger.Infof("stash is running at " + s.displayAddress)
|
||||
|
||||
if s.TLSConfig != nil {
|
||||
return s.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
displayAddress = "http://" + displayAddress + "/"
|
||||
return s.ListenAndServe()
|
||||
}
|
||||
}
|
||||
|
||||
logger.Infof("stash is running at " + displayAddress)
|
||||
if tlsConfig != nil {
|
||||
err = server.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
err = server.ListenAndServe()
|
||||
func (s *Server) Shutdown() {
|
||||
err := s.Server.Shutdown(context.TODO())
|
||||
if err != nil {
|
||||
logger.Errorf("Error shutting down http server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
func (s *Server) getPerformerRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return performerRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
performerFinder: repo.Performer,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
return nil
|
||||
func (s *Server) getSceneRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return sceneRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
sceneFinder: repo.Scene,
|
||||
fileGetter: repo.File,
|
||||
captionFinder: repo.File,
|
||||
sceneMarkerFinder: repo.SceneMarker,
|
||||
tagFinder: repo.Tag,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getImageRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return imageRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
imageFinder: repo.Image,
|
||||
fileGetter: repo.File,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getStudioRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return studioRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
studioFinder: repo.Studio,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getMovieRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return movieRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
movieFinder: repo.Movie,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getTagRoutes() chi.Router {
|
||||
repo := s.manager.Repository
|
||||
return tagRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
tagFinder: repo.Tag,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getDownloadsRoutes() chi.Router {
|
||||
return downloadsRoutes{}.Routes()
|
||||
}
|
||||
|
||||
func (s *Server) getPluginRoutes() chi.Router {
|
||||
return pluginRoutes{
|
||||
pluginCache: s.manager.PluginCache,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
func copyFile(w io.Writer, path string) error {
|
||||
|
|
@ -290,7 +370,7 @@ func serveFiles(w http.ResponseWriter, r *http.Request, paths []string) {
|
|||
utils.ServeStaticContent(w, r, buffer.Bytes())
|
||||
}
|
||||
|
||||
func cssHandler(c *config.Instance) func(w http.ResponseWriter, r *http.Request) {
|
||||
func cssHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var paths []string
|
||||
|
||||
|
|
@ -308,7 +388,7 @@ func cssHandler(c *config.Instance) func(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
}
|
||||
|
||||
func javascriptHandler(c *config.Instance) func(w http.ResponseWriter, r *http.Request) {
|
||||
func javascriptHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var paths []string
|
||||
|
||||
|
|
@ -326,7 +406,7 @@ func javascriptHandler(c *config.Instance) func(w http.ResponseWriter, r *http.R
|
|||
}
|
||||
}
|
||||
|
||||
func customLocalesHandler(c *config.Instance) func(w http.ResponseWriter, r *http.Request) {
|
||||
func customLocalesHandler(c *config.Config) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
buffer := bytes.Buffer{}
|
||||
|
||||
|
|
@ -351,7 +431,7 @@ func customLocalesHandler(c *config.Instance) func(w http.ResponseWriter, r *htt
|
|||
}
|
||||
}
|
||||
|
||||
func makeTLSConfig(c *config.Instance) (*tls.Config, error) {
|
||||
func makeTLSConfig(c *config.Config) (*tls.Config, error) {
|
||||
c.InitTLS()
|
||||
certFile, keyFile := c.GetTLSFiles()
|
||||
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ import (
|
|||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/session"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"github.com/stashapp/stash/ui"
|
||||
)
|
||||
|
||||
const returnURLParam = "returnURL"
|
||||
|
||||
func getLoginPage(loginUIBox fs.FS) []byte {
|
||||
data, err := fs.ReadFile(loginUIBox, "login.html")
|
||||
func getLoginPage() []byte {
|
||||
data, err := fs.ReadFile(ui.LoginUIBox, "login.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
@ -31,8 +32,8 @@ type loginTemplateData struct {
|
|||
Error string
|
||||
}
|
||||
|
||||
func serveLoginPage(loginUIBox fs.FS, w http.ResponseWriter, r *http.Request, returnURL string, loginError string) {
|
||||
loginPage := string(getLoginPage(loginUIBox))
|
||||
func serveLoginPage(w http.ResponseWriter, r *http.Request, returnURL string, loginError string) {
|
||||
loginPage := string(getLoginPage())
|
||||
prefix := getProxyPrefix(r)
|
||||
loginPage = strings.ReplaceAll(loginPage, "/%BASE_URL%", prefix)
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ func serveLoginPage(loginUIBox fs.FS, w http.ResponseWriter, r *http.Request, re
|
|||
utils.ServeStaticContent(w, r, buffer.Bytes())
|
||||
}
|
||||
|
||||
func handleLogin(loginUIBox fs.FS) http.HandlerFunc {
|
||||
func handleLogin() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
returnURL := r.URL.Query().Get(returnURLParam)
|
||||
|
||||
|
|
@ -71,11 +72,11 @@ func handleLogin(loginUIBox fs.FS) http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
serveLoginPage(loginUIBox, w, r, returnURL, "")
|
||||
serveLoginPage(w, r, returnURL, "")
|
||||
}
|
||||
}
|
||||
|
||||
func handleLoginPost(loginUIBox fs.FS) http.HandlerFunc {
|
||||
func handleLoginPost() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.FormValue(returnURLParam)
|
||||
if url == "" {
|
||||
|
|
@ -92,7 +93,7 @@ func handleLoginPost(loginUIBox fs.FS) http.HandlerFunc {
|
|||
|
||||
if errors.As(err, &invalidCredentialsError) {
|
||||
// serve login page with an error
|
||||
serveLoginPage(loginUIBox, w, r, url, "Username or password is invalid")
|
||||
serveLoginPage(w, r, url, "Username or password is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sqlite"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
|
|
@ -77,6 +78,9 @@ func runTests(m *testing.M) int {
|
|||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// initialise empty config - needed by some db migrations
|
||||
_ = config.InitializeEmpty()
|
||||
|
||||
ret := runTests(m)
|
||||
os.Exit(ret)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,6 @@ import (
|
|||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type ShutdownHandler interface {
|
||||
Shutdown(code int)
|
||||
}
|
||||
|
||||
type FaviconProvider interface {
|
||||
GetFavicon() []byte
|
||||
GetFaviconPng() []byte
|
||||
|
|
@ -27,7 +23,7 @@ type FaviconProvider interface {
|
|||
|
||||
// Start starts the desktop icon process. It blocks until the process exits.
|
||||
// MUST be run on the main goroutine or will have no effect on macOS
|
||||
func Start(shutdownHandler ShutdownHandler, faviconProvider FaviconProvider) {
|
||||
func Start(exit chan<- int, faviconProvider FaviconProvider) {
|
||||
if IsDesktop() {
|
||||
hideConsole()
|
||||
|
||||
|
|
@ -36,7 +32,7 @@ func Start(shutdownHandler ShutdownHandler, faviconProvider FaviconProvider) {
|
|||
openURLInBrowser("")
|
||||
}
|
||||
writeStashIcon(faviconProvider)
|
||||
startSystray(shutdownHandler, faviconProvider)
|
||||
startSystray(exit, faviconProvider)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
package desktop
|
||||
|
||||
func startSystray(shutdownHandler ShutdownHandler, favicon FaviconProvider) {
|
||||
func startSystray(exit chan<- int, favicon FaviconProvider) {
|
||||
// The systray is not available on Linux because the required libraries (libappindicator3 and gtk+3.0)
|
||||
// are not able to be statically compiled. Technically, the systray works perfectly fine when dynamically
|
||||
// linked, but we cannot distribute it for compatibility reasons.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
)
|
||||
|
||||
// MUST be run on the main goroutine or will have no effect on macOS
|
||||
func startSystray(shutdownHandler ShutdownHandler, faviconProvider FaviconProvider) {
|
||||
func startSystray(exit chan<- int, faviconProvider FaviconProvider) {
|
||||
// Shows a small notification to inform that Stash will no longer show a terminal window,
|
||||
// and instead will be available in the tray. Will only show the first time a pre-desktop integration
|
||||
// system is started from a non-terminal method, e.g. double-clicking an icon.
|
||||
|
|
@ -39,12 +39,12 @@ func startSystray(shutdownHandler ShutdownHandler, faviconProvider FaviconProvid
|
|||
|
||||
for {
|
||||
systray.Run(func() {
|
||||
systrayInitialize(shutdownHandler, faviconProvider)
|
||||
systrayInitialize(exit, faviconProvider)
|
||||
}, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func systrayInitialize(shutdownHandler ShutdownHandler, faviconProvider FaviconProvider) {
|
||||
func systrayInitialize(exit chan<- int, faviconProvider FaviconProvider) {
|
||||
favicon := faviconProvider.GetFavicon()
|
||||
systray.SetTemplateIcon(favicon, favicon)
|
||||
systray.SetTooltip("🟢 Stash is Running.")
|
||||
|
|
@ -86,7 +86,7 @@ func systrayInitialize(shutdownHandler ShutdownHandler, faviconProvider FaviconP
|
|||
openURLInBrowser("")
|
||||
case <-quitStashButton.ClickedCh:
|
||||
systray.Quit()
|
||||
shutdownHandler.Shutdown(0)
|
||||
exit <- 0
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
// should be run with -race
|
||||
func TestConcurrentConfigAccess(t *testing.T) {
|
||||
i := GetInstance()
|
||||
i := InitializeEmpty()
|
||||
|
||||
const workers = 8
|
||||
const loops = 200
|
||||
|
|
@ -16,13 +16,12 @@ func TestConcurrentConfigAccess(t *testing.T) {
|
|||
wg.Add(1)
|
||||
go func(wk int) {
|
||||
for l := 0; l < loops; l++ {
|
||||
if err := i.SetInitialMemoryConfig(); err != nil {
|
||||
if err := i.SetInitialConfig(); err != nil {
|
||||
t.Errorf("Failure setting initial configuration in worker %v iteration %v: %v", wk, l, err)
|
||||
}
|
||||
|
||||
i.HasCredentials()
|
||||
i.ValidateCredentials("", "")
|
||||
i.GetCPUProfilePath()
|
||||
i.GetConfigFile()
|
||||
i.GetConfigPath()
|
||||
i.GetDefaultDatabaseFilePath()
|
||||
|
|
|
|||
|
|
@ -6,84 +6,107 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/stashapp/stash/internal/build"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
initOnce sync.Once
|
||||
instanceOnce sync.Once
|
||||
)
|
||||
|
||||
type flagStruct struct {
|
||||
configFilePath string
|
||||
cpuProfilePath string
|
||||
nobrowser bool
|
||||
helpFlag bool
|
||||
versionFlag bool
|
||||
}
|
||||
|
||||
func GetInstance() *Instance {
|
||||
instanceOnce.Do(func() {
|
||||
instance = &Instance{
|
||||
main: viper.New(),
|
||||
overrides: viper.New(),
|
||||
var flags flagStruct
|
||||
|
||||
func init() {
|
||||
pflag.IP("host", net.IPv4(0, 0, 0, 0), "ip address for the host")
|
||||
pflag.Int("port", 9999, "port to serve from")
|
||||
pflag.StringVarP(&flags.configFilePath, "config", "c", "", "config file to use")
|
||||
pflag.BoolVar(&flags.nobrowser, "nobrowser", false, "Don't open a browser window after launch")
|
||||
}
|
||||
|
||||
// Called at startup
|
||||
func Initialize() (*Config, error) {
|
||||
cfg := &Config{
|
||||
main: viper.New(),
|
||||
overrides: viper.New(),
|
||||
}
|
||||
|
||||
cfg.initOverrides()
|
||||
|
||||
err := cfg.initConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cfg.isNewSystem {
|
||||
if cfg.Validate() == nil {
|
||||
// system has been initialised by the environment
|
||||
cfg.isNewSystem = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if !cfg.isNewSystem {
|
||||
cfg.setExistingSystemDefaults()
|
||||
|
||||
err := cfg.SetInitialConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cfg.Write()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cfg.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
instance = cfg
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// Called by tests to initialize an empty config
|
||||
func InitializeEmpty() *Config {
|
||||
cfg := &Config{
|
||||
main: viper.New(),
|
||||
overrides: viper.New(),
|
||||
}
|
||||
instance = cfg
|
||||
return instance
|
||||
}
|
||||
|
||||
func Initialize() (*Instance, error) {
|
||||
var err error
|
||||
initOnce.Do(func() {
|
||||
flags := initFlags()
|
||||
|
||||
if flags.helpFlag {
|
||||
pflag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if flags.versionFlag {
|
||||
fmt.Printf(build.VersionString() + "\n")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
overrides := makeOverrideConfig()
|
||||
|
||||
_ = GetInstance()
|
||||
instance.overrides = overrides
|
||||
instance.cpuProfilePath = flags.cpuProfilePath
|
||||
// instance.configUpdates = make(chan int)
|
||||
|
||||
if err = initConfig(instance, flags); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if instance.isNewSystem {
|
||||
if instance.Validate() == nil {
|
||||
// system has been initialised by the environment
|
||||
instance.isNewSystem = false
|
||||
}
|
||||
}
|
||||
|
||||
if !instance.isNewSystem {
|
||||
err = instance.setExistingSystemDefaults()
|
||||
if err == nil {
|
||||
err = instance.SetInitialConfig()
|
||||
}
|
||||
}
|
||||
})
|
||||
return instance, err
|
||||
func bindEnv(v *viper.Viper, key string) {
|
||||
if err := v.BindEnv(key); err != nil {
|
||||
panic(fmt.Sprintf("unable to set environment key (%v): %v", key, err))
|
||||
}
|
||||
}
|
||||
|
||||
func initConfig(instance *Instance, flags flagStruct) error {
|
||||
v := instance.main
|
||||
func (i *Config) initOverrides() {
|
||||
v := i.overrides
|
||||
|
||||
if err := v.BindPFlags(pflag.CommandLine); err != nil {
|
||||
logger.Infof("failed to bind flags: %v", err)
|
||||
}
|
||||
|
||||
v.SetEnvPrefix("stash") // will be uppercased automatically
|
||||
bindEnv(v, "host") // STASH_HOST
|
||||
bindEnv(v, "port") // STASH_PORT
|
||||
bindEnv(v, "external_host") // STASH_EXTERNAL_HOST
|
||||
bindEnv(v, "generated") // STASH_GENERATED
|
||||
bindEnv(v, "metadata") // STASH_METADATA
|
||||
bindEnv(v, "cache") // STASH_CACHE
|
||||
bindEnv(v, "stash") // STASH_STASH
|
||||
}
|
||||
|
||||
func (i *Config) initConfig() error {
|
||||
v := i.main
|
||||
|
||||
// The config file is called config. Leave off the file extension.
|
||||
v.SetConfigName("config")
|
||||
|
|
@ -105,11 +128,11 @@ func initConfig(instance *Instance, flags flagStruct) error {
|
|||
|
||||
// if file does not exist, assume it is a new system
|
||||
if exists, _ := fsutil.FileExists(configFile); !exists {
|
||||
instance.isNewSystem = true
|
||||
i.isNewSystem = true
|
||||
|
||||
// ensure we can write to the file
|
||||
if err := fsutil.Touch(configFile); err != nil {
|
||||
return fmt.Errorf(`could not write to provided config path "%s": %s`, configFile, err.Error())
|
||||
return fmt.Errorf(`could not write to provided config path "%s": %v`, configFile, err)
|
||||
} else {
|
||||
// remove the file
|
||||
os.Remove(configFile)
|
||||
|
|
@ -123,7 +146,7 @@ func initConfig(instance *Instance, flags flagStruct) error {
|
|||
// if not found, assume its a new system
|
||||
var notFoundErr viper.ConfigFileNotFoundError
|
||||
if errors.As(err, ¬FoundErr) {
|
||||
instance.isNewSystem = true
|
||||
i.isNewSystem = true
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
|
|
@ -131,48 +154,3 @@ func initConfig(instance *Instance, flags flagStruct) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initFlags() flagStruct {
|
||||
flags := flagStruct{}
|
||||
|
||||
pflag.IP("host", net.IPv4(0, 0, 0, 0), "ip address for the host")
|
||||
pflag.Int("port", 9999, "port to serve from")
|
||||
pflag.StringVarP(&flags.configFilePath, "config", "c", "", "config file to use")
|
||||
pflag.StringVar(&flags.cpuProfilePath, "cpuprofile", "", "write cpu profile to file")
|
||||
pflag.BoolVar(&flags.nobrowser, "nobrowser", false, "Don't open a browser window after launch")
|
||||
pflag.BoolVarP(&flags.helpFlag, "help", "h", false, "show this help text and exit")
|
||||
pflag.BoolVarP(&flags.versionFlag, "version", "v", false, "show version number and exit")
|
||||
|
||||
pflag.Parse()
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
func initEnvs(viper *viper.Viper) {
|
||||
viper.SetEnvPrefix("stash") // will be uppercased automatically
|
||||
bindEnv(viper, "host") // STASH_HOST
|
||||
bindEnv(viper, "port") // STASH_PORT
|
||||
bindEnv(viper, "external_host") // STASH_EXTERNAL_HOST
|
||||
bindEnv(viper, "generated") // STASH_GENERATED
|
||||
bindEnv(viper, "metadata") // STASH_METADATA
|
||||
bindEnv(viper, "cache") // STASH_CACHE
|
||||
bindEnv(viper, "stash") // STASH_STASH
|
||||
}
|
||||
|
||||
func bindEnv(viper *viper.Viper, key string) {
|
||||
if err := viper.BindEnv(key); err != nil {
|
||||
panic(fmt.Sprintf("unable to set environment key (%v): %v", key, err))
|
||||
}
|
||||
}
|
||||
|
||||
func makeOverrideConfig() *viper.Viper {
|
||||
viper := viper.New()
|
||||
|
||||
if err := viper.BindPFlags(pflag.CommandLine); err != nil {
|
||||
logger.Infof("failed to bind flags: %s", err.Error())
|
||||
}
|
||||
|
||||
initEnvs(viper)
|
||||
|
||||
return viper
|
||||
}
|
||||
|
|
|
|||
50
internal/manager/enums.go
Normal file
50
internal/manager/enums.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type SystemStatusEnum string
|
||||
|
||||
const (
|
||||
SystemStatusEnumSetup SystemStatusEnum = "SETUP"
|
||||
SystemStatusEnumNeedsMigration SystemStatusEnum = "NEEDS_MIGRATION"
|
||||
SystemStatusEnumOk SystemStatusEnum = "OK"
|
||||
)
|
||||
|
||||
var AllSystemStatusEnum = []SystemStatusEnum{
|
||||
SystemStatusEnumSetup,
|
||||
SystemStatusEnumNeedsMigration,
|
||||
SystemStatusEnumOk,
|
||||
}
|
||||
|
||||
func (e SystemStatusEnum) IsValid() bool {
|
||||
switch e {
|
||||
case SystemStatusEnumSetup, SystemStatusEnumNeedsMigration, SystemStatusEnumOk:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e SystemStatusEnum) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *SystemStatusEnum) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = SystemStatusEnum(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid SystemStatusEnum", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e SystemStatusEnum) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ import (
|
|||
)
|
||||
|
||||
type fingerprintCalculator struct {
|
||||
Config *config.Instance
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
func (c *fingerprintCalculator) calculateOshash(f *models.BaseFile, o file.Opener) (*models.Fingerprint, error) {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ func (g *generatorInfo) calculateFrameRate(videoStream *ffmpeg.FFProbeStream) er
|
|||
|
||||
// If we are missing the frame count or frame rate then seek through the file and extract the info with regex
|
||||
if numberOfFrames == 0 || !isValidFloat64(framerate) {
|
||||
info, err := instance.FFMPEG.CalculateFrameRate(context.TODO(), &g.VideoFile)
|
||||
info, err := instance.FFMpeg.CalculateFrameRate(context.TODO(), &g.VideoFile)
|
||||
if err != nil {
|
||||
logger.Errorf("error calculating frame rate: %v", err)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageO
|
|||
SlowSeek: slowSeek,
|
||||
Columns: cols,
|
||||
g: &generate.Generator{
|
||||
Encoder: instance.FFMPEG,
|
||||
Encoder: instance.FFMpeg,
|
||||
FFMpegConfig: instance.Config,
|
||||
LockManager: instance.ReadLockManager,
|
||||
ScenePaths: instance.Paths.Scene,
|
||||
|
|
|
|||
308
internal/manager/init.go
Normal file
308
internal/manager/init.go
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/desktop"
|
||||
"github.com/stashapp/stash/internal/dlna"
|
||||
"github.com/stashapp/stash/internal/log"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/job"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models/paths"
|
||||
"github.com/stashapp/stash/pkg/pkg"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/session"
|
||||
"github.com/stashapp/stash/pkg/sqlite"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"github.com/stashapp/stash/ui"
|
||||
)
|
||||
|
||||
// Called at startup
|
||||
func Initialize(cfg *config.Config, l *log.Logger) (*Manager, error) {
|
||||
ctx := context.TODO()
|
||||
|
||||
db := sqlite.NewDatabase()
|
||||
repo := db.Repository()
|
||||
|
||||
// start with empty paths
|
||||
mgrPaths := &paths.Paths{}
|
||||
|
||||
scraperRepository := scraper.NewRepository(repo)
|
||||
scraperCache := scraper.NewCache(cfg, scraperRepository)
|
||||
|
||||
pluginCache := plugin.NewCache(cfg)
|
||||
|
||||
sceneService := &scene.Service{
|
||||
File: db.File,
|
||||
Repository: db.Scene,
|
||||
MarkerRepository: db.SceneMarker,
|
||||
PluginCache: pluginCache,
|
||||
Paths: mgrPaths,
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
imageService := &image.Service{
|
||||
File: db.File,
|
||||
Repository: db.Image,
|
||||
}
|
||||
|
||||
galleryService := &gallery.Service{
|
||||
Repository: db.Gallery,
|
||||
ImageFinder: db.Image,
|
||||
ImageService: imageService,
|
||||
File: db.File,
|
||||
Folder: db.Folder,
|
||||
}
|
||||
|
||||
sceneServer := &SceneServer{
|
||||
TxnManager: repo.TxnManager,
|
||||
SceneCoverGetter: repo.Scene,
|
||||
}
|
||||
|
||||
dlnaRepository := dlna.NewRepository(repo)
|
||||
dlnaService := dlna.NewService(dlnaRepository, cfg, sceneServer)
|
||||
|
||||
mgr := &Manager{
|
||||
Config: cfg,
|
||||
Logger: l,
|
||||
|
||||
Paths: mgrPaths,
|
||||
|
||||
JobManager: initJobManager(cfg),
|
||||
ReadLockManager: fsutil.NewReadLockManager(),
|
||||
|
||||
DownloadStore: NewDownloadStore(),
|
||||
|
||||
PluginCache: pluginCache,
|
||||
ScraperCache: scraperCache,
|
||||
|
||||
DLNAService: dlnaService,
|
||||
|
||||
Database: db,
|
||||
Repository: repo,
|
||||
|
||||
SceneService: sceneService,
|
||||
ImageService: imageService,
|
||||
GalleryService: galleryService,
|
||||
|
||||
scanSubs: &subscriptionManager{},
|
||||
}
|
||||
|
||||
mgr.RefreshPluginSourceManager()
|
||||
mgr.RefreshScraperSourceManager()
|
||||
|
||||
if !cfg.IsNewSystem() {
|
||||
logger.Infof("using config file: %s", cfg.GetConfigFile())
|
||||
|
||||
err := cfg.Validate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
if err := mgr.postInit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mgr.checkSecurityTripwire()
|
||||
} else {
|
||||
cfgFile := cfg.GetConfigFile()
|
||||
if cfgFile != "" {
|
||||
cfgFile += " "
|
||||
}
|
||||
|
||||
// create temporary session store - this will be re-initialised
|
||||
// after config is complete
|
||||
mgr.SessionStore = session.NewStore(cfg)
|
||||
|
||||
logger.Warnf("config file %snot found. Assuming new system...", cfgFile)
|
||||
}
|
||||
|
||||
instance = mgr
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
func initialisePackageManager(localPath string, srcPathGetter pkg.SourcePathGetter) *pkg.Manager {
|
||||
const timeout = 10 * time.Second
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
return &pkg.Manager{
|
||||
Local: &pkg.Store{
|
||||
BaseDir: localPath,
|
||||
ManifestFile: pkg.ManifestFile,
|
||||
},
|
||||
PackagePathGetter: srcPathGetter,
|
||||
Client: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
func formatDuration(t time.Duration) string {
|
||||
return fmt.Sprintf("%02.f:%02.f:%02.f", t.Hours(), t.Minutes(), t.Seconds())
|
||||
}
|
||||
|
||||
func initJobManager(cfg *config.Config) *job.Manager {
|
||||
ret := job.NewManager()
|
||||
|
||||
// desktop notifications
|
||||
ctx := context.Background()
|
||||
c := ret.Subscribe(context.Background())
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case j := <-c.RemovedJob:
|
||||
if cfg.GetNotificationsEnabled() {
|
||||
cleanDesc := strings.TrimRight(j.Description, ".")
|
||||
|
||||
if j.StartTime == nil {
|
||||
// Task was never started
|
||||
return
|
||||
}
|
||||
|
||||
timeElapsed := j.EndTime.Sub(*j.StartTime)
|
||||
msg := fmt.Sprintf("Task \"%s\" is finished in %s.", cleanDesc, formatDuration(timeElapsed))
|
||||
desktop.SendNotification("Task Finished", msg)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// postInit initialises the paths, caches and database after the initial
|
||||
// configuration has been set. Should only be called if the configuration
|
||||
// is valid.
|
||||
func (s *Manager) postInit(ctx context.Context) error {
|
||||
s.RefreshConfig()
|
||||
|
||||
s.SessionStore = session.NewStore(s.Config)
|
||||
s.PluginCache.RegisterSessionStore(s.SessionStore)
|
||||
|
||||
s.RefreshPluginCache()
|
||||
s.RefreshScraperCache()
|
||||
s.RefreshStreamManager()
|
||||
s.RefreshDLNA()
|
||||
|
||||
s.SetBlobStoreOptions()
|
||||
|
||||
s.writeStashIcon()
|
||||
|
||||
// clear the downloads and tmp directories
|
||||
// #1021 - only clear these directories if the generated folder is non-empty
|
||||
if s.Config.GetGeneratedPath() != "" {
|
||||
const deleteTimeout = 1 * time.Second
|
||||
|
||||
utils.Timeout(func() {
|
||||
if err := fsutil.EmptyDir(s.Paths.Generated.Downloads); err != nil {
|
||||
logger.Warnf("could not empty downloads directory: %v", err)
|
||||
}
|
||||
if err := fsutil.EnsureDir(s.Paths.Generated.Tmp); err != nil {
|
||||
logger.Warnf("could not create temporary directory: %v", err)
|
||||
} else {
|
||||
if err := fsutil.EmptyDir(s.Paths.Generated.Tmp); err != nil {
|
||||
logger.Warnf("could not empty temporary directory: %v", err)
|
||||
}
|
||||
}
|
||||
}, deleteTimeout, func(done chan struct{}) {
|
||||
logger.Info("Please wait. Deleting temporary files...") // print
|
||||
<-done // and wait for deletion
|
||||
logger.Info("Temporary files deleted.")
|
||||
})
|
||||
}
|
||||
|
||||
if err := s.Database.Open(s.Config.GetDatabasePath()); err != nil {
|
||||
var migrationNeededErr *sqlite.MigrationNeededError
|
||||
if errors.As(err, &migrationNeededErr) {
|
||||
logger.Warn(err)
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Set the proxy if defined in config
|
||||
if s.Config.GetProxy() != "" {
|
||||
os.Setenv("HTTP_PROXY", s.Config.GetProxy())
|
||||
os.Setenv("HTTPS_PROXY", s.Config.GetProxy())
|
||||
os.Setenv("NO_PROXY", s.Config.GetNoProxy())
|
||||
logger.Info("Using HTTP proxy")
|
||||
}
|
||||
|
||||
if err := s.initFFmpeg(ctx); err != nil {
|
||||
return fmt.Errorf("error initializing FFmpeg subsystem: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Manager) checkSecurityTripwire() {
|
||||
if err := session.CheckExternalAccessTripwire(s.Config); err != nil {
|
||||
session.LogExternalAccessError(*err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Manager) writeStashIcon() {
|
||||
iconPath := filepath.Join(s.Config.GetConfigPath(), "icon.png")
|
||||
err := os.WriteFile(iconPath, ui.FaviconProvider.GetFaviconPng(), 0644)
|
||||
if err != nil {
|
||||
logger.Errorf("Couldn't write icon file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Manager) initFFmpeg(ctx context.Context) error {
|
||||
// use same directory as config path
|
||||
configDirectory := s.Config.GetConfigPath()
|
||||
paths := []string{
|
||||
configDirectory,
|
||||
paths.GetStashHomeDirectory(),
|
||||
}
|
||||
ffmpegPath, ffprobePath := ffmpeg.GetPaths(paths)
|
||||
|
||||
if ffmpegPath == "" || ffprobePath == "" {
|
||||
logger.Infof("couldn't find FFmpeg, attempting to download it")
|
||||
if err := ffmpeg.Download(ctx, configDirectory); err != nil {
|
||||
path, absErr := filepath.Abs(configDirectory)
|
||||
if absErr != nil {
|
||||
path = configDirectory
|
||||
}
|
||||
msg := `Unable to automatically download FFmpeg
|
||||
|
||||
Check the readme for download links.
|
||||
The ffmpeg and ffprobe binaries should be placed in %s.
|
||||
|
||||
`
|
||||
logger.Errorf(msg, path)
|
||||
return err
|
||||
} else {
|
||||
// After download get new paths for ffmpeg and ffprobe
|
||||
ffmpegPath, ffprobePath = ffmpeg.GetPaths(paths)
|
||||
}
|
||||
}
|
||||
|
||||
s.FFMpeg = ffmpeg.NewEncoder(ffmpegPath)
|
||||
s.FFProbe = ffmpeg.FFProbe(ffprobePath)
|
||||
|
||||
s.FFMpeg.InitHWSupport(ctx)
|
||||
s.RefreshStreamManager()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -4,130 +4,44 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/desktop"
|
||||
"github.com/stashapp/stash/internal/dlna"
|
||||
"github.com/stashapp/stash/internal/log"
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
file_image "github.com/stashapp/stash/pkg/file/image"
|
||||
"github.com/stashapp/stash/pkg/file/video"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/job"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/paths"
|
||||
"github.com/stashapp/stash/pkg/pkg"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/session"
|
||||
"github.com/stashapp/stash/pkg/sqlite"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
"github.com/stashapp/stash/ui"
|
||||
|
||||
// register custom migrations
|
||||
_ "github.com/stashapp/stash/pkg/sqlite/migrations"
|
||||
)
|
||||
|
||||
type SystemStatus struct {
|
||||
DatabaseSchema *int `json:"databaseSchema"`
|
||||
DatabasePath *string `json:"databasePath"`
|
||||
ConfigPath *string `json:"configPath"`
|
||||
AppSchema int `json:"appSchema"`
|
||||
Status SystemStatusEnum `json:"status"`
|
||||
Os string `json:"os"`
|
||||
WorkingDir string `json:"working_dir"`
|
||||
HomeDir string `json:"home_dir"`
|
||||
}
|
||||
|
||||
type SystemStatusEnum string
|
||||
|
||||
const (
|
||||
SystemStatusEnumSetup SystemStatusEnum = "SETUP"
|
||||
SystemStatusEnumNeedsMigration SystemStatusEnum = "NEEDS_MIGRATION"
|
||||
SystemStatusEnumOk SystemStatusEnum = "OK"
|
||||
)
|
||||
|
||||
var AllSystemStatusEnum = []SystemStatusEnum{
|
||||
SystemStatusEnumSetup,
|
||||
SystemStatusEnumNeedsMigration,
|
||||
SystemStatusEnumOk,
|
||||
}
|
||||
|
||||
func (e SystemStatusEnum) IsValid() bool {
|
||||
switch e {
|
||||
case SystemStatusEnumSetup, SystemStatusEnumNeedsMigration, SystemStatusEnumOk:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e SystemStatusEnum) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *SystemStatusEnum) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = SystemStatusEnum(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid SystemStatusEnum", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e SystemStatusEnum) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type SetupInput struct {
|
||||
// Empty to indicate $HOME/.stash/config.yml default
|
||||
ConfigLocation string `json:"configLocation"`
|
||||
Stashes []*config.StashConfigInput `json:"stashes"`
|
||||
// Empty to indicate default
|
||||
DatabaseFile string `json:"databaseFile"`
|
||||
// Empty to indicate default
|
||||
GeneratedLocation string `json:"generatedLocation"`
|
||||
// Empty to indicate default
|
||||
CacheLocation string `json:"cacheLocation"`
|
||||
|
||||
StoreBlobsInDatabase bool `json:"storeBlobsInDatabase"`
|
||||
// Empty to indicate default
|
||||
BlobsLocation string `json:"blobsLocation"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
Config *config.Instance
|
||||
Config *config.Config
|
||||
Logger *log.Logger
|
||||
|
||||
Paths *paths.Paths
|
||||
|
||||
FFMPEG *ffmpeg.FFMpeg
|
||||
FFMpeg *ffmpeg.FFMpeg
|
||||
FFProbe ffmpeg.FFProbe
|
||||
StreamManager *ffmpeg.StreamManager
|
||||
|
||||
JobManager *job.Manager
|
||||
ReadLockManager *fsutil.ReadLockManager
|
||||
|
||||
SessionStore *session.Store
|
||||
|
||||
JobManager *job.Manager
|
||||
DownloadStore *DownloadStore
|
||||
SessionStore *session.Store
|
||||
|
||||
PluginCache *plugin.Cache
|
||||
ScraperCache *scraper.Cache
|
||||
|
|
@ -135,8 +49,6 @@ type Manager struct {
|
|||
PluginPackageManager *pkg.Manager
|
||||
ScraperPackageManager *pkg.Manager
|
||||
|
||||
DownloadStore *DownloadStore
|
||||
|
||||
DLNAService *dlna.Service
|
||||
|
||||
Database *sqlite.Database
|
||||
|
|
@ -146,378 +58,18 @@ type Manager struct {
|
|||
ImageService ImageService
|
||||
GalleryService GalleryService
|
||||
|
||||
Scanner *file.Scanner
|
||||
Cleaner *file.Cleaner
|
||||
|
||||
scanSubs *subscriptionManager
|
||||
}
|
||||
|
||||
var instance *Manager
|
||||
var once sync.Once
|
||||
|
||||
func GetInstance() *Manager {
|
||||
if _, err := Initialize(); err != nil {
|
||||
panic(err)
|
||||
if instance == nil {
|
||||
panic("manager not initialized")
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
func Initialize() (*Manager, error) {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
err = initialize()
|
||||
})
|
||||
|
||||
return instance, err
|
||||
}
|
||||
|
||||
func initialize() error {
|
||||
ctx := context.TODO()
|
||||
cfg, err := config.Initialize()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing configuration: %w", err)
|
||||
}
|
||||
|
||||
l := initLog()
|
||||
initProfiling(cfg.GetCPUProfilePath())
|
||||
|
||||
db := sqlite.NewDatabase()
|
||||
repo := db.Repository()
|
||||
|
||||
// start with empty paths
|
||||
emptyPaths := paths.Paths{}
|
||||
|
||||
instance = &Manager{
|
||||
Config: cfg,
|
||||
Logger: l,
|
||||
ReadLockManager: fsutil.NewReadLockManager(),
|
||||
DownloadStore: NewDownloadStore(),
|
||||
PluginCache: plugin.NewCache(cfg),
|
||||
|
||||
Database: db,
|
||||
Repository: repo,
|
||||
Paths: &emptyPaths,
|
||||
|
||||
scanSubs: &subscriptionManager{},
|
||||
}
|
||||
|
||||
instance.SceneService = &scene.Service{
|
||||
File: repo.File,
|
||||
Repository: repo.Scene,
|
||||
MarkerRepository: repo.SceneMarker,
|
||||
PluginCache: instance.PluginCache,
|
||||
Paths: instance.Paths,
|
||||
Config: cfg,
|
||||
}
|
||||
|
||||
instance.ImageService = &image.Service{
|
||||
File: repo.File,
|
||||
Repository: repo.Image,
|
||||
}
|
||||
|
||||
instance.GalleryService = &gallery.Service{
|
||||
Repository: repo.Gallery,
|
||||
ImageFinder: repo.Image,
|
||||
ImageService: instance.ImageService,
|
||||
File: repo.File,
|
||||
Folder: repo.Folder,
|
||||
}
|
||||
|
||||
instance.JobManager = initJobManager()
|
||||
|
||||
sceneServer := SceneServer{
|
||||
TxnManager: repo.TxnManager,
|
||||
SceneCoverGetter: repo.Scene,
|
||||
}
|
||||
|
||||
dlnaRepository := dlna.NewRepository(repo)
|
||||
instance.DLNAService = dlna.NewService(dlnaRepository, cfg, &sceneServer)
|
||||
|
||||
instance.RefreshPluginSourceManager()
|
||||
instance.RefreshScraperSourceManager()
|
||||
|
||||
if !cfg.IsNewSystem() {
|
||||
logger.Infof("using config file: %s", cfg.GetConfigFile())
|
||||
|
||||
if err == nil {
|
||||
err = cfg.Validate()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error initializing configuration: %w", err)
|
||||
}
|
||||
|
||||
if err := instance.PostInit(ctx); err != nil {
|
||||
var migrationNeededErr *sqlite.MigrationNeededError
|
||||
if errors.As(err, &migrationNeededErr) {
|
||||
logger.Warn(err.Error())
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
initSecurity(cfg)
|
||||
} else {
|
||||
cfgFile := cfg.GetConfigFile()
|
||||
if cfgFile != "" {
|
||||
cfgFile += " "
|
||||
}
|
||||
|
||||
// create temporary session store - this will be re-initialised
|
||||
// after config is complete
|
||||
instance.SessionStore = session.NewStore(cfg)
|
||||
|
||||
logger.Warnf("config file %snot found. Assuming new system...", cfgFile)
|
||||
}
|
||||
|
||||
if err = initFFMPEG(ctx); err != nil {
|
||||
logger.Warnf("could not initialize FFMPEG subsystem: %v", err)
|
||||
}
|
||||
|
||||
instance.Scanner = makeScanner(repo, instance.PluginCache)
|
||||
instance.Cleaner = makeCleaner(repo, instance.PluginCache)
|
||||
|
||||
// if DLNA is enabled, start it now
|
||||
if instance.Config.GetDLNADefaultEnabled() {
|
||||
if err := instance.DLNAService.Start(nil); err != nil {
|
||||
logger.Warnf("could not start DLNA service: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initialisePackageManager(localPath string, srcPathGetter pkg.SourcePathGetter) *pkg.Manager {
|
||||
const timeout = 10 * time.Second
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
return &pkg.Manager{
|
||||
Local: &pkg.Store{
|
||||
BaseDir: localPath,
|
||||
ManifestFile: pkg.ManifestFile,
|
||||
},
|
||||
PackagePathGetter: srcPathGetter,
|
||||
Client: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
func videoFileFilter(ctx context.Context, f models.File) bool {
|
||||
return useAsVideo(f.Base().Path)
|
||||
}
|
||||
|
||||
func imageFileFilter(ctx context.Context, f models.File) bool {
|
||||
return useAsImage(f.Base().Path)
|
||||
}
|
||||
|
||||
func galleryFileFilter(ctx context.Context, f models.File) bool {
|
||||
return isZip(f.Base().Basename)
|
||||
}
|
||||
|
||||
func makeScanner(repo models.Repository, pluginCache *plugin.Cache) *file.Scanner {
|
||||
return &file.Scanner{
|
||||
Repository: file.NewRepository(repo),
|
||||
FileDecorators: []file.Decorator{
|
||||
&file.FilteredDecorator{
|
||||
Decorator: &video.Decorator{
|
||||
FFProbe: instance.FFProbe,
|
||||
},
|
||||
Filter: file.FilterFunc(videoFileFilter),
|
||||
},
|
||||
&file.FilteredDecorator{
|
||||
Decorator: &file_image.Decorator{
|
||||
FFProbe: instance.FFProbe,
|
||||
},
|
||||
Filter: file.FilterFunc(imageFileFilter),
|
||||
},
|
||||
},
|
||||
FingerprintCalculator: &fingerprintCalculator{instance.Config},
|
||||
FS: &file.OsFS{},
|
||||
}
|
||||
}
|
||||
|
||||
func makeCleaner(repo models.Repository, pluginCache *plugin.Cache) *file.Cleaner {
|
||||
return &file.Cleaner{
|
||||
FS: &file.OsFS{},
|
||||
Repository: file.NewRepository(repo),
|
||||
Handlers: []file.CleanHandler{
|
||||
&cleanHandler{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func initJobManager() *job.Manager {
|
||||
ret := job.NewManager()
|
||||
|
||||
// desktop notifications
|
||||
ctx := context.Background()
|
||||
c := ret.Subscribe(context.Background())
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case j := <-c.RemovedJob:
|
||||
if instance.Config.GetNotificationsEnabled() {
|
||||
cleanDesc := strings.TrimRight(j.Description, ".")
|
||||
|
||||
if j.StartTime == nil {
|
||||
// Task was never started
|
||||
return
|
||||
}
|
||||
|
||||
timeElapsed := j.EndTime.Sub(*j.StartTime)
|
||||
desktop.SendNotification("Task Finished", "Task \""+cleanDesc+"\" is finished in "+formatDuration(timeElapsed)+".")
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func formatDuration(t time.Duration) string {
|
||||
return fmt.Sprintf("%02.f:%02.f:%02.f", t.Hours(), t.Minutes(), t.Seconds())
|
||||
}
|
||||
|
||||
func initSecurity(cfg *config.Instance) {
|
||||
if err := session.CheckExternalAccessTripwire(cfg); err != nil {
|
||||
session.LogExternalAccessError(*err)
|
||||
}
|
||||
}
|
||||
|
||||
func initProfiling(cpuProfilePath string) {
|
||||
if cpuProfilePath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := os.Create(cpuProfilePath)
|
||||
if err != nil {
|
||||
logger.Fatalf("unable to create cpu profile file: %s", err.Error())
|
||||
}
|
||||
|
||||
logger.Infof("profiling to %s", cpuProfilePath)
|
||||
|
||||
// StopCPUProfile is defer called in main
|
||||
if err = pprof.StartCPUProfile(f); err != nil {
|
||||
logger.Warnf("could not start CPU profiling: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func initFFMPEG(ctx context.Context) error {
|
||||
// only do this if we have a config file set
|
||||
if instance.Config.GetConfigFile() != "" {
|
||||
// use same directory as config path
|
||||
configDirectory := instance.Config.GetConfigPath()
|
||||
paths := []string{
|
||||
configDirectory,
|
||||
paths.GetStashHomeDirectory(),
|
||||
}
|
||||
ffmpegPath, ffprobePath := ffmpeg.GetPaths(paths)
|
||||
|
||||
if ffmpegPath == "" || ffprobePath == "" {
|
||||
logger.Infof("couldn't find FFMPEG, attempting to download it")
|
||||
if err := ffmpeg.Download(ctx, configDirectory); err != nil {
|
||||
msg := `Unable to locate / automatically download FFMPEG
|
||||
|
||||
Check the readme for download links.
|
||||
The FFMPEG and FFProbe binaries should be placed in %s
|
||||
|
||||
The error was: %s
|
||||
`
|
||||
logger.Errorf(msg, configDirectory, err)
|
||||
return err
|
||||
} else {
|
||||
// After download get new paths for ffmpeg and ffprobe
|
||||
ffmpegPath, ffprobePath = ffmpeg.GetPaths(paths)
|
||||
}
|
||||
}
|
||||
|
||||
instance.FFMPEG = ffmpeg.NewEncoder(ffmpegPath)
|
||||
instance.FFProbe = ffmpeg.FFProbe(ffprobePath)
|
||||
|
||||
instance.FFMPEG.InitHWSupport(ctx)
|
||||
instance.RefreshStreamManager()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initLog() *log.Logger {
|
||||
config := config.GetInstance()
|
||||
l := log.NewLogger()
|
||||
l.Init(config.GetLogFile(), config.GetLogOut(), config.GetLogLevel())
|
||||
logger.Logger = l
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// PostInit initialises the paths, caches and txnManager after the initial
|
||||
// configuration has been set. Should only be called if the configuration
|
||||
// is valid.
|
||||
func (s *Manager) PostInit(ctx context.Context) error {
|
||||
if err := s.Config.SetInitialConfig(); err != nil {
|
||||
logger.Warnf("could not set initial configuration: %v", err)
|
||||
}
|
||||
|
||||
*s.Paths = paths.NewPaths(s.Config.GetGeneratedPath(), s.Config.GetBlobsPath())
|
||||
s.RefreshConfig()
|
||||
s.SessionStore = session.NewStore(s.Config)
|
||||
s.PluginCache.RegisterSessionStore(s.SessionStore)
|
||||
|
||||
if err := s.PluginCache.LoadPlugins(); err != nil {
|
||||
logger.Errorf("Error reading plugin configs: %s", err.Error())
|
||||
}
|
||||
|
||||
s.SetBlobStoreOptions()
|
||||
|
||||
s.ScraperCache = instance.initScraperCache()
|
||||
writeStashIcon()
|
||||
|
||||
// clear the downloads and tmp directories
|
||||
// #1021 - only clear these directories if the generated folder is non-empty
|
||||
if s.Config.GetGeneratedPath() != "" {
|
||||
const deleteTimeout = 1 * time.Second
|
||||
|
||||
utils.Timeout(func() {
|
||||
if err := fsutil.EmptyDir(instance.Paths.Generated.Downloads); err != nil {
|
||||
logger.Warnf("could not empty Downloads directory: %v", err)
|
||||
}
|
||||
if err := fsutil.EnsureDir(instance.Paths.Generated.Tmp); err != nil {
|
||||
logger.Warnf("could not create Tmp directory: %v", err)
|
||||
} else {
|
||||
if err := fsutil.EmptyDir(instance.Paths.Generated.Tmp); err != nil {
|
||||
logger.Warnf("could not empty Tmp directory: %v", err)
|
||||
}
|
||||
}
|
||||
}, deleteTimeout, func(done chan struct{}) {
|
||||
logger.Info("Please wait. Deleting temporary files...") // print
|
||||
<-done // and wait for deletion
|
||||
logger.Info("Temporary files deleted.")
|
||||
})
|
||||
}
|
||||
|
||||
database := s.Database
|
||||
if err := database.Open(s.Config.GetDatabasePath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the proxy if defined in config
|
||||
if s.Config.GetProxy() != "" {
|
||||
os.Setenv("HTTP_PROXY", s.Config.GetProxy())
|
||||
os.Setenv("HTTPS_PROXY", s.Config.GetProxy())
|
||||
os.Setenv("NO_PROXY", s.Config.GetNoProxy())
|
||||
logger.Info("Using HTTP Proxy")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Manager) SetBlobStoreOptions() {
|
||||
storageType := s.Config.GetBlobsStorage()
|
||||
blobsPath := s.Config.GetBlobsPath()
|
||||
|
|
@ -529,59 +81,45 @@ func (s *Manager) SetBlobStoreOptions() {
|
|||
})
|
||||
}
|
||||
|
||||
func writeStashIcon() {
|
||||
iconPath := filepath.Join(instance.Config.GetConfigPath(), "icon.png")
|
||||
err := os.WriteFile(iconPath, ui.FaviconProvider.GetFaviconPng(), 0644)
|
||||
if err != nil {
|
||||
logger.Errorf("Couldn't write icon file: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// initScraperCache initializes a new scraper cache and returns it.
|
||||
func (s *Manager) initScraperCache() *scraper.Cache {
|
||||
scraperRepository := scraper.NewRepository(s.Repository)
|
||||
ret, err := scraper.NewCache(s.Config, scraperRepository)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("Error reading scraper configs: %s", err.Error())
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Manager) RefreshConfig() {
|
||||
*s.Paths = paths.NewPaths(s.Config.GetGeneratedPath(), s.Config.GetBlobsPath())
|
||||
config := s.Config
|
||||
if config.Validate() == nil {
|
||||
cfg := s.Config
|
||||
*s.Paths = paths.NewPaths(cfg.GetGeneratedPath(), cfg.GetBlobsPath())
|
||||
if cfg.Validate() == nil {
|
||||
if err := fsutil.EnsureDir(s.Paths.Generated.Screenshots); err != nil {
|
||||
logger.Warnf("could not create directory for Screenshots: %v", err)
|
||||
logger.Warnf("could not create screenshots directory: %v", err)
|
||||
}
|
||||
if err := fsutil.EnsureDir(s.Paths.Generated.Vtt); err != nil {
|
||||
logger.Warnf("could not create directory for VTT: %v", err)
|
||||
logger.Warnf("could not create VTT directory: %v", err)
|
||||
}
|
||||
if err := fsutil.EnsureDir(s.Paths.Generated.Markers); err != nil {
|
||||
logger.Warnf("could not create directory for Markers: %v", err)
|
||||
logger.Warnf("could not create markers directory: %v", err)
|
||||
}
|
||||
if err := fsutil.EnsureDir(s.Paths.Generated.Transcodes); err != nil {
|
||||
logger.Warnf("could not create directory for Transcodes: %v", err)
|
||||
logger.Warnf("could not create transcodes directory: %v", err)
|
||||
}
|
||||
if err := fsutil.EnsureDir(s.Paths.Generated.Downloads); err != nil {
|
||||
logger.Warnf("could not create directory for Downloads: %v", err)
|
||||
logger.Warnf("could not create downloads directory: %v", err)
|
||||
}
|
||||
if err := fsutil.EnsureDir(s.Paths.Generated.InteractiveHeatmap); err != nil {
|
||||
logger.Warnf("could not create directory for Interactive Heatmaps: %v", err)
|
||||
logger.Warnf("could not create interactive heatmaps directory: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshScraperCache refreshes the scraper cache. Call this when scraper
|
||||
// configuration changes.
|
||||
func (s *Manager) RefreshScraperCache() {
|
||||
s.ScraperCache = s.initScraperCache()
|
||||
// RefreshPluginCache refreshes the plugin cache.
|
||||
// Call this when the plugin configuration changes.
|
||||
func (s *Manager) RefreshPluginCache() {
|
||||
s.PluginCache.ReloadPlugins()
|
||||
}
|
||||
|
||||
// RefreshStreamManager refreshes the stream manager. Call this when cache directory
|
||||
// changes.
|
||||
// RefreshScraperCache refreshes the scraper cache.
|
||||
// Call this when the scraper configuration changes.
|
||||
func (s *Manager) RefreshScraperCache() {
|
||||
s.ScraperCache.ReloadScrapers()
|
||||
}
|
||||
|
||||
// RefreshStreamManager refreshes the stream manager.
|
||||
// Call this when the cache directory changes.
|
||||
func (s *Manager) RefreshStreamManager() {
|
||||
// shutdown existing manager if needed
|
||||
if s.StreamManager != nil {
|
||||
|
|
@ -589,8 +127,22 @@ func (s *Manager) RefreshStreamManager() {
|
|||
s.StreamManager = nil
|
||||
}
|
||||
|
||||
cacheDir := s.Config.GetCachePath()
|
||||
s.StreamManager = ffmpeg.NewStreamManager(cacheDir, s.FFMPEG, s.FFProbe, s.Config, s.ReadLockManager)
|
||||
cfg := s.Config
|
||||
cacheDir := cfg.GetCachePath()
|
||||
s.StreamManager = ffmpeg.NewStreamManager(cacheDir, s.FFMpeg, s.FFProbe, cfg, s.ReadLockManager)
|
||||
}
|
||||
|
||||
// RefreshDLNA starts/stops the DLNA service as needed.
|
||||
func (s *Manager) RefreshDLNA() {
|
||||
dlnaService := s.DLNAService
|
||||
enabled := s.Config.GetDLNADefaultEnabled()
|
||||
if !enabled && dlnaService.IsRunning() {
|
||||
dlnaService.Stop(nil)
|
||||
} else if enabled && !dlnaService.IsRunning() {
|
||||
if err := dlnaService.Start(nil); err != nil {
|
||||
logger.Warnf("error starting DLNA service: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Manager) RefreshScraperSourceManager() {
|
||||
|
|
@ -625,7 +177,11 @@ func setSetupDefaults(input *SetupInput) {
|
|||
|
||||
func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
||||
setSetupDefaults(&input)
|
||||
c := s.Config
|
||||
cfg := s.Config
|
||||
|
||||
if err := cfg.SetInitialConfig(); err != nil {
|
||||
return fmt.Errorf("error setting initial configuration: %v", err)
|
||||
}
|
||||
|
||||
// create the config directory if it does not exist
|
||||
// don't do anything if config is already set in the environment
|
||||
|
|
@ -652,7 +208,7 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
|||
}
|
||||
|
||||
// create the generated directory if it does not exist
|
||||
if !c.HasOverride(config.Generated) {
|
||||
if !cfg.HasOverride(config.Generated) {
|
||||
if exists, _ := fsutil.DirExists(input.GeneratedLocation); !exists {
|
||||
if err := os.MkdirAll(input.GeneratedLocation, 0755); err != nil {
|
||||
return fmt.Errorf("error creating generated directory: %v", err)
|
||||
|
|
@ -663,75 +219,60 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
|||
}
|
||||
|
||||
// create the cache directory if it does not exist
|
||||
if !c.HasOverride(config.Cache) {
|
||||
if !cfg.HasOverride(config.Cache) {
|
||||
if exists, _ := fsutil.DirExists(input.CacheLocation); !exists {
|
||||
if err := os.MkdirAll(input.CacheLocation, 0755); err != nil {
|
||||
return fmt.Errorf("error creating cache directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.Config.Set(config.Cache, input.CacheLocation)
|
||||
cfg.Set(config.Cache, input.CacheLocation)
|
||||
}
|
||||
|
||||
if input.StoreBlobsInDatabase {
|
||||
s.Config.Set(config.BlobsStorage, config.BlobStorageTypeDatabase)
|
||||
cfg.Set(config.BlobsStorage, config.BlobStorageTypeDatabase)
|
||||
} else {
|
||||
if !c.HasOverride(config.BlobsPath) {
|
||||
if !cfg.HasOverride(config.BlobsPath) {
|
||||
if exists, _ := fsutil.DirExists(input.BlobsLocation); !exists {
|
||||
if err := os.MkdirAll(input.BlobsLocation, 0755); err != nil {
|
||||
return fmt.Errorf("error creating blobs directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.Config.Set(config.BlobsPath, input.BlobsLocation)
|
||||
cfg.Set(config.BlobsPath, input.BlobsLocation)
|
||||
}
|
||||
|
||||
s.Config.Set(config.BlobsStorage, config.BlobStorageTypeFilesystem)
|
||||
cfg.Set(config.BlobsStorage, config.BlobStorageTypeFilesystem)
|
||||
}
|
||||
|
||||
// set the configuration
|
||||
if !c.HasOverride(config.Database) {
|
||||
s.Config.Set(config.Database, input.DatabaseFile)
|
||||
if !cfg.HasOverride(config.Database) {
|
||||
cfg.Set(config.Database, input.DatabaseFile)
|
||||
}
|
||||
|
||||
s.Config.Set(config.Stash, input.Stashes)
|
||||
if err := s.Config.Write(); err != nil {
|
||||
cfg.Set(config.Stash, input.Stashes)
|
||||
|
||||
if err := cfg.Write(); err != nil {
|
||||
return fmt.Errorf("error writing configuration file: %v", err)
|
||||
}
|
||||
|
||||
// initialise the database
|
||||
if err := s.PostInit(ctx); err != nil {
|
||||
var migrationNeededErr *sqlite.MigrationNeededError
|
||||
if errors.As(err, &migrationNeededErr) {
|
||||
logger.Warn(err.Error())
|
||||
} else {
|
||||
return fmt.Errorf("error initializing the database: %v", err)
|
||||
}
|
||||
// finish initialization
|
||||
if err := s.postInit(ctx); err != nil {
|
||||
return fmt.Errorf("error completing initialization: %v", err)
|
||||
}
|
||||
|
||||
s.Config.FinalizeSetup()
|
||||
|
||||
if err := initFFMPEG(ctx); err != nil {
|
||||
return fmt.Errorf("error initializing FFMPEG subsystem: %v", err)
|
||||
}
|
||||
|
||||
instance.Scanner = makeScanner(instance.Repository, instance.PluginCache)
|
||||
cfg.FinalizeSetup()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Manager) validateFFMPEG() error {
|
||||
if s.FFMPEG == nil || s.FFProbe == "" {
|
||||
func (s *Manager) validateFFmpeg() error {
|
||||
if s.FFMpeg == nil || s.FFProbe == "" {
|
||||
return errors.New("missing ffmpeg and/or ffprobe")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type MigrateInput struct {
|
||||
BackupPath string `json:"backupPath"`
|
||||
}
|
||||
|
||||
func (s *Manager) Migrate(ctx context.Context, input MigrateInput) error {
|
||||
database := s.Database
|
||||
|
||||
|
|
@ -778,6 +319,76 @@ func (s *Manager) Migrate(ctx context.Context, input MigrateInput) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Manager) BackupDatabase(download bool) (string, string, error) {
|
||||
var backupPath string
|
||||
var backupName string
|
||||
if download {
|
||||
backupDir := s.Paths.Generated.Downloads
|
||||
if err := fsutil.EnsureDir(backupDir); err != nil {
|
||||
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
|
||||
}
|
||||
f, err := os.CreateTemp(backupDir, "backup*.sqlite")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
backupPath = f.Name()
|
||||
backupName = s.Database.DatabaseBackupPath("")
|
||||
f.Close()
|
||||
} else {
|
||||
backupDir := s.Config.GetBackupDirectoryPathOrDefault()
|
||||
if backupDir != "" {
|
||||
if err := fsutil.EnsureDir(backupDir); err != nil {
|
||||
return "", "", fmt.Errorf("could not create backup directory %v: %w", backupDir, err)
|
||||
}
|
||||
}
|
||||
backupPath = s.Database.DatabaseBackupPath(backupDir)
|
||||
backupName = filepath.Base(backupPath)
|
||||
}
|
||||
|
||||
err := s.Database.Backup(backupPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return backupPath, backupName, nil
|
||||
}
|
||||
|
||||
func (s *Manager) AnonymiseDatabase(download bool) (string, string, error) {
|
||||
var outPath string
|
||||
var outName string
|
||||
if download {
|
||||
outDir := s.Paths.Generated.Downloads
|
||||
if err := fsutil.EnsureDir(outDir); err != nil {
|
||||
return "", "", fmt.Errorf("could not create output directory %v: %w", outDir, err)
|
||||
}
|
||||
f, err := os.CreateTemp(outDir, "anonymous*.sqlite")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
outPath = f.Name()
|
||||
outName = s.Database.AnonymousDatabasePath("")
|
||||
f.Close()
|
||||
} else {
|
||||
outDir := s.Config.GetBackupDirectoryPathOrDefault()
|
||||
if outDir != "" {
|
||||
if err := fsutil.EnsureDir(outDir); err != nil {
|
||||
return "", "", fmt.Errorf("could not create output directory %v: %w", outDir, err)
|
||||
}
|
||||
}
|
||||
outPath = s.Database.AnonymousDatabasePath(outDir)
|
||||
outName = filepath.Base(outPath)
|
||||
}
|
||||
|
||||
err := s.Database.Anonymise(outPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return outPath, outName, nil
|
||||
}
|
||||
|
||||
func (s *Manager) GetSystemStatus() *SystemStatus {
|
||||
workingDir := fsutil.GetWorkingDirectory()
|
||||
homeDir := fsutil.GetHomeDirectory()
|
||||
|
|
@ -809,24 +420,16 @@ func (s *Manager) GetSystemStatus() *SystemStatus {
|
|||
}
|
||||
|
||||
// Shutdown gracefully stops the manager
|
||||
func (s *Manager) Shutdown(code int) {
|
||||
// stop any profiling at exit
|
||||
pprof.StopCPUProfile()
|
||||
func (s *Manager) Shutdown() {
|
||||
// TODO: Each part of the manager needs to gracefully stop at some point
|
||||
|
||||
if s.StreamManager != nil {
|
||||
s.StreamManager.Shutdown()
|
||||
s.StreamManager = nil
|
||||
}
|
||||
|
||||
// TODO: Each part of the manager needs to gracefully stop at some point
|
||||
// for now, we just close the database.
|
||||
err := s.Database.Close()
|
||||
if err != nil {
|
||||
logger.Errorf("Error closing database: %s", err)
|
||||
if code == 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/file"
|
||||
file_image "github.com/stashapp/stash/pkg/file/image"
|
||||
"github.com/stashapp/stash/pkg/file/video"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/job"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
|
|
@ -90,12 +93,32 @@ type ScanMetaDataFilterInput struct {
|
|||
}
|
||||
|
||||
func (s *Manager) Scan(ctx context.Context, input ScanMetadataInput) (int, error) {
|
||||
if err := s.validateFFMPEG(); err != nil {
|
||||
if err := s.validateFFmpeg(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
scanner := &file.Scanner{
|
||||
Repository: file.NewRepository(s.Repository),
|
||||
FileDecorators: []file.Decorator{
|
||||
&file.FilteredDecorator{
|
||||
Decorator: &video.Decorator{
|
||||
FFProbe: s.FFProbe,
|
||||
},
|
||||
Filter: file.FilterFunc(videoFileFilter),
|
||||
},
|
||||
&file.FilteredDecorator{
|
||||
Decorator: &file_image.Decorator{
|
||||
FFProbe: s.FFProbe,
|
||||
},
|
||||
Filter: file.FilterFunc(imageFileFilter),
|
||||
},
|
||||
},
|
||||
FingerprintCalculator: &fingerprintCalculator{s.Config},
|
||||
FS: &file.OsFS{},
|
||||
}
|
||||
|
||||
scanJob := ScanJob{
|
||||
scanner: s.Scanner,
|
||||
scanner: scanner,
|
||||
input: input,
|
||||
subscriptions: s.scanSubs,
|
||||
}
|
||||
|
|
@ -160,7 +183,7 @@ func (s *Manager) RunSingleTask(ctx context.Context, t Task) int {
|
|||
}
|
||||
|
||||
func (s *Manager) Generate(ctx context.Context, input GenerateMetadataInput) (int, error) {
|
||||
if err := s.validateFFMPEG(); err != nil {
|
||||
if err := s.validateFFmpeg(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := instance.Paths.Generated.EnsureTmpDir(); err != nil {
|
||||
|
|
@ -254,8 +277,16 @@ type CleanMetadataInput struct {
|
|||
}
|
||||
|
||||
func (s *Manager) Clean(ctx context.Context, input CleanMetadataInput) int {
|
||||
cleaner := &file.Cleaner{
|
||||
FS: &file.OsFS{},
|
||||
Repository: file.NewRepository(s.Repository),
|
||||
Handlers: []file.CleanHandler{
|
||||
&cleanHandler{},
|
||||
},
|
||||
}
|
||||
|
||||
j := cleanJob{
|
||||
cleaner: s.Cleaner,
|
||||
cleaner: cleaner,
|
||||
repository: s.Repository,
|
||||
sceneService: s.SceneService,
|
||||
imageService: s.ImageService,
|
||||
|
|
|
|||
36
internal/manager/models.go
Normal file
36
internal/manager/models.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package manager
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
)
|
||||
|
||||
type SystemStatus struct {
|
||||
DatabaseSchema *int `json:"databaseSchema"`
|
||||
DatabasePath *string `json:"databasePath"`
|
||||
ConfigPath *string `json:"configPath"`
|
||||
AppSchema int `json:"appSchema"`
|
||||
Status SystemStatusEnum `json:"status"`
|
||||
Os string `json:"os"`
|
||||
WorkingDir string `json:"working_dir"`
|
||||
HomeDir string `json:"home_dir"`
|
||||
}
|
||||
|
||||
type SetupInput struct {
|
||||
// Empty to indicate $HOME/.stash/config.yml default
|
||||
ConfigLocation string `json:"configLocation"`
|
||||
Stashes []*config.StashConfigInput `json:"stashes"`
|
||||
// Empty to indicate default
|
||||
DatabaseFile string `json:"databaseFile"`
|
||||
// Empty to indicate default
|
||||
GeneratedLocation string `json:"generatedLocation"`
|
||||
// Empty to indicate default
|
||||
CacheLocation string `json:"cacheLocation"`
|
||||
|
||||
StoreBlobsInDatabase bool `json:"storeBlobsInDatabase"`
|
||||
// Empty to indicate default
|
||||
BlobsLocation string `json:"blobsLocation"`
|
||||
}
|
||||
|
||||
type MigrateInput struct {
|
||||
BackupPath string `json:"backupPath"`
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ type cleanFilter struct {
|
|||
scanFilter
|
||||
}
|
||||
|
||||
func newCleanFilter(c *config.Instance) *cleanFilter {
|
||||
func newCleanFilter(c *config.Config) *cleanFilter {
|
||||
return &cleanFilter{
|
||||
scanFilter: scanFilter{
|
||||
extensionConfig: newExtensionConfig(c),
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) {
|
|||
}
|
||||
|
||||
g := &generate.Generator{
|
||||
Encoder: instance.FFMPEG,
|
||||
Encoder: instance.FFMpeg,
|
||||
FFMpegConfig: instance.Config,
|
||||
LockManager: instance.ReadLockManager,
|
||||
MarkerPaths: instance.Paths.SceneMarkers,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func (t *GenerateClipPreviewTask) Start(ctx context.Context) {
|
|||
Preset: GetInstance().Config.GetPreviewPreset().String(),
|
||||
}
|
||||
|
||||
encoder := image.NewThumbnailEncoder(GetInstance().FFMPEG, GetInstance().FFProbe, clipPreviewOptions)
|
||||
encoder := image.NewThumbnailEncoder(GetInstance().FFMpeg, GetInstance().FFProbe, clipPreviewOptions)
|
||||
err := encoder.GetPreview(filePath, prevPath, models.DefaultGthumbWidth)
|
||||
if err != nil {
|
||||
logger.Errorf("getting preview for image %s: %w", filePath, err)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func (t *GeneratePhashTask) Start(ctx context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
hash, err := videophash.Generate(instance.FFMPEG, t.File)
|
||||
hash, err := videophash.Generate(instance.FFMpeg, t.File)
|
||||
if err != nil {
|
||||
logger.Errorf("error generating phash: %s", err.Error())
|
||||
logErrorOutput(err)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ func (t *GenerateCoverTask) Start(ctx context.Context) {
|
|||
logger.Debugf("Creating screenshot for %s", scenePath)
|
||||
|
||||
g := generate.Generator{
|
||||
Encoder: instance.FFMPEG,
|
||||
Encoder: instance.FFMpeg,
|
||||
FFMpegConfig: instance.Config,
|
||||
LockManager: instance.ReadLockManager,
|
||||
ScenePaths: instance.Paths.Scene,
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ type extensionConfig struct {
|
|||
zipExt []string
|
||||
}
|
||||
|
||||
func newExtensionConfig(c *config.Instance) extensionConfig {
|
||||
func newExtensionConfig(c *config.Config) extensionConfig {
|
||||
return extensionConfig{
|
||||
vidExt: c.GetVideoExtensions(),
|
||||
imgExt: c.GetImageExtensions(),
|
||||
|
|
@ -126,7 +126,7 @@ type handlerRequiredFilter struct {
|
|||
videoFileNamingAlgorithm models.HashAlgorithm
|
||||
}
|
||||
|
||||
func newHandlerRequiredFilter(c *config.Instance, repo models.Repository) *handlerRequiredFilter {
|
||||
func newHandlerRequiredFilter(c *config.Config, repo models.Repository) *handlerRequiredFilter {
|
||||
processes := c.GetParallelTasksWithAutoDetection()
|
||||
|
||||
return &handlerRequiredFilter{
|
||||
|
|
@ -239,7 +239,7 @@ type scanFilter struct {
|
|||
minModTime time.Time
|
||||
}
|
||||
|
||||
func newScanFilter(c *config.Instance, repo models.Repository, minModTime time.Time) *scanFilter {
|
||||
func newScanFilter(c *config.Config, repo models.Repository, minModTime time.Time) *scanFilter {
|
||||
return &scanFilter{
|
||||
extensionConfig: newExtensionConfig(c),
|
||||
txnManager: repo.TxnManager,
|
||||
|
|
@ -325,6 +325,18 @@ func (c *scanConfig) GetCreateGalleriesFromFolders() bool {
|
|||
return c.createGalleriesFromFolders
|
||||
}
|
||||
|
||||
func videoFileFilter(ctx context.Context, f models.File) bool {
|
||||
return useAsVideo(f.Base().Path)
|
||||
}
|
||||
|
||||
func imageFileFilter(ctx context.Context, f models.File) bool {
|
||||
return useAsImage(f.Base().Path)
|
||||
}
|
||||
|
||||
func galleryFileFilter(ctx context.Context, f models.File) bool {
|
||||
return isZip(f.Base().Basename)
|
||||
}
|
||||
|
||||
func getScanHandlers(options ScanMetadataInput, taskQueue *job.TaskQueue, progress *job.Progress) []file.Handler {
|
||||
mgr := GetInstance()
|
||||
c := mgr.Config
|
||||
|
|
@ -464,7 +476,7 @@ func (g *imageGenerators) generateThumbnail(ctx context.Context, i *models.Image
|
|||
Preset: c.GetPreviewPreset().String(),
|
||||
}
|
||||
|
||||
encoder := image.NewThumbnailEncoder(mgr.FFMPEG, mgr.FFProbe, clipPreviewOptions)
|
||||
encoder := image.NewThumbnailEncoder(mgr.FFMpeg, mgr.FFProbe, clipPreviewOptions)
|
||||
data, err := encoder.GetThumbnail(f, models.DefaultGthumbWidth)
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -547,7 +559,7 @@ func (g *sceneGenerators) Generate(ctx context.Context, s *models.Scene, f *mode
|
|||
options := getGeneratePreviewOptions(GeneratePreviewOptionsInput{})
|
||||
|
||||
generator := &generate.Generator{
|
||||
Encoder: mgr.FFMPEG,
|
||||
Encoder: mgr.FFMpeg,
|
||||
FFMpegConfig: mgr.Config,
|
||||
LockManager: mgr.ReadLockManager,
|
||||
MarkerPaths: g.paths.SceneMarkers,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ func GetPaths(paths []string) (string, string) {
|
|||
|
||||
// Check if ffmpeg exists in the config directory
|
||||
if ffmpegPath == "" {
|
||||
ffmpegPath = fsutil.FindInPaths(paths, getFFMPEGFilename())
|
||||
ffmpegPath = fsutil.FindInPaths(paths, getFFMpegFilename())
|
||||
}
|
||||
if ffprobePath == "" {
|
||||
ffprobePath = fsutil.FindInPaths(paths, getFFProbeFilename())
|
||||
|
|
@ -39,7 +39,7 @@ func GetPaths(paths []string) (string, string) {
|
|||
}
|
||||
|
||||
func Download(ctx context.Context, configDirectory string) error {
|
||||
for _, url := range getFFMPEGURL() {
|
||||
for _, url := range getFFmpegURL() {
|
||||
err := downloadSingle(ctx, configDirectory, url)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -47,7 +47,7 @@ func Download(ctx context.Context, configDirectory string) error {
|
|||
}
|
||||
|
||||
// validate that the urls contained what we needed
|
||||
executables := []string{getFFMPEGFilename(), getFFProbeFilename()}
|
||||
executables := []string{getFFMpegFilename(), getFFProbeFilename()}
|
||||
for _, executable := range executables {
|
||||
_, err := os.Stat(filepath.Join(configDirectory, executable))
|
||||
if err != nil {
|
||||
|
|
@ -173,7 +173,7 @@ func downloadSingle(ctx context.Context, configDirectory, url string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getFFMPEGURL() []string {
|
||||
func getFFmpegURL() []string {
|
||||
var urls []string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
|
|
@ -195,7 +195,7 @@ func getFFMPEGURL() []string {
|
|||
return urls
|
||||
}
|
||||
|
||||
func getFFMPEGFilename() string {
|
||||
func getFFMpegFilename() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "ffmpeg.exe"
|
||||
}
|
||||
|
|
@ -209,7 +209,7 @@ func getFFProbeFilename() string {
|
|||
return "ffprobe"
|
||||
}
|
||||
|
||||
// Checks if FFMPEG in the path has the correct flags
|
||||
// Checks if ffmpeg in the path has the correct flags
|
||||
func pathBinaryHasCorrectFlags() bool {
|
||||
ffmpegPath, err := exec.LookPath("ffmpeg")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/plugin/common"
|
||||
|
|
@ -123,46 +124,31 @@ func (c *Cache) RegisterSessionStore(sessionStore *session.Store) {
|
|||
c.sessionStore = sessionStore
|
||||
}
|
||||
|
||||
// LoadPlugins clears the plugin cache and loads from the plugin path.
|
||||
// In the event of an error during loading, the cache will be left empty.
|
||||
func (c *Cache) LoadPlugins() error {
|
||||
c.plugins = nil
|
||||
plugins, err := loadPlugins(c.config.GetPluginsPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.plugins = plugins
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadPlugins(path string) ([]Config, error) {
|
||||
// ReloadPlugins clears the plugin cache and loads from the plugin path.
|
||||
// If a plugin cannot be loaded, an error is logged and the plugin is skipped.
|
||||
func (c *Cache) ReloadPlugins() {
|
||||
path := c.config.GetPluginsPath()
|
||||
plugins := make([]Config, 0)
|
||||
|
||||
logger.Debugf("Reading plugin configs from %s", path)
|
||||
pluginFiles := []string{}
|
||||
err := filepath.Walk(path, func(fp string, f os.FileInfo, err error) error {
|
||||
|
||||
err := fsutil.SymWalk(path, func(fp string, f os.FileInfo, err error) error {
|
||||
if filepath.Ext(fp) == ".yml" {
|
||||
pluginFiles = append(pluginFiles, fp)
|
||||
plugin, err := loadPluginFromYAMLFile(fp)
|
||||
if err != nil {
|
||||
logger.Errorf("Error loading plugin %s: %v", fp, err)
|
||||
} else {
|
||||
plugins = append(plugins, *plugin)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
logger.Errorf("Error reading plugin configs: %v", err)
|
||||
}
|
||||
|
||||
for _, file := range pluginFiles {
|
||||
plugin, err := loadPluginFromYAMLFile(file)
|
||||
if err != nil {
|
||||
logger.Errorf("Error loading plugin %s: %s", file, err.Error())
|
||||
} else {
|
||||
plugins = append(plugins, *plugin)
|
||||
}
|
||||
}
|
||||
|
||||
return plugins, nil
|
||||
c.plugins = plugins
|
||||
}
|
||||
|
||||
func (c Cache) enabledPlugins() []Config {
|
||||
|
|
|
|||
|
|
@ -133,32 +133,27 @@ func newClient(gc GlobalConfig) *http.Client {
|
|||
return client
|
||||
}
|
||||
|
||||
// NewCache returns a new Cache loading scraper configurations from the
|
||||
// scraper path provided in the global config object. It returns a new
|
||||
// instance and an error if the scraper directory could not be loaded.
|
||||
// NewCache returns a new Cache.
|
||||
//
|
||||
// Scraper configurations are loaded from yml files in the provided scrapers
|
||||
// directory and any subdirectories.
|
||||
func NewCache(globalConfig GlobalConfig, repo Repository) (*Cache, error) {
|
||||
// Scraper configurations are loaded from yml files in the scrapers
|
||||
// directory in the config and any subdirectories.
|
||||
//
|
||||
// Does not load scrapers. Scrapers will need to be
|
||||
// loaded explicitly using ReloadScrapers.
|
||||
func NewCache(globalConfig GlobalConfig, repo Repository) *Cache {
|
||||
// HTTP Client setup
|
||||
client := newClient(globalConfig)
|
||||
|
||||
ret := &Cache{
|
||||
return &Cache{
|
||||
client: client,
|
||||
globalConfig: globalConfig,
|
||||
repository: repo,
|
||||
}
|
||||
|
||||
var err error
|
||||
ret.scrapers, err = ret.loadScrapers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (c *Cache) loadScrapers() (map[string]scraper, error) {
|
||||
// ReloadScrapers clears the scraper cache and reloads from the scraper path.
|
||||
// If a scraper cannot be loaded, an error is logged and the scraper is skipped.
|
||||
func (c *Cache) ReloadScrapers() {
|
||||
path := c.globalConfig.GetScrapersPath()
|
||||
scrapers := make(map[string]scraper)
|
||||
|
||||
|
|
@ -185,23 +180,9 @@ func (c *Cache) loadScrapers() (map[string]scraper, error) {
|
|||
|
||||
if err != nil {
|
||||
logger.Errorf("Error reading scraper configs: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return scrapers, nil
|
||||
}
|
||||
|
||||
// ReloadScrapers clears the scraper cache and reloads from the scraper path.
|
||||
// In the event of an error during loading, the cache will be left empty.
|
||||
func (c *Cache) ReloadScrapers() error {
|
||||
c.scrapers = nil
|
||||
scrapers, err := c.loadScrapers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.scrapers = scrapers
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListScrapers lists scrapers matching one of the given types.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/internal/manager/config"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sqlite"
|
||||
|
|
@ -535,6 +536,10 @@ func indexFromID(ids []int, id int) int {
|
|||
var db *sqlite.Database
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// initialise empty config - needed by some migrations
|
||||
_ = config.InitializeEmpty()
|
||||
|
||||
|
||||
ret := runTests(m)
|
||||
os.Exit(ret)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export const Setup: React.FC = () => {
|
|||
const [storeBlobsInDatabase, setStoreBlobsInDatabase] = useState(false);
|
||||
const [blobsLocation, setBlobsLocation] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [setupError, setSetupError] = useState("");
|
||||
const [setupError, setSetupError] = useState<string>();
|
||||
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
|
@ -617,7 +617,11 @@ export const Setup: React.FC = () => {
|
|||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Error) setSetupError(e.message ?? e.toString());
|
||||
if (e instanceof Error && e.message) {
|
||||
setSetupError(e.message);
|
||||
} else {
|
||||
setSetupError(String(e));
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
next();
|
||||
|
|
@ -737,6 +741,11 @@ export const Setup: React.FC = () => {
|
|||
}
|
||||
|
||||
function renderError() {
|
||||
function onBackClick() {
|
||||
setSetupError(undefined);
|
||||
goBack(2);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
|
|
@ -758,7 +767,7 @@ export const Setup: React.FC = () => {
|
|||
</section>
|
||||
<section className="mt-5">
|
||||
<div className="d-flex justify-content-center">
|
||||
<Button variant="secondary mx-2 p-5" onClick={() => goBack(2)}>
|
||||
<Button variant="secondary mx-2 p-5" onClick={onBackClick}>
|
||||
<FormattedMessage id="actions.previous_action" />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -851,7 +860,7 @@ export const Setup: React.FC = () => {
|
|||
}
|
||||
|
||||
function renderFinish() {
|
||||
if (setupError) {
|
||||
if (setupError !== undefined) {
|
||||
return renderError();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue