mirror of https://github.com/stashapp/stash.git
319 lines
7.8 KiB
Go
319 lines
7.8 KiB
Go
package manager
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/remeh/sizedwaitgroup"
|
|
"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/group"
|
|
"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/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,
|
|
}
|
|
|
|
groupService := &group.Service{
|
|
Repository: db.Group,
|
|
}
|
|
|
|
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,
|
|
|
|
ImageThumbnailGenerateWaitGroup: sizedwaitgroup.New(1),
|
|
|
|
JobManager: initJobManager(cfg),
|
|
ReadLockManager: fsutil.NewReadLockManager(),
|
|
|
|
DownloadStore: NewDownloadStore(),
|
|
|
|
PluginCache: pluginCache,
|
|
ScraperCache: scraperCache,
|
|
|
|
DLNAService: dlnaService,
|
|
|
|
Database: db,
|
|
Repository: repo,
|
|
|
|
SceneService: sceneService,
|
|
ImageService: imageService,
|
|
GalleryService: galleryService,
|
|
GroupService: groupService,
|
|
|
|
scanSubs: &subscriptionManager{},
|
|
}
|
|
|
|
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 formatDuration(t time.Duration) string {
|
|
switch {
|
|
case t >= time.Minute: // 1m23s or 2h45m12s
|
|
t = t.Round(time.Second)
|
|
case t >= time.Second: // 45.36s
|
|
t = t.Round(10 * time.Millisecond)
|
|
default: // 51ms
|
|
t = t.Round(time.Millisecond)
|
|
}
|
|
|
|
return t.String()
|
|
}
|
|
|
|
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\" 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.RefreshPluginSourceManager()
|
|
|
|
s.RefreshScraperCache()
|
|
s.RefreshScraperSourceManager()
|
|
|
|
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")
|
|
}
|
|
|
|
s.RefreshFFMpeg(ctx)
|
|
s.RefreshStreamManager()
|
|
|
|
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) RefreshFFMpeg(ctx context.Context) {
|
|
// use same directory as config path
|
|
// executing binaries requires directory to be included
|
|
// https://pkg.go.dev/os/exec#hdr-Executables_in_the_current_directory
|
|
configDirectory := s.Config.GetConfigPathAbs()
|
|
stashHomeDir := paths.GetStashHomeDirectory()
|
|
|
|
// prefer the configured paths
|
|
ffmpegPath := s.Config.GetFFMpegPath()
|
|
ffprobePath := s.Config.GetFFProbePath()
|
|
|
|
// ensure the paths are valid
|
|
if ffmpegPath != "" {
|
|
// path was set explicitly
|
|
if err := ffmpeg.ValidateFFMpeg(ffmpegPath); err != nil {
|
|
logger.Errorf("invalid ffmpeg path: %v", err)
|
|
return
|
|
}
|
|
|
|
if err := ffmpeg.ValidateFFMpegCodecSupport(ffmpegPath); err != nil {
|
|
logger.Warn(err)
|
|
}
|
|
} else {
|
|
ffmpegPath = ffmpeg.ResolveFFMpeg(configDirectory, stashHomeDir)
|
|
}
|
|
|
|
if ffprobePath != "" {
|
|
if err := ffmpeg.ValidateFFProbe(ffmpegPath); err != nil {
|
|
logger.Errorf("invalid ffprobe path: %v", err)
|
|
return
|
|
}
|
|
} else {
|
|
ffprobePath = ffmpeg.ResolveFFProbe(configDirectory, stashHomeDir)
|
|
}
|
|
|
|
if ffmpegPath == "" {
|
|
logger.Warn("Couldn't find FFmpeg")
|
|
}
|
|
if ffprobePath == "" {
|
|
logger.Warn("Couldn't find FFProbe")
|
|
}
|
|
|
|
if ffmpegPath != "" && ffprobePath != "" {
|
|
logger.Debugf("using ffmpeg: %s", ffmpegPath)
|
|
logger.Debugf("using ffprobe: %s", ffprobePath)
|
|
|
|
s.FFMpeg = ffmpeg.NewEncoder(ffmpegPath)
|
|
s.FFProbe = ffmpeg.NewFFProbe(ffprobePath)
|
|
|
|
s.FFMpeg.InitHWSupport(ctx)
|
|
}
|
|
}
|