Files
seanime-docker/seanime-2.9.10/internal/core/modules.go
2025-09-20 14:08:38 +01:00

728 lines
21 KiB
Go

package core
import (
"runtime"
"seanime/internal/api/anilist"
"seanime/internal/continuity"
"seanime/internal/database/db"
"seanime/internal/database/db_bridge"
"seanime/internal/database/models"
debrid_client "seanime/internal/debrid/client"
"seanime/internal/directstream"
discordrpc_presence "seanime/internal/discordrpc/presence"
"seanime/internal/events"
"seanime/internal/library/anime"
"seanime/internal/library/autodownloader"
"seanime/internal/library/autoscanner"
"seanime/internal/library/fillermanager"
"seanime/internal/library/playbackmanager"
"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/notifier"
"seanime/internal/plugin"
"seanime/internal/torrent_clients/qbittorrent"
"seanime/internal/torrent_clients/torrent_client"
"seanime/internal/torrent_clients/transmission"
"seanime/internal/torrents/torrent"
"seanime/internal/torrentstream"
"seanime/internal/user"
"github.com/cli/browser"
"github.com/rs/zerolog"
)
// initModulesOnce will initialize modules that need to persist.
// This function is called once after the App instance is created.
// The settings of these modules will be set/refreshed in InitOrRefreshModules.
func (a *App) initModulesOnce() {
a.LocalManager.SetRefreshAnilistCollectionsFunc(func() {
_, _ = a.RefreshAnimeCollection()
_, _ = a.RefreshMangaCollection()
})
plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
OnRefreshAnilistAnimeCollection: func() {
_, _ = a.RefreshAnimeCollection()
},
OnRefreshAnilistMangaCollection: func() {
_, _ = a.RefreshMangaCollection()
},
})
// +---------------------+
// | Discord RPC |
// +---------------------+
a.DiscordPresence = discordrpc_presence.New(nil, a.Logger)
a.AddCleanupFunction(func() {
a.DiscordPresence.Close()
})
plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
DiscordPresence: a.DiscordPresence,
})
// +---------------------+
// | Filler |
// +---------------------+
a.FillerManager = fillermanager.New(&fillermanager.NewFillerManagerOptions{
DB: a.Database,
Logger: a.Logger,
})
plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
FillerManager: a.FillerManager,
})
// +---------------------+
// | Continuity |
// +---------------------+
a.ContinuityManager = continuity.NewManager(&continuity.NewManagerOptions{
FileCacher: a.FileCacher,
Logger: a.Logger,
Database: a.Database,
})
// +---------------------+
// | Playback Manager |
// +---------------------+
// Playback Manager
a.PlaybackManager = playbackmanager.New(&playbackmanager.NewPlaybackManagerOptions{
Logger: a.Logger,
WSEventManager: a.WSEventManager,
Platform: a.AnilistPlatform,
MetadataProvider: a.MetadataProvider,
Database: a.Database,
DiscordPresence: a.DiscordPresence,
IsOffline: a.IsOffline(),
ContinuityManager: a.ContinuityManager,
RefreshAnimeCollectionFunc: func() {
_, _ = a.RefreshAnimeCollection()
},
})
// +---------------------+
// | Torrent Repository |
// +---------------------+
a.TorrentRepository = torrent.NewRepository(&torrent.NewRepositoryOptions{
Logger: a.Logger,
MetadataProvider: a.MetadataProvider,
})
// +---------------------+
// | Manga Downloader |
// +---------------------+
a.MangaDownloader = manga.NewDownloader(&manga.NewDownloaderOptions{
Database: a.Database,
Logger: a.Logger,
WSEventManager: a.WSEventManager,
DownloadDir: a.Config.Manga.DownloadDir,
Repository: a.MangaRepository,
IsOffline: a.IsOffline(),
})
a.MangaDownloader.Start()
// +---------------------+
// | Media Stream |
// +---------------------+
a.MediastreamRepository = mediastream.NewRepository(&mediastream.NewRepositoryOptions{
Logger: a.Logger,
WSEventManager: a.WSEventManager,
FileCacher: a.FileCacher,
})
a.AddCleanupFunction(func() {
a.MediastreamRepository.OnCleanup()
})
// +---------------------+
// | Native Player |
// +---------------------+
a.NativePlayer = nativeplayer.New(nativeplayer.NewNativePlayerOptions{
WsEventManager: a.WSEventManager,
Logger: a.Logger,
})
// +---------------------+
// | Direct Stream |
// +---------------------+
a.DirectStreamManager = directstream.NewManager(directstream.NewManagerOptions{
Logger: a.Logger,
WSEventManager: a.WSEventManager,
ContinuityManager: a.ContinuityManager,
MetadataProvider: a.MetadataProvider,
DiscordPresence: a.DiscordPresence,
Platform: a.AnilistPlatform,
RefreshAnimeCollectionFunc: func() {
_, _ = a.RefreshAnimeCollection()
},
IsOffline: a.IsOffline(),
NativePlayer: a.NativePlayer,
})
// +---------------------+
// | Torrent Stream |
// +---------------------+
a.TorrentstreamRepository = torrentstream.NewRepository(&torrentstream.NewRepositoryOptions{
Logger: a.Logger,
BaseAnimeCache: anilist.NewBaseAnimeCache(),
CompleteAnimeCache: anilist.NewCompleteAnimeCache(),
MetadataProvider: a.MetadataProvider,
TorrentRepository: a.TorrentRepository,
Platform: a.AnilistPlatform,
PlaybackManager: a.PlaybackManager,
WSEventManager: a.WSEventManager,
Database: a.Database,
DirectStreamManager: a.DirectStreamManager,
NativePlayer: a.NativePlayer,
})
// +---------------------+
// | Debrid Client Repo |
// +---------------------+
a.DebridClientRepository = debrid_client.NewRepository(&debrid_client.NewRepositoryOptions{
Logger: a.Logger,
WSEventManager: a.WSEventManager,
Database: a.Database,
MetadataProvider: a.MetadataProvider,
Platform: a.AnilistPlatform,
PlaybackManager: a.PlaybackManager,
TorrentRepository: a.TorrentRepository,
DirectStreamManager: a.DirectStreamManager,
})
plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
PlaybackManager: a.PlaybackManager,
MangaRepository: a.MangaRepository,
})
// +---------------------+
// | Auto Downloader |
// +---------------------+
a.AutoDownloader = autodownloader.New(&autodownloader.NewAutoDownloaderOptions{
Logger: a.Logger,
TorrentClientRepository: a.TorrentClientRepository,
TorrentRepository: a.TorrentRepository,
Database: a.Database,
WSEventManager: a.WSEventManager,
MetadataProvider: a.MetadataProvider,
DebridClientRepository: a.DebridClientRepository,
IsOffline: a.IsOffline(),
})
// This is run in a goroutine
a.AutoDownloader.Start()
// +---------------------+
// | Auto Scanner |
// +---------------------+
a.AutoScanner = autoscanner.New(&autoscanner.NewAutoScannerOptions{
Database: a.Database,
Platform: a.AnilistPlatform,
Logger: a.Logger,
WSEventManager: a.WSEventManager,
Enabled: false, // Will be set in InitOrRefreshModules
AutoDownloader: a.AutoDownloader,
MetadataProvider: a.MetadataProvider,
LogsDir: a.Config.Logs.Dir,
})
// This is run in a goroutine
a.AutoScanner.Start()
// +---------------------+
// | Nakama |
// +---------------------+
a.NakamaManager = nakama.NewManager(&nakama.NewManagerOptions{
Logger: a.Logger,
WSEventManager: a.WSEventManager,
PlaybackManager: a.PlaybackManager,
TorrentstreamRepository: a.TorrentstreamRepository,
DebridClientRepository: a.DebridClientRepository,
Platform: a.AnilistPlatform,
ServerHost: a.Config.Server.Host,
ServerPort: a.Config.Server.Port,
})
}
// HandleNewDatabaseEntries initializes essential database collections.
// It creates an empty local files collection if one does not already exist.
func HandleNewDatabaseEntries(database *db.Database, logger *zerolog.Logger) {
// Create initial empty local files collection if none exists
if _, _, err := db_bridge.GetLocalFiles(database); err != nil {
_, err := db_bridge.InsertLocalFiles(database, make([]*anime.LocalFile, 0))
if err != nil {
logger.Fatal().Err(err).Msgf("app: Failed to initialize local files in the database")
}
}
}
// InitOrRefreshModules will initialize or refresh modules that depend on settings.
// This function is called:
// - After the App instance is created
// - After settings are updated.
func (a *App) InitOrRefreshModules() {
a.moduleMu.Lock()
defer a.moduleMu.Unlock()
a.Logger.Debug().Msgf("app: Refreshing modules")
// Stop watching if already watching
if a.Watcher != nil {
a.Watcher.StopWatching()
}
// If Discord presence is already initialized, close it
if a.DiscordPresence != nil {
a.DiscordPresence.Close()
}
// Get settings from database
settings, err := a.Database.GetSettings()
if err != nil || settings == nil {
a.Logger.Warn().Msg("app: Did not initialize modules, no settings found")
return
}
a.Settings = settings // Store settings instance in app
if settings.Library != nil {
a.LibraryDir = settings.GetLibrary().LibraryPath
}
// +---------------------+
// | Module settings |
// +---------------------+
// Refresh settings of modules that were initialized in initModulesOnce
notifier.GlobalNotifier.SetSettings(a.Config.Data.AppDataDir, a.Settings.GetNotifications(), a.Logger)
// Refresh updater settings
if settings.Library != nil {
plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
AnimeLibraryPaths: a.Database.AllLibraryPathsFromSettings(settings),
})
if a.Updater != nil {
a.Updater.SetEnabled(!settings.Library.DisableUpdateCheck)
}
// Refresh auto scanner settings
if a.AutoScanner != nil {
a.AutoScanner.SetSettings(*settings.Library)
}
// Torrent Repository
a.TorrentRepository.SetSettings(&torrent.RepositorySettings{
DefaultAnimeProvider: settings.Library.TorrentProvider,
})
}
if settings.MediaPlayer != nil {
a.MediaPlayer.VLC = &vlc.VLC{
Host: settings.MediaPlayer.Host,
Port: settings.MediaPlayer.VlcPort,
Password: settings.MediaPlayer.VlcPassword,
Path: settings.MediaPlayer.VlcPath,
Logger: a.Logger,
}
a.MediaPlayer.MpcHc = &mpchc.MpcHc{
Host: settings.MediaPlayer.Host,
Port: settings.MediaPlayer.MpcPort,
Path: settings.MediaPlayer.MpcPath,
Logger: a.Logger,
}
a.MediaPlayer.Mpv = mpv.New(a.Logger, settings.MediaPlayer.MpvSocket, settings.MediaPlayer.MpvPath, settings.MediaPlayer.MpvArgs)
a.MediaPlayer.Iina = iina.New(a.Logger, settings.MediaPlayer.IinaSocket, settings.MediaPlayer.IinaPath, settings.MediaPlayer.IinaArgs)
// Set media player repository
a.MediaPlayerRepository = mediaplayer.NewRepository(&mediaplayer.NewRepositoryOptions{
Logger: a.Logger,
Default: settings.MediaPlayer.Default,
VLC: a.MediaPlayer.VLC,
MpcHc: a.MediaPlayer.MpcHc,
Mpv: a.MediaPlayer.Mpv, // Socket
Iina: a.MediaPlayer.Iina,
WSEventManager: a.WSEventManager,
ContinuityManager: a.ContinuityManager,
})
a.PlaybackManager.SetMediaPlayerRepository(a.MediaPlayerRepository)
a.PlaybackManager.SetSettings(&playbackmanager.Settings{
AutoPlayNextEpisode: a.Settings.GetLibrary().AutoPlayNextEpisode,
})
a.DirectStreamManager.SetSettings(&directstream.Settings{
AutoPlayNextEpisode: a.Settings.GetLibrary().AutoPlayNextEpisode,
AutoUpdateProgress: a.Settings.GetLibrary().AutoUpdateProgress,
})
a.TorrentstreamRepository.SetMediaPlayerRepository(a.MediaPlayerRepository)
plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
MediaPlayerRepository: a.MediaPlayerRepository,
})
} else {
a.Logger.Warn().Msg("app: Did not initialize media player module, no settings found")
}
// +---------------------+
// | Torrents |
// +---------------------+
if settings.Torrent != nil {
// Init qBittorrent
qbit := qbittorrent.NewClient(&qbittorrent.NewClientOptions{
Logger: a.Logger,
Username: settings.Torrent.QBittorrentUsername,
Password: settings.Torrent.QBittorrentPassword,
Port: settings.Torrent.QBittorrentPort,
Host: settings.Torrent.QBittorrentHost,
Path: settings.Torrent.QBittorrentPath,
Tags: settings.Torrent.QBittorrentTags,
})
// Login to qBittorrent
go func() {
if settings.Torrent.Default == "qbittorrent" {
err = qbit.Login()
if err != nil {
a.Logger.Error().Err(err).Msg("app: Failed to login to qBittorrent")
} else {
a.Logger.Info().Msg("app: Logged in to qBittorrent")
}
}
}()
// Init Transmission
trans, err := transmission.New(&transmission.NewTransmissionOptions{
Logger: a.Logger,
Username: settings.Torrent.TransmissionUsername,
Password: settings.Torrent.TransmissionPassword,
Port: settings.Torrent.TransmissionPort,
Host: settings.Torrent.TransmissionHost,
Path: settings.Torrent.TransmissionPath,
})
if err != nil && settings.Torrent.TransmissionUsername != "" && settings.Torrent.TransmissionPassword != "" { // Only log error if username and password are set
a.Logger.Error().Err(err).Msg("app: Failed to initialize transmission client")
}
// Shutdown torrent client first
if a.TorrentClientRepository != nil {
a.TorrentClientRepository.Shutdown()
}
// Torrent Client Repository
a.TorrentClientRepository = torrent_client.NewRepository(&torrent_client.NewRepositoryOptions{
Logger: a.Logger,
QbittorrentClient: qbit,
Transmission: trans,
TorrentRepository: a.TorrentRepository,
Provider: settings.Torrent.Default,
MetadataProvider: a.MetadataProvider,
})
a.TorrentClientRepository.InitActiveTorrentCount(settings.Torrent.ShowActiveTorrentCount, a.WSEventManager)
// Set AutoDownloader qBittorrent client
a.AutoDownloader.SetTorrentClientRepository(a.TorrentClientRepository)
plugin.GlobalAppContext.SetModulesPartial(plugin.AppContextModules{
TorrentClientRepository: a.TorrentClientRepository,
AutoDownloader: a.AutoDownloader,
})
} else {
a.Logger.Warn().Msg("app: Did not initialize torrent client module, no settings found")
}
// +---------------------+
// | AutoDownloader |
// +---------------------+
// Update Auto Downloader - This runs in a goroutine
if settings.AutoDownloader != nil {
a.AutoDownloader.SetSettings(settings.AutoDownloader, settings.Library.TorrentProvider)
}
// +---------------------+
// | Library Watcher |
// +---------------------+
// Initialize library watcher
if settings.Library != nil && len(settings.Library.LibraryPath) > 0 {
go func() {
a.initLibraryWatcher(settings.Library.GetLibraryPaths())
}()
}
// +---------------------+
// | Discord |
// +---------------------+
if settings.Discord != nil && a.DiscordPresence != nil {
a.DiscordPresence.SetSettings(settings.Discord)
}
// +---------------------+
// | Continuity |
// +---------------------+
if settings.Library != nil {
a.ContinuityManager.SetSettings(&continuity.Settings{
WatchContinuityEnabled: settings.Library.EnableWatchContinuity,
})
}
if settings.Manga != nil {
a.MangaRepository.SetSettings(settings)
}
// +---------------------+
// | Nakama |
// +---------------------+
if settings.Nakama != nil {
a.NakamaManager.SetSettings(settings.Nakama)
}
runtime.GC()
a.Logger.Info().Msg("app: Refreshed modules")
}
// InitOrRefreshMediastreamSettings will initialize or refresh the mediastream settings.
// It is called after the App instance is created and after settings are updated.
func (a *App) InitOrRefreshMediastreamSettings() {
var settings *models.MediastreamSettings
var found bool
settings, found = a.Database.GetMediastreamSettings()
if !found {
var err error
settings, err = a.Database.UpsertMediastreamSettings(&models.MediastreamSettings{
BaseModel: models.BaseModel{
ID: 1,
},
TranscodeEnabled: false,
TranscodeHwAccel: "cpu",
TranscodePreset: "fast",
PreTranscodeEnabled: false,
})
if err != nil {
a.Logger.Error().Err(err).Msg("app: Failed to initialize mediastream module")
return
}
}
a.MediastreamRepository.InitializeModules(settings, a.Config.Cache.Dir, a.Config.Cache.TranscodeDir)
// Cleanup cache
go func() {
if settings.TranscodeEnabled {
// If transcoding is enabled, trim files
_ = a.FileCacher.TrimMediastreamVideoFiles()
} else {
// If transcoding is disabled, clear all files
_ = a.FileCacher.ClearMediastreamVideoFiles()
}
}()
a.SecondarySettings.Mediastream = settings
}
// InitOrRefreshTorrentstreamSettings will initialize or refresh the mediastream settings.
// It is called after the App instance is created and after settings are updated.
func (a *App) InitOrRefreshTorrentstreamSettings() {
var settings *models.TorrentstreamSettings
var found bool
settings, found = a.Database.GetTorrentstreamSettings()
if !found {
var err error
settings, err = a.Database.UpsertTorrentstreamSettings(&models.TorrentstreamSettings{
BaseModel: models.BaseModel{
ID: 1,
},
Enabled: false,
AutoSelect: true,
PreferredResolution: "",
DisableIPV6: false,
DownloadDir: "",
AddToLibrary: false,
TorrentClientHost: "",
TorrentClientPort: 43213,
StreamingServerHost: "0.0.0.0",
StreamingServerPort: 43214,
IncludeInLibrary: false,
StreamUrlAddress: "",
SlowSeeding: false,
})
if err != nil {
a.Logger.Error().Err(err).Msg("app: Failed to initialize mediastream module")
return
}
}
err := a.TorrentstreamRepository.InitModules(settings, a.Config.Server.Host, a.Config.Server.Port)
if err != nil && settings.Enabled {
a.Logger.Error().Err(err).Msg("app: Failed to initialize Torrent streaming module")
//_, _ = a.Database.UpsertTorrentstreamSettings(&models.TorrentstreamSettings{
// BaseModel: models.BaseModel{
// ID: 1,
// },
// Enabled: false,
//})
}
a.Cleanups = append(a.Cleanups, func() {
a.TorrentstreamRepository.Shutdown()
})
// Set torrent streaming settings in secondary settings
// so the client can use them
a.SecondarySettings.Torrentstream = settings
}
func (a *App) InitOrRefreshDebridSettings() {
settings, found := a.Database.GetDebridSettings()
if !found {
var err error
settings, err = a.Database.UpsertDebridSettings(&models.DebridSettings{
BaseModel: models.BaseModel{
ID: 1,
},
Enabled: false,
Provider: "",
ApiKey: "",
IncludeDebridStreamInLibrary: false,
StreamAutoSelect: false,
StreamPreferredResolution: "",
})
if err != nil {
a.Logger.Error().Err(err).Msg("app: Failed to initialize debrid module")
return
}
}
a.SecondarySettings.Debrid = settings
err := a.DebridClientRepository.InitializeProvider(settings)
if err != nil {
a.Logger.Error().Err(err).Msg("app: Failed to initialize debrid provider")
return
}
}
// InitOrRefreshAnilistData will initialize the Anilist anime collection and the account.
// This function should be called after App.Database is initialized and after settings are updated.
func (a *App) InitOrRefreshAnilistData() {
a.Logger.Debug().Msg("app: Fetching Anilist data")
var currUser *user.User
acc, err := a.Database.GetAccount()
if err != nil || acc.Username == "" {
a.ServerReady = true
currUser = user.NewSimulatedUser() // Create a simulated user if no account is found
} else {
currUser, err = user.NewUser(acc)
if err != nil {
a.Logger.Error().Err(err).Msg("app: Failed to create user from account")
return
}
}
a.user = currUser
// Set username to Anilist platform
a.AnilistPlatform.SetUsername(currUser.Viewer.Name)
a.Logger.Info().Msg("app: Authenticated to AniList")
go func() {
_, err = a.RefreshAnimeCollection()
if err != nil {
a.Logger.Error().Err(err).Msg("app: Failed to fetch Anilist anime collection")
}
a.ServerReady = true
a.WSEventManager.SendEvent(events.ServerReady, nil)
_, err = a.RefreshMangaCollection()
if err != nil {
a.Logger.Error().Err(err).Msg("app: Failed to fetch Anilist manga collection")
}
}()
go func(username string) {
a.DiscordPresence.SetUsername(username)
}(currUser.Viewer.Name)
a.Logger.Info().Msg("app: Fetched Anilist data")
}
func (a *App) performActionsOnce() {
go func() {
if a.Settings == nil || a.Settings.Library == nil {
return
}
if a.Settings.GetLibrary().OpenWebURLOnStart {
// Open the web URL
err := browser.OpenURL(a.Config.GetServerURI("127.0.0.1"))
if err != nil {
a.Logger.Warn().Err(err).Msg("app: Failed to open web URL, please open it manually in your browser")
} else {
a.Logger.Info().Msg("app: Opened web URL")
}
}
if a.Settings.GetLibrary().RefreshLibraryOnStart {
go func() {
a.Logger.Debug().Msg("app: Refreshing library")
a.AutoScanner.RunNow()
a.Logger.Info().Msg("app: Refreshed library")
}()
}
if a.Settings.GetLibrary().OpenTorrentClientOnStart && a.TorrentClientRepository != nil {
// Start the torrent client
ok := a.TorrentClientRepository.Start()
if !ok {
a.Logger.Warn().Msg("app: Failed to open torrent client")
} else {
a.Logger.Info().Msg("app: Started torrent client")
}
}
}()
}