stash/internal/dlna/service.go

334 lines
7.2 KiB
Go
Raw Normal View History

2021-05-20 06:58:43 +00:00
package dlna
import (
"fmt"
"net"
"net/http"
"path/filepath"
"sync"
"time"
"github.com/stashapp/stash/pkg/file"
2021-05-20 06:58:43 +00:00
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/txn"
2021-05-20 06:58:43 +00:00
)
type Repository struct {
SceneFinder SceneFinder
FileFinder file.Finder
StudioFinder StudioFinder
TagFinder TagFinder
PerformerFinder PerformerFinder
MovieFinder MovieFinder
}
type Status struct {
Running bool `json:"running"`
// If not currently running, time until it will be started. If running, time until it will be stopped
Until *time.Time `json:"until"`
RecentIPAddresses []string `json:"recentIPAddresses"`
AllowedIPAddresses []*Dlnaip `json:"allowedIPAddresses"`
}
type Dlnaip struct {
IPAddress string `json:"ipAddress"`
// Time until IP will be no longer allowed/disallowed
Until *time.Time `json:"until"`
}
2021-05-20 06:58:43 +00:00
type dmsConfig struct {
Path string
IfNames []string
Http string
FriendlyName string
LogHeaders bool
StallEventSubscribe bool
NotifyInterval time.Duration
}
type sceneServer interface {
StreamSceneDirect(scene *models.Scene, w http.ResponseWriter, r *http.Request)
ServeScreenshot(scene *models.Scene, w http.ResponseWriter, r *http.Request)
}
type Config interface {
GetDLNAInterfaces() []string
GetDLNAServerName() string
GetDLNADefaultIPWhitelist() []string
}
2021-05-20 06:58:43 +00:00
type Service struct {
txnManager txn.Manager
repository Repository
config Config
2021-05-20 06:58:43 +00:00
sceneServer sceneServer
ipWhitelistMgr *ipWhitelistManager
server *Server
running bool
mutex sync.Mutex
startTimer *time.Timer
startTime *time.Time
stopTimer *time.Timer
stopTime *time.Time
}
func (s *Service) getInterfaces() ([]net.Interface, error) {
var ifs []net.Interface
var err error
ifNames := s.config.GetDLNAInterfaces()
if len(ifNames) == 0 {
ifs, err = net.Interfaces()
} else {
for _, n := range ifNames {
if_, err := net.InterfaceByName(n)
if err != nil {
return nil, fmt.Errorf("error getting interface for name %s: %s", n, err.Error())
}
if if_ != nil {
ifs = append(ifs, *if_)
}
}
}
if err != nil {
return nil, err
}
var tmp []net.Interface
for _, if_ := range ifs {
if if_.Flags&net.FlagUp == 0 || if_.MTU <= 0 {
continue
}
tmp = append(tmp, if_)
}
ifs = tmp
return ifs, nil
}
func (s *Service) init() error {
friendlyName := s.config.GetDLNAServerName()
if friendlyName == "" {
friendlyName = "stash"
}
var dmsConfig = &dmsConfig{
Path: "",
IfNames: s.config.GetDLNADefaultIPWhitelist(),
Http: ":1338",
FriendlyName: friendlyName,
LogHeaders: false,
NotifyInterval: 30 * time.Second,
}
interfaces, err := s.getInterfaces()
if err != nil {
return err
}
s.server = &Server{
txnManager: s.txnManager,
sceneServer: s.sceneServer,
repository: s.repository,
2021-05-20 06:58:43 +00:00
ipWhitelistManager: s.ipWhitelistMgr,
Interfaces: interfaces,
HTTPConn: func() net.Listener {
conn, err := net.Listen("tcp", dmsConfig.Http)
if err != nil {
logger.Error(err.Error())
}
return conn
}(),
FriendlyName: dmsConfig.FriendlyName,
RootObjectPath: filepath.Clean(dmsConfig.Path),
LogHeaders: dmsConfig.LogHeaders,
// Icons: []Icon{
// {
// Width: 48,
// Height: 48,
// Depth: 8,
// Mimetype: "image/png",
// //ReadSeeker: readIcon(config.Config.Interfaces.DLNA.ServiceImage, 48),
// },
// {
// Width: 128,
// Height: 128,
// Depth: 8,
// Mimetype: "image/png",
// //ReadSeeker: readIcon(config.Config.Interfaces.DLNA.ServiceImage, 128),
// },
// },
StallEventSubscribe: dmsConfig.StallEventSubscribe,
NotifyInterval: dmsConfig.NotifyInterval,
}
return nil
}
// func getIconReader(fn string) (io.Reader, error) {
// b, err := assets.ReadFile("dlna/" + fn + ".png")
// return bytes.NewReader(b), err
// }
// func readIcon(path string, size uint) *bytes.Reader {
// r, err := getIconReader(path)
// if err != nil {
// panic(err)
// }
// imageData, _, err := image.Decode(r)
// if err != nil {
// panic(err)
// }
// return resizeImage(imageData, size)
// }
// func resizeImage(imageData image.Image, size uint) *bytes.Reader {
// img := resize.Resize(size, size, imageData, resize.Lanczos3)
// var buff bytes.Buffer
// png.Encode(&buff, img)
// return bytes.NewReader(buff.Bytes())
// }
// NewService initialises and returns a new DLNA service.
func NewService(txnManager txn.Manager, repo Repository, cfg Config, sceneServer sceneServer) *Service {
2021-05-20 06:58:43 +00:00
ret := &Service{
txnManager: txnManager,
repository: repo,
2021-05-20 06:58:43 +00:00
sceneServer: sceneServer,
config: cfg,
ipWhitelistMgr: &ipWhitelistManager{
config: cfg,
},
mutex: sync.Mutex{},
}
return ret
}
// Start starts the DLNA service. If duration is provided, then the service
// is stopped after the duration has elapsed.
func (s *Service) Start(duration *time.Duration) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if !s.running {
if err := s.init(); err != nil {
logger.Error(err)
return err
}
go func() {
logger.Info("Starting DLNA")
if err := s.server.Serve(); err != nil {
logger.Error(err)
}
}()
s.running = true
if s.startTimer != nil {
s.startTimer.Stop()
s.startTimer = nil
s.startTime = nil
}
}
if duration != nil {
// clear the existing stop timer
if s.stopTimer != nil {
s.stopTimer.Stop()
s.stopTime = nil
}
if s.stopTimer == nil {
s.stopTimer = time.AfterFunc(*duration, func() {
s.Stop(nil)
})
t := time.Now().Add(*duration)
s.stopTime = &t
}
}
return nil
}
// Stop stops the DLNA service. If duration is provided, then the service
// is started after the duration has elapsed.
func (s *Service) Stop(duration *time.Duration) {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.running {
logger.Info("Stopping DLNA")
err := s.server.Close()
if err != nil {
logger.Error(err)
}
s.running = false
if s.stopTimer != nil {
s.stopTimer.Stop()
s.stopTimer = nil
s.stopTime = nil
}
}
if duration != nil {
// clear the existing stop timer
if s.startTimer != nil {
s.startTimer.Stop()
}
if s.startTimer == nil {
s.startTimer = time.AfterFunc(*duration, func() {
Errcheck phase 1 (#1715) * Avoid redundant logging in migrations Return the error and let the caller handle the logging of the error if needed. While here, defer m.Close() to the function boundary. * Treat errors as values Use %v rather than %s and pass the errors directly. * Generate a wrapped error on stat-failure * Log 3 unchecked errors Rather than ignore errors, log them at the WARNING log level. The server has been functioning without these, so assume they are not at the ERROR level. * Propagate errors upward Failure in path generation was ignored. Propagate the errors upward the call stack, so it can be handled at the level of orchestration. * Warn on errors Log errors rather than quenching them. Errors are logged at the Warn-level for now. * Check error when creating test databases Use the builtin log package and stop the program fatally on error. * Add warnings to uncheck task errors Focus on the task system in a single commit, logging unchecked errors as warnings. * Warn-on-error in API routes Look through the API routes, and make sure errors are being logged if they occur. Prefer the Warn-log-level because none of these has proven to be fatal in the system up until now. * Propagate error when adding Util API * Propagate error on adding util API * Return unhandled error * JS log API: propagate and log errors * JS Plugins: log GQL addition failures. * Warn on failure to write to stdin * Warn on failure to stop task * Wrap viper.BindEnv The current viper code only errors if no name is provided, so it should never fail. Rewrite the code flow to factor through a panic-function. This removes error warnings from this part of the code. * Log errors in concurrency test If we can't initialize the configuration, treat the test as a failure. * Warn on errors in configuration code * Plug an unchecked error in gallery zip walking * Warn on screenshot serving failure * Warn on encoder screenshot failure * Warn on errors in path-handling code * Undo the errcheck on configurations for now. * Use one-line initializers where applicable rather than using err := f() if err!= nil { .. prefer the shorter if err := f(); err != nil { .. If f() isn't too long of a name, or wraps a function with a body.
2021-09-20 23:34:25 +00:00
if err := s.Start(nil); err != nil {
logger.Warnf("error restarting DLNA server: %v", err)
}
2021-05-20 06:58:43 +00:00
})
t := time.Now().Add(*duration)
s.startTime = &t
}
}
}
// IsRunning returns true if the DLNA service is running.
func (s *Service) IsRunning() bool {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.running
}
func (s *Service) Status() *Status {
2021-05-20 06:58:43 +00:00
s.mutex.Lock()
defer s.mutex.Unlock()
ret := &Status{
2021-05-20 06:58:43 +00:00
Running: s.running,
RecentIPAddresses: s.ipWhitelistMgr.getRecent(),
AllowedIPAddresses: s.ipWhitelistMgr.getTempAllowed(),
}
if s.startTime != nil {
t := *s.startTime
ret.Until = &t
}
if s.stopTime != nil {
t := *s.stopTime
ret.Until = &t
}
return ret
}
func (s *Service) AddTempDLNAIP(pattern string, duration *time.Duration) {
s.ipWhitelistMgr.allowPattern(pattern, duration)
}
func (s *Service) RemoveTempDLNAIP(pattern string) bool {
return s.ipWhitelistMgr.removePattern(pattern)
}