441 lines
16 KiB
Go
441 lines
16 KiB
Go
package core
|
|
|
|
import (
|
|
"os"
|
|
"runtime"
|
|
"seanime/internal/api/anilist"
|
|
"seanime/internal/api/metadata"
|
|
"seanime/internal/constants"
|
|
"seanime/internal/continuity"
|
|
"seanime/internal/database/db"
|
|
"seanime/internal/database/models"
|
|
debrid_client "seanime/internal/debrid/client"
|
|
"seanime/internal/directstream"
|
|
discordrpc_presence "seanime/internal/discordrpc/presence"
|
|
"seanime/internal/doh"
|
|
"seanime/internal/events"
|
|
"seanime/internal/extension_playground"
|
|
"seanime/internal/extension_repo"
|
|
"seanime/internal/hook"
|
|
"seanime/internal/library/autodownloader"
|
|
"seanime/internal/library/autoscanner"
|
|
"seanime/internal/library/fillermanager"
|
|
"seanime/internal/library/playbackmanager"
|
|
"seanime/internal/library/scanner"
|
|
"seanime/internal/local"
|
|
"seanime/internal/manga"
|
|
"seanime/internal/mediaplayers/iina"
|
|
"seanime/internal/mediaplayers/mediaplayer"
|
|
"seanime/internal/mediaplayers/mpchc"
|
|
"seanime/internal/mediaplayers/mpv"
|
|
"seanime/internal/mediaplayers/vlc"
|
|
"seanime/internal/mediastream"
|
|
"seanime/internal/nakama"
|
|
"seanime/internal/nativeplayer"
|
|
"seanime/internal/onlinestream"
|
|
"seanime/internal/platforms/anilist_platform"
|
|
"seanime/internal/platforms/offline_platform"
|
|
"seanime/internal/platforms/platform"
|
|
"seanime/internal/platforms/simulated_platform"
|
|
"seanime/internal/plugin"
|
|
"seanime/internal/report"
|
|
"seanime/internal/torrent_clients/torrent_client"
|
|
"seanime/internal/torrents/torrent"
|
|
"seanime/internal/torrentstream"
|
|
"seanime/internal/updater"
|
|
"seanime/internal/user"
|
|
"seanime/internal/util"
|
|
"seanime/internal/util/filecache"
|
|
"seanime/internal/util/result"
|
|
"sync"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
type (
|
|
App struct {
|
|
Config *Config
|
|
Database *db.Database
|
|
Logger *zerolog.Logger
|
|
TorrentClientRepository *torrent_client.Repository
|
|
TorrentRepository *torrent.Repository
|
|
DebridClientRepository *debrid_client.Repository
|
|
Watcher *scanner.Watcher
|
|
AnilistClient anilist.AnilistClient
|
|
AnilistPlatform platform.Platform
|
|
OfflinePlatform platform.Platform
|
|
LocalManager local.Manager
|
|
FillerManager *fillermanager.FillerManager
|
|
WSEventManager *events.WSEventManager
|
|
AutoDownloader *autodownloader.AutoDownloader
|
|
ExtensionRepository *extension_repo.Repository
|
|
ExtensionPlaygroundRepository *extension_playground.PlaygroundRepository
|
|
DirectStreamManager *directstream.Manager
|
|
NativePlayer *nativeplayer.NativePlayer
|
|
MediaPlayer struct {
|
|
VLC *vlc.VLC
|
|
MpcHc *mpchc.MpcHc
|
|
Mpv *mpv.Mpv
|
|
Iina *iina.Iina
|
|
}
|
|
MediaPlayerRepository *mediaplayer.Repository
|
|
Version string
|
|
Updater *updater.Updater
|
|
AutoScanner *autoscanner.AutoScanner
|
|
PlaybackManager *playbackmanager.PlaybackManager
|
|
FileCacher *filecache.Cacher
|
|
OnlinestreamRepository *onlinestream.Repository
|
|
MangaRepository *manga.Repository
|
|
MetadataProvider metadata.Provider
|
|
DiscordPresence *discordrpc_presence.Presence
|
|
MangaDownloader *manga.Downloader
|
|
ContinuityManager *continuity.Manager
|
|
Cleanups []func()
|
|
OnRefreshAnilistCollectionFuncs *result.Map[string, func()]
|
|
OnFlushLogs func()
|
|
MediastreamRepository *mediastream.Repository
|
|
TorrentstreamRepository *torrentstream.Repository
|
|
FeatureFlags FeatureFlags
|
|
Settings *models.Settings
|
|
SecondarySettings struct {
|
|
Mediastream *models.MediastreamSettings
|
|
Torrentstream *models.TorrentstreamSettings
|
|
Debrid *models.DebridSettings
|
|
} // Struct for other settings sent to clientN
|
|
SelfUpdater *updater.SelfUpdater
|
|
ReportRepository *report.Repository
|
|
TotalLibrarySize uint64 // Initialized in modules.go
|
|
LibraryDir string
|
|
IsDesktopSidecar bool
|
|
animeCollection *anilist.AnimeCollection
|
|
rawAnimeCollection *anilist.AnimeCollection // (retains custom lists)
|
|
mangaCollection *anilist.MangaCollection
|
|
rawMangaCollection *anilist.MangaCollection // (retains custom lists)
|
|
user *user.User
|
|
previousVersion string
|
|
moduleMu sync.Mutex
|
|
HookManager hook.Manager
|
|
ServerReady bool // Whether the Anilist data from the first request has been fetched
|
|
isOffline *bool
|
|
NakamaManager *nakama.Manager
|
|
ServerPasswordHash string // SHA-256 hash of the server password
|
|
}
|
|
)
|
|
|
|
// NewApp creates a new server instance
|
|
func NewApp(configOpts *ConfigOptions, selfupdater *updater.SelfUpdater) *App {
|
|
|
|
// Initialize logger with predefined format
|
|
logger := util.NewLogger()
|
|
|
|
// Log application version, OS, architecture and system info
|
|
logger.Info().Msgf("app: Seanime %s-%s", constants.Version, constants.VersionName)
|
|
logger.Info().Msgf("app: OS: %s", runtime.GOOS)
|
|
logger.Info().Msgf("app: Arch: %s", runtime.GOARCH)
|
|
logger.Info().Msgf("app: Processor count: %d", runtime.NumCPU())
|
|
|
|
// Initialize hook manager for plugin event system
|
|
hookManager := hook.NewHookManager(hook.NewHookManagerOptions{Logger: logger})
|
|
hook.SetGlobalHookManager(hookManager)
|
|
plugin.GlobalAppContext.SetLogger(logger)
|
|
|
|
// Store current version to detect version changes
|
|
previousVersion := constants.Version
|
|
|
|
// Add callback to track version changes
|
|
configOpts.OnVersionChange = append(configOpts.OnVersionChange, func(oldVersion string, newVersion string) {
|
|
logger.Info().Str("prev", oldVersion).Str("current", newVersion).Msg("app: Version change detected")
|
|
previousVersion = oldVersion
|
|
})
|
|
|
|
// Initialize configuration with provided options
|
|
// Creates config directory if it doesn't exist
|
|
cfg, err := NewConfig(configOpts, logger)
|
|
if err != nil {
|
|
logger.Fatal().Err(err).Msgf("app: Failed to initialize config")
|
|
}
|
|
|
|
// Compute SHA-256 hash of the server password
|
|
serverPasswordHash := ""
|
|
if cfg.Server.Password != "" {
|
|
serverPasswordHash = util.HashSHA256Hex(cfg.Server.Password)
|
|
}
|
|
|
|
// Create logs directory if it doesn't exist
|
|
_ = os.MkdirAll(cfg.Logs.Dir, 0755)
|
|
|
|
// Start background process to trim log files
|
|
go TrimLogEntries(cfg.Logs.Dir, logger)
|
|
|
|
logger.Info().Msgf("app: Data directory: %s", cfg.Data.AppDataDir)
|
|
logger.Info().Msgf("app: Working directory: %s", cfg.Data.WorkingDir)
|
|
|
|
// Log if running in desktop sidecar mode
|
|
if configOpts.IsDesktopSidecar {
|
|
logger.Info().Msg("app: Desktop sidecar mode enabled")
|
|
}
|
|
|
|
// Initialize database connection
|
|
database, err := db.NewDatabase(cfg.Data.AppDataDir, cfg.Database.Name, logger)
|
|
if err != nil {
|
|
logger.Fatal().Err(err).Msgf("app: Failed to initialize database")
|
|
}
|
|
|
|
HandleNewDatabaseEntries(database, logger)
|
|
|
|
// Clean up old database entries in background goroutines
|
|
database.TrimLocalFileEntries() // Remove old local file entries
|
|
database.TrimScanSummaryEntries() // Remove old scan summaries
|
|
database.TrimTorrentstreamHistory() // Remove old torrent stream history
|
|
|
|
// Get anime library paths for plugin context
|
|
animeLibraryPaths, _ := database.GetAllLibraryPathsFromSettings()
|
|
plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
|
|
Database: database,
|
|
AnimeLibraryPaths: &animeLibraryPaths,
|
|
})
|
|
|
|
// Get Anilist token from database if available
|
|
anilistToken := database.GetAnilistToken()
|
|
|
|
// Initialize Anilist API client with the token
|
|
// If the token is empty, the client will not be authenticated
|
|
anilistCW := anilist.NewAnilistClient(anilistToken)
|
|
|
|
// Initialize WebSocket event manager for real-time communication
|
|
wsEventManager := events.NewWSEventManager(logger)
|
|
|
|
// Exit if no WebSocket connections in desktop sidecar mode
|
|
if configOpts.IsDesktopSidecar {
|
|
wsEventManager.ExitIfNoConnsAsDesktopSidecar()
|
|
}
|
|
|
|
// Initialize DNS-over-HTTPS service in background
|
|
go doh.HandleDoH(cfg.Server.DoHUrl, logger)
|
|
|
|
// Initialize file cache system for media and metadata
|
|
fileCacher, err := filecache.NewCacher(cfg.Cache.Dir)
|
|
if err != nil {
|
|
logger.Fatal().Err(err).Msgf("app: Failed to initialize file cacher")
|
|
}
|
|
|
|
// Initialize extension repository
|
|
extensionRepository := extension_repo.NewRepository(&extension_repo.NewRepositoryOptions{
|
|
Logger: logger,
|
|
ExtensionDir: cfg.Extensions.Dir,
|
|
WSEventManager: wsEventManager,
|
|
FileCacher: fileCacher,
|
|
HookManager: hookManager,
|
|
})
|
|
// Load extensions in background
|
|
go LoadExtensions(extensionRepository, logger, cfg)
|
|
|
|
// Initialize metadata provider for media information
|
|
metadataProvider := metadata.NewProvider(&metadata.NewProviderImplOptions{
|
|
Logger: logger,
|
|
FileCacher: fileCacher,
|
|
})
|
|
|
|
// Set initial metadata provider (will change if offline mode is enabled)
|
|
activeMetadataProvider := metadataProvider
|
|
|
|
// Initialize manga repository
|
|
mangaRepository := manga.NewRepository(&manga.NewRepositoryOptions{
|
|
Logger: logger,
|
|
FileCacher: fileCacher,
|
|
CacheDir: cfg.Cache.Dir,
|
|
ServerURI: cfg.GetServerURI(),
|
|
WsEventManager: wsEventManager,
|
|
DownloadDir: cfg.Manga.DownloadDir,
|
|
Database: database,
|
|
})
|
|
|
|
// Initialize Anilist platform
|
|
anilistPlatform := anilist_platform.NewAnilistPlatform(anilistCW, logger)
|
|
|
|
// Update plugin context with new modules
|
|
plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
|
|
AnilistPlatform: anilistPlatform,
|
|
WSEventManager: wsEventManager,
|
|
MetadataProvider: metadataProvider,
|
|
})
|
|
|
|
// Initialize sync manager for offline/online synchronization
|
|
localManager, err := local.NewManager(&local.NewManagerOptions{
|
|
LocalDir: cfg.Offline.Dir,
|
|
AssetDir: cfg.Offline.AssetDir,
|
|
Logger: logger,
|
|
MetadataProvider: metadataProvider,
|
|
MangaRepository: mangaRepository,
|
|
Database: database,
|
|
WSEventManager: wsEventManager,
|
|
IsOffline: cfg.Server.Offline,
|
|
AnilistPlatform: anilistPlatform,
|
|
})
|
|
if err != nil {
|
|
logger.Fatal().Err(err).Msgf("app: Failed to initialize sync manager")
|
|
}
|
|
|
|
// Use local metadata provider if in offline mode
|
|
if cfg.Server.Offline {
|
|
activeMetadataProvider = localManager.GetOfflineMetadataProvider()
|
|
}
|
|
|
|
// Initialize local platform for offline operations
|
|
offlinePlatform, err := offline_platform.NewOfflinePlatform(localManager, anilistCW, logger)
|
|
if err != nil {
|
|
logger.Fatal().Err(err).Msgf("app: Failed to initialize local platform")
|
|
}
|
|
|
|
// Initialize simulated platform for unauthenticated operations
|
|
simulatedPlatform, err := simulated_platform.NewSimulatedPlatform(localManager, anilistCW, logger)
|
|
if err != nil {
|
|
logger.Fatal().Err(err).Msgf("app: Failed to initialize simulated platform")
|
|
}
|
|
|
|
// Change active platform if offline mode is enabled
|
|
activePlatform := anilistPlatform
|
|
if cfg.Server.Offline {
|
|
activePlatform = offlinePlatform
|
|
} else if !anilistCW.IsAuthenticated() {
|
|
logger.Warn().Msg("app: Anilist client is not authenticated, using simulated platform")
|
|
activePlatform = simulatedPlatform
|
|
}
|
|
|
|
// Initialize online streaming repository
|
|
onlinestreamRepository := onlinestream.NewRepository(&onlinestream.NewRepositoryOptions{
|
|
Logger: logger,
|
|
FileCacher: fileCacher,
|
|
MetadataProvider: activeMetadataProvider,
|
|
Platform: activePlatform,
|
|
Database: database,
|
|
})
|
|
|
|
// Initialize extension playground for testing extensions
|
|
extensionPlaygroundRepository := extension_playground.NewPlaygroundRepository(logger, activePlatform, activeMetadataProvider)
|
|
|
|
isOffline := cfg.Server.Offline
|
|
|
|
// Create the main app instance with initialized components
|
|
app := &App{
|
|
Config: cfg,
|
|
Database: database,
|
|
AnilistClient: anilistCW,
|
|
AnilistPlatform: activePlatform,
|
|
OfflinePlatform: offlinePlatform,
|
|
LocalManager: localManager,
|
|
WSEventManager: wsEventManager,
|
|
Logger: logger,
|
|
Version: constants.Version,
|
|
Updater: updater.New(constants.Version, logger, wsEventManager),
|
|
FileCacher: fileCacher,
|
|
OnlinestreamRepository: onlinestreamRepository,
|
|
MetadataProvider: activeMetadataProvider,
|
|
MangaRepository: mangaRepository,
|
|
ExtensionRepository: extensionRepository,
|
|
ExtensionPlaygroundRepository: extensionPlaygroundRepository,
|
|
ReportRepository: report.NewRepository(logger),
|
|
TorrentRepository: nil, // Initialized in App.initModulesOnce
|
|
FillerManager: nil, // Initialized in App.initModulesOnce
|
|
MangaDownloader: nil, // Initialized in App.initModulesOnce
|
|
PlaybackManager: nil, // Initialized in App.initModulesOnce
|
|
AutoDownloader: nil, // Initialized in App.initModulesOnce
|
|
AutoScanner: nil, // Initialized in App.initModulesOnce
|
|
MediastreamRepository: nil, // Initialized in App.initModulesOnce
|
|
TorrentstreamRepository: nil, // Initialized in App.initModulesOnce
|
|
ContinuityManager: nil, // Initialized in App.initModulesOnce
|
|
DebridClientRepository: nil, // Initialized in App.initModulesOnce
|
|
DirectStreamManager: nil, // Initialized in App.initModulesOnce
|
|
NativePlayer: nil, // Initialized in App.initModulesOnce
|
|
NakamaManager: nil, // Initialized in App.initModulesOnce
|
|
TorrentClientRepository: nil, // Initialized in App.InitOrRefreshModules
|
|
MediaPlayerRepository: nil, // Initialized in App.InitOrRefreshModules
|
|
DiscordPresence: nil, // Initialized in App.InitOrRefreshModules
|
|
previousVersion: previousVersion,
|
|
FeatureFlags: NewFeatureFlags(cfg, logger),
|
|
IsDesktopSidecar: configOpts.IsDesktopSidecar,
|
|
SecondarySettings: struct {
|
|
Mediastream *models.MediastreamSettings
|
|
Torrentstream *models.TorrentstreamSettings
|
|
Debrid *models.DebridSettings
|
|
}{Mediastream: nil, Torrentstream: nil},
|
|
SelfUpdater: selfupdater,
|
|
moduleMu: sync.Mutex{},
|
|
OnRefreshAnilistCollectionFuncs: result.NewResultMap[string, func()](),
|
|
HookManager: hookManager,
|
|
isOffline: &isOffline,
|
|
ServerPasswordHash: serverPasswordHash,
|
|
}
|
|
|
|
// Run database migrations if version has changed
|
|
app.runMigrations()
|
|
|
|
// Initialize modules that only need to be initialized once
|
|
app.initModulesOnce()
|
|
|
|
plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
|
|
IsOffline: app.IsOffline(),
|
|
ContinuityManager: app.ContinuityManager,
|
|
AutoScanner: app.AutoScanner,
|
|
AutoDownloader: app.AutoDownloader,
|
|
FileCacher: app.FileCacher,
|
|
OnlinestreamRepository: app.OnlinestreamRepository,
|
|
MediastreamRepository: app.MediastreamRepository,
|
|
TorrentstreamRepository: app.TorrentstreamRepository,
|
|
})
|
|
|
|
if !*app.IsOffline() {
|
|
go app.Updater.FetchAnnouncements()
|
|
}
|
|
|
|
// Initialize all modules that depend on settings
|
|
app.InitOrRefreshModules()
|
|
|
|
// Load built-in extensions into extension consumers
|
|
app.AddExtensionBankToConsumers()
|
|
|
|
// Initialize Anilist data if not in offline mode
|
|
if !*app.IsOffline() {
|
|
app.InitOrRefreshAnilistData()
|
|
} else {
|
|
app.ServerReady = true
|
|
}
|
|
|
|
// Initialize mediastream settings (for streaming media)
|
|
app.InitOrRefreshMediastreamSettings()
|
|
|
|
// Initialize torrentstream settings (for torrent streaming)
|
|
app.InitOrRefreshTorrentstreamSettings()
|
|
|
|
// Initialize debrid settings (for debrid services)
|
|
app.InitOrRefreshDebridSettings()
|
|
|
|
// Register Nakama manager cleanup
|
|
app.AddCleanupFunction(app.NakamaManager.Cleanup)
|
|
|
|
// Run one-time initialization actions
|
|
app.performActionsOnce()
|
|
|
|
return app
|
|
}
|
|
|
|
func (a *App) IsOffline() *bool {
|
|
return a.isOffline
|
|
}
|
|
|
|
func (a *App) AddCleanupFunction(f func()) {
|
|
a.Cleanups = append(a.Cleanups, f)
|
|
}
|
|
func (a *App) AddOnRefreshAnilistCollectionFunc(key string, f func()) {
|
|
if key == "" {
|
|
return
|
|
}
|
|
a.OnRefreshAnilistCollectionFuncs.Set(key, f)
|
|
}
|
|
|
|
func (a *App) Cleanup() {
|
|
for _, f := range a.Cleanups {
|
|
f()
|
|
}
|
|
}
|