node build fixed
This commit is contained in:
109
seanime-2.9.10/internal/core/anilist.go
Normal file
109
seanime-2.9.10/internal/core/anilist.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/events"
|
||||
"seanime/internal/platforms/platform"
|
||||
"seanime/internal/user"
|
||||
)
|
||||
|
||||
// GetUser returns the currently logged-in user or a simulated one.
|
||||
func (a *App) GetUser() *user.User {
|
||||
if a.user == nil {
|
||||
return user.NewSimulatedUser()
|
||||
}
|
||||
return a.user
|
||||
}
|
||||
|
||||
func (a *App) GetUserAnilistToken() string {
|
||||
if a.user == nil || a.user.Token == user.SimulatedUserToken {
|
||||
return ""
|
||||
}
|
||||
|
||||
return a.user.Token
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// UpdatePlatform changes the current platform to the provided one.
|
||||
func (a *App) UpdatePlatform(platform platform.Platform) {
|
||||
a.AnilistPlatform = platform
|
||||
}
|
||||
|
||||
// UpdateAnilistClientToken will update the Anilist Client Wrapper token.
|
||||
// This function should be called when a user logs in
|
||||
func (a *App) UpdateAnilistClientToken(token string) {
|
||||
a.AnilistClient = anilist.NewAnilistClient(token)
|
||||
a.AnilistPlatform.SetAnilistClient(a.AnilistClient) // Update Anilist Client Wrapper in Platform
|
||||
}
|
||||
|
||||
// GetAnimeCollection returns the user's Anilist collection if it in the cache, otherwise it queries Anilist for the user's collection.
|
||||
// When bypassCache is true, it will always query Anilist for the user's collection
|
||||
func (a *App) GetAnimeCollection(bypassCache bool) (*anilist.AnimeCollection, error) {
|
||||
return a.AnilistPlatform.GetAnimeCollection(context.Background(), bypassCache)
|
||||
}
|
||||
|
||||
// GetRawAnimeCollection is the same as GetAnimeCollection but returns the raw collection that includes custom lists
|
||||
func (a *App) GetRawAnimeCollection(bypassCache bool) (*anilist.AnimeCollection, error) {
|
||||
return a.AnilistPlatform.GetRawAnimeCollection(context.Background(), bypassCache)
|
||||
}
|
||||
|
||||
// RefreshAnimeCollection queries Anilist for the user's collection
|
||||
func (a *App) RefreshAnimeCollection() (*anilist.AnimeCollection, error) {
|
||||
go func() {
|
||||
a.OnRefreshAnilistCollectionFuncs.Range(func(key string, f func()) bool {
|
||||
go f()
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
ret, err := a.AnilistPlatform.RefreshAnimeCollection(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save the collection to PlaybackManager
|
||||
a.PlaybackManager.SetAnimeCollection(ret)
|
||||
|
||||
// Save the collection to AutoDownloader
|
||||
a.AutoDownloader.SetAnimeCollection(ret)
|
||||
|
||||
// Save the collection to LocalManager
|
||||
a.LocalManager.SetAnimeCollection(ret)
|
||||
|
||||
// Save the collection to DirectStreamManager
|
||||
a.DirectStreamManager.SetAnimeCollection(ret)
|
||||
|
||||
a.WSEventManager.SendEvent(events.RefreshedAnilistAnimeCollection, nil)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// GetMangaCollection is the same as GetAnimeCollection but for manga
|
||||
func (a *App) GetMangaCollection(bypassCache bool) (*anilist.MangaCollection, error) {
|
||||
return a.AnilistPlatform.GetMangaCollection(context.Background(), bypassCache)
|
||||
}
|
||||
|
||||
// GetRawMangaCollection does not exclude custom lists
|
||||
func (a *App) GetRawMangaCollection(bypassCache bool) (*anilist.MangaCollection, error) {
|
||||
return a.AnilistPlatform.GetRawMangaCollection(context.Background(), bypassCache)
|
||||
}
|
||||
|
||||
// RefreshMangaCollection queries Anilist for the user's manga collection
|
||||
func (a *App) RefreshMangaCollection() (*anilist.MangaCollection, error) {
|
||||
mc, err := a.AnilistPlatform.RefreshMangaCollection(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.LocalManager.SetMangaCollection(mc)
|
||||
|
||||
a.WSEventManager.SendEvent(events.RefreshedAnilistMangaCollection, nil)
|
||||
|
||||
return mc, nil
|
||||
}
|
||||
440
seanime-2.9.10/internal/core/app.go
Normal file
440
seanime-2.9.10/internal/core/app.go
Normal file
@@ -0,0 +1,440 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
439
seanime-2.9.10/internal/core/config.go
Normal file
439
seanime-2.9.10/internal/core/config.go
Normal file
@@ -0,0 +1,439 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"seanime/internal/constants"
|
||||
"seanime/internal/util"
|
||||
"strconv"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Version string
|
||||
Server struct {
|
||||
Host string
|
||||
Port int
|
||||
Offline bool
|
||||
UseBinaryPath bool // Makes $SEANIME_WORKING_DIR point to the binary's directory
|
||||
Systray bool
|
||||
DoHUrl string
|
||||
Password string
|
||||
}
|
||||
Database struct {
|
||||
Name string
|
||||
}
|
||||
Web struct {
|
||||
AssetDir string
|
||||
}
|
||||
Logs struct {
|
||||
Dir string
|
||||
}
|
||||
Cache struct {
|
||||
Dir string
|
||||
TranscodeDir string
|
||||
}
|
||||
Offline struct {
|
||||
Dir string
|
||||
AssetDir string
|
||||
}
|
||||
Manga struct {
|
||||
DownloadDir string
|
||||
LocalDir string
|
||||
}
|
||||
Data struct { // Hydrated after config is loaded
|
||||
AppDataDir string
|
||||
WorkingDir string
|
||||
}
|
||||
Extensions struct {
|
||||
Dir string
|
||||
}
|
||||
Anilist struct {
|
||||
ClientID string
|
||||
}
|
||||
Experimental struct {
|
||||
MainServerTorrentStreaming bool
|
||||
}
|
||||
}
|
||||
|
||||
type ConfigOptions struct {
|
||||
DataDir string // The path to the Seanime data directory, if any
|
||||
OnVersionChange []func(oldVersion string, newVersion string)
|
||||
EmbeddedLogo []byte // The embedded logo
|
||||
IsDesktopSidecar bool // Run as the desktop sidecar
|
||||
}
|
||||
|
||||
// NewConfig initializes the config
|
||||
func NewConfig(options *ConfigOptions, logger *zerolog.Logger) (*Config, error) {
|
||||
|
||||
logger.Debug().Msg("app: Initializing config")
|
||||
|
||||
// Set Seanime's environment variables
|
||||
if os.Getenv("SEANIME_DATA_DIR") != "" {
|
||||
options.DataDir = os.Getenv("SEANIME_DATA_DIR")
|
||||
}
|
||||
|
||||
defaultHost := "127.0.0.1"
|
||||
defaultPort := 43211
|
||||
|
||||
if os.Getenv("SEANIME_SERVER_HOST") != "" {
|
||||
defaultHost = os.Getenv("SEANIME_SERVER_HOST")
|
||||
}
|
||||
if os.Getenv("SEANIME_SERVER_PORT") != "" {
|
||||
var err error
|
||||
defaultPort, err = strconv.Atoi(os.Getenv("SEANIME_SERVER_PORT"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid SEANIME_SERVER_PORT environment variable: %s", os.Getenv("SEANIME_SERVER_PORT"))
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the app data directory
|
||||
dataDir, configPath, err := initAppDataDir(options.DataDir, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set Seanime's default custom environment variables
|
||||
if err = setDataDirEnv(dataDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Configure viper
|
||||
viper.SetConfigName(constants.ConfigFileName)
|
||||
viper.SetConfigType("toml")
|
||||
viper.SetConfigFile(configPath)
|
||||
|
||||
// Set default values
|
||||
viper.SetDefault("version", constants.Version)
|
||||
viper.SetDefault("server.host", defaultHost)
|
||||
viper.SetDefault("server.port", defaultPort)
|
||||
viper.SetDefault("server.offline", false)
|
||||
// Use the binary's directory as the working directory environment variable on macOS
|
||||
viper.SetDefault("server.useBinaryPath", true)
|
||||
//viper.SetDefault("server.systray", true)
|
||||
viper.SetDefault("database.name", "seanime")
|
||||
viper.SetDefault("web.assetDir", "$SEANIME_DATA_DIR/assets")
|
||||
viper.SetDefault("cache.dir", "$SEANIME_DATA_DIR/cache")
|
||||
viper.SetDefault("cache.transcodeDir", "$SEANIME_DATA_DIR/cache/transcode")
|
||||
viper.SetDefault("manga.downloadDir", "$SEANIME_DATA_DIR/manga")
|
||||
viper.SetDefault("manga.localDir", "$SEANIME_DATA_DIR/manga-local")
|
||||
viper.SetDefault("logs.dir", "$SEANIME_DATA_DIR/logs")
|
||||
viper.SetDefault("offline.dir", "$SEANIME_DATA_DIR/offline")
|
||||
viper.SetDefault("offline.assetDir", "$SEANIME_DATA_DIR/offline/assets")
|
||||
viper.SetDefault("extensions.dir", "$SEANIME_DATA_DIR/extensions")
|
||||
|
||||
// Create and populate the config file if it doesn't exist
|
||||
if err = createConfigFile(configPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read the config file
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal the config values
|
||||
cfg := &Config{}
|
||||
if err := viper.Unmarshal(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update the config if the version has changed
|
||||
if err := updateVersion(cfg, options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Before expanding the values, check if we need to override the working directory
|
||||
if err = setWorkingDirEnv(cfg.Server.UseBinaryPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Expand the values, replacing environment variables
|
||||
expandEnvironmentValues(cfg)
|
||||
cfg.Data.AppDataDir = dataDir
|
||||
cfg.Data.WorkingDir = os.Getenv("SEANIME_WORKING_DIR")
|
||||
|
||||
// Check validity of the config
|
||||
if err := validateConfig(cfg, logger); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go loadLogo(options.EmbeddedLogo, dataDir)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (cfg *Config) GetServerAddr(df ...string) string {
|
||||
return fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
}
|
||||
|
||||
func (cfg *Config) GetServerURI(df ...string) string {
|
||||
pAddr := fmt.Sprintf("http://%s", cfg.GetServerAddr(df...))
|
||||
if cfg.Server.Host == "" || cfg.Server.Host == "0.0.0.0" {
|
||||
pAddr = fmt.Sprintf(":%d", cfg.Server.Port)
|
||||
if len(df) > 0 {
|
||||
pAddr = fmt.Sprintf("http://%s:%d", df[0], cfg.Server.Port)
|
||||
}
|
||||
}
|
||||
return pAddr
|
||||
}
|
||||
|
||||
func getWorkingDir(useBinaryPath bool) (string, error) {
|
||||
// Get the working directory
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
binaryDir := ""
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
if p, err := filepath.EvalSymlinks(exe); err == nil {
|
||||
binaryDir = filepath.Dir(p)
|
||||
binaryDir = filepath.FromSlash(binaryDir)
|
||||
}
|
||||
}
|
||||
|
||||
if useBinaryPath && binaryDir != "" {
|
||||
return binaryDir, nil
|
||||
}
|
||||
|
||||
//// Use the binary's directory as the working directory if needed
|
||||
//if useBinaryPath {
|
||||
// exe, err := os.Executable()
|
||||
// if err != nil {
|
||||
// return wd, nil // Fallback to working dir
|
||||
// }
|
||||
// p, err := filepath.EvalSymlinks(exe)
|
||||
// if err != nil {
|
||||
// return wd, nil // Fallback to working dir
|
||||
// }
|
||||
// wd = filepath.Dir(p) // Set the binary's directory as the working directory
|
||||
// return wd, nil
|
||||
//}
|
||||
return wd, nil
|
||||
}
|
||||
|
||||
func setDataDirEnv(dataDir string) error {
|
||||
// Set the data directory environment variable
|
||||
if os.Getenv("SEANIME_DATA_DIR") == "" {
|
||||
if err := os.Setenv("SEANIME_DATA_DIR", dataDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setWorkingDirEnv(useBinaryPath bool) error {
|
||||
// Set the working directory environment variable
|
||||
wd, err := getWorkingDir(useBinaryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Setenv("SEANIME_WORKING_DIR", filepath.FromSlash(wd)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateConfig checks if the config values are valid
|
||||
func validateConfig(cfg *Config, logger *zerolog.Logger) error {
|
||||
if cfg.Server.Host == "" {
|
||||
return errInvalidConfigValue("server.host", "cannot be empty")
|
||||
}
|
||||
if cfg.Server.Port == 0 {
|
||||
return errInvalidConfigValue("server.port", "cannot be 0")
|
||||
}
|
||||
if cfg.Database.Name == "" {
|
||||
return errInvalidConfigValue("database.name", "cannot be empty")
|
||||
}
|
||||
if cfg.Web.AssetDir == "" {
|
||||
return errInvalidConfigValue("web.assetDir", "cannot be empty")
|
||||
}
|
||||
if err := checkIsValidPath(cfg.Web.AssetDir); err != nil {
|
||||
return wrapInvalidConfigValue("web.assetDir", err)
|
||||
}
|
||||
|
||||
if cfg.Cache.Dir == "" {
|
||||
return errInvalidConfigValue("cache.dir", "cannot be empty")
|
||||
}
|
||||
if err := checkIsValidPath(cfg.Cache.Dir); err != nil {
|
||||
return wrapInvalidConfigValue("cache.dir", err)
|
||||
}
|
||||
|
||||
if cfg.Cache.TranscodeDir == "" {
|
||||
return errInvalidConfigValue("cache.transcodeDir", "cannot be empty")
|
||||
}
|
||||
if err := checkIsValidPath(cfg.Cache.TranscodeDir); err != nil {
|
||||
return wrapInvalidConfigValue("cache.transcodeDir", err)
|
||||
}
|
||||
|
||||
if cfg.Logs.Dir == "" {
|
||||
return errInvalidConfigValue("logs.dir", "cannot be empty")
|
||||
}
|
||||
if err := checkIsValidPath(cfg.Logs.Dir); err != nil {
|
||||
return wrapInvalidConfigValue("logs.dir", err)
|
||||
}
|
||||
|
||||
if cfg.Manga.DownloadDir == "" {
|
||||
return errInvalidConfigValue("manga.downloadDir", "cannot be empty")
|
||||
}
|
||||
if err := checkIsValidPath(cfg.Manga.DownloadDir); err != nil {
|
||||
return wrapInvalidConfigValue("manga.downloadDir", err)
|
||||
}
|
||||
|
||||
if cfg.Manga.LocalDir == "" {
|
||||
return errInvalidConfigValue("manga.localDir", "cannot be empty")
|
||||
}
|
||||
if err := checkIsValidPath(cfg.Manga.LocalDir); err != nil {
|
||||
return wrapInvalidConfigValue("manga.localDir", err)
|
||||
}
|
||||
|
||||
if cfg.Extensions.Dir == "" {
|
||||
return errInvalidConfigValue("extensions.dir", "cannot be empty")
|
||||
}
|
||||
if err := checkIsValidPath(cfg.Extensions.Dir); err != nil {
|
||||
return wrapInvalidConfigValue("extensions.dir", err)
|
||||
}
|
||||
|
||||
// Uncomment if "MainServerTorrentStreaming" is no longer an experimental feature
|
||||
if cfg.Experimental.MainServerTorrentStreaming {
|
||||
logger.Warn().Msgf("app: 'Main Server Torrent Streaming' feature is no longer experimental, remove the flag from your config file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkIsValidPath(path string) error {
|
||||
ok := filepath.IsAbs(path)
|
||||
if !ok {
|
||||
return errors.New("path is not an absolute path")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// errInvalidConfigValue returns an error for an invalid config value
|
||||
func errInvalidConfigValue(s string, s2 string) error {
|
||||
return fmt.Errorf("invalid config value: \"%s\" %s", s, s2)
|
||||
}
|
||||
func wrapInvalidConfigValue(s string, err error) error {
|
||||
return fmt.Errorf("invalid config value: \"%s\" %w", s, err)
|
||||
}
|
||||
|
||||
func updateVersion(cfg *Config, opts *ConfigOptions) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Do nothing
|
||||
}
|
||||
}()
|
||||
|
||||
if cfg.Version != constants.Version {
|
||||
for _, f := range opts.OnVersionChange {
|
||||
f(cfg.Version, constants.Version)
|
||||
}
|
||||
cfg.Version = constants.Version
|
||||
}
|
||||
|
||||
viper.Set("version", constants.Version)
|
||||
|
||||
return viper.WriteConfig()
|
||||
}
|
||||
|
||||
func expandEnvironmentValues(cfg *Config) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// Do nothing
|
||||
}
|
||||
}()
|
||||
cfg.Web.AssetDir = filepath.FromSlash(os.ExpandEnv(cfg.Web.AssetDir))
|
||||
cfg.Cache.Dir = filepath.FromSlash(os.ExpandEnv(cfg.Cache.Dir))
|
||||
cfg.Cache.TranscodeDir = filepath.FromSlash(os.ExpandEnv(cfg.Cache.TranscodeDir))
|
||||
cfg.Logs.Dir = filepath.FromSlash(os.ExpandEnv(cfg.Logs.Dir))
|
||||
cfg.Manga.DownloadDir = filepath.FromSlash(os.ExpandEnv(cfg.Manga.DownloadDir))
|
||||
cfg.Manga.LocalDir = filepath.FromSlash(os.ExpandEnv(cfg.Manga.LocalDir))
|
||||
cfg.Offline.Dir = filepath.FromSlash(os.ExpandEnv(cfg.Offline.Dir))
|
||||
cfg.Offline.AssetDir = filepath.FromSlash(os.ExpandEnv(cfg.Offline.AssetDir))
|
||||
cfg.Extensions.Dir = filepath.FromSlash(os.ExpandEnv(cfg.Extensions.Dir))
|
||||
}
|
||||
|
||||
// createConfigFile creates a default config file if it doesn't exist
|
||||
func createConfigFile(configPath string) error {
|
||||
_, err := os.Stat(configPath)
|
||||
if os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initAppDataDir(definedDataDir string, logger *zerolog.Logger) (dataDir string, configPath string, err error) {
|
||||
|
||||
// User defined data directory
|
||||
if definedDataDir != "" {
|
||||
|
||||
// Expand environment variables
|
||||
definedDataDir = filepath.FromSlash(os.ExpandEnv(definedDataDir))
|
||||
|
||||
if !filepath.IsAbs(definedDataDir) {
|
||||
return "", "", errors.New("app: Data directory path must be absolute")
|
||||
}
|
||||
|
||||
// Replace the default data directory
|
||||
dataDir = definedDataDir
|
||||
|
||||
logger.Trace().Str("dataDir", dataDir).Msg("app: Overriding default data directory")
|
||||
} else {
|
||||
// Default OS data directory
|
||||
// windows: %APPDATA%
|
||||
// unix: $XDG_CONFIG_HOME or $HOME
|
||||
// darwin: $HOME/Library/Application Support
|
||||
dataDir, err = os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
// Get the app directory
|
||||
dataDir = filepath.Join(dataDir, "Seanime")
|
||||
}
|
||||
|
||||
// Create data dir if it doesn't exist
|
||||
if err := os.MkdirAll(dataDir, 0700); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// Get the config file path
|
||||
// Normalize the config file path
|
||||
configPath = filepath.FromSlash(filepath.Join(dataDir, constants.ConfigFileName))
|
||||
// Normalize the data directory path
|
||||
dataDir = filepath.FromSlash(dataDir)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func loadLogo(embeddedLogo []byte, dataDir string) (err error) {
|
||||
defer util.HandlePanicInModuleWithError("core/loadLogo", &err)
|
||||
|
||||
if len(embeddedLogo) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
logoPath := filepath.Join(dataDir, "logo.png")
|
||||
if _, err = os.Stat(logoPath); os.IsNotExist(err) {
|
||||
if err = os.WriteFile(logoPath, embeddedLogo, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
93
seanime-2.9.10/internal/core/echo.go
Normal file
93
seanime-2.9.10/internal/core/echo.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
func NewEchoApp(app *App, webFS *embed.FS) *echo.Echo {
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
e.Debug = false
|
||||
e.JSONSerializer = &CustomJSONSerializer{}
|
||||
|
||||
distFS, err := fs.Sub(webFS, "web")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
Filesystem: http.FS(distFS),
|
||||
Browse: true,
|
||||
HTML5: true,
|
||||
Skipper: func(c echo.Context) bool {
|
||||
cUrl := c.Request().URL
|
||||
if strings.HasPrefix(cUrl.RequestURI(), "/api") ||
|
||||
strings.HasPrefix(cUrl.RequestURI(), "/events") ||
|
||||
strings.HasPrefix(cUrl.RequestURI(), "/assets") ||
|
||||
strings.HasPrefix(cUrl.RequestURI(), "/manga-downloads") ||
|
||||
strings.HasPrefix(cUrl.RequestURI(), "/offline-assets") {
|
||||
return true // Continue to the next handler
|
||||
}
|
||||
if !strings.HasSuffix(cUrl.Path, ".html") && filepath.Ext(cUrl.Path) == "" {
|
||||
cUrl.Path = cUrl.Path + ".html"
|
||||
}
|
||||
if cUrl.Path == "/.html" {
|
||||
cUrl.Path = "/index.html"
|
||||
}
|
||||
return false // Continue to the filesystem handler
|
||||
},
|
||||
}))
|
||||
|
||||
app.Logger.Info().Msgf("app: Serving embedded web interface")
|
||||
|
||||
// Serve web assets
|
||||
app.Logger.Info().Msgf("app: Web assets path: %s", app.Config.Web.AssetDir)
|
||||
e.Static("/assets", app.Config.Web.AssetDir)
|
||||
|
||||
// Serve manga downloads
|
||||
if app.Config.Manga.DownloadDir != "" {
|
||||
app.Logger.Info().Msgf("app: Manga downloads path: %s", app.Config.Manga.DownloadDir)
|
||||
e.Static("/manga-downloads", app.Config.Manga.DownloadDir)
|
||||
}
|
||||
|
||||
// Serve offline assets
|
||||
app.Logger.Info().Msgf("app: Offline assets path: %s", app.Config.Offline.AssetDir)
|
||||
e.Static("/offline-assets", app.Config.Offline.AssetDir)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
type CustomJSONSerializer struct{}
|
||||
|
||||
func (j *CustomJSONSerializer) Serialize(c echo.Context, i interface{}, indent string) error {
|
||||
enc := json.NewEncoder(c.Response())
|
||||
return enc.Encode(i)
|
||||
}
|
||||
|
||||
func (j *CustomJSONSerializer) Deserialize(c echo.Context, i interface{}) error {
|
||||
dec := json.NewDecoder(c.Request().Body)
|
||||
return dec.Decode(i)
|
||||
}
|
||||
|
||||
func RunEchoServer(app *App, e *echo.Echo) {
|
||||
app.Logger.Info().Msgf("app: Server Address: %s", app.Config.GetServerAddr())
|
||||
|
||||
// Start the server
|
||||
go func() {
|
||||
log.Fatal(e.Start(app.Config.GetServerAddr()))
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
app.Logger.Info().Msg("app: Seanime started at " + app.Config.GetServerURI())
|
||||
}
|
||||
263
seanime-2.9.10/internal/core/extensions.go
Normal file
263
seanime-2.9.10/internal/core/extensions.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/extension_repo"
|
||||
manga_providers "seanime/internal/manga/providers"
|
||||
onlinestream_providers "seanime/internal/onlinestream/providers"
|
||||
"seanime/internal/torrents/animetosho"
|
||||
"seanime/internal/torrents/nyaa"
|
||||
"seanime/internal/torrents/seadex"
|
||||
"seanime/internal/util"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func LoadExtensions(extensionRepository *extension_repo.Repository, logger *zerolog.Logger, config *Config) {
|
||||
|
||||
//
|
||||
// Built-in manga providers
|
||||
//
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: "comick",
|
||||
Name: "ComicK",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeMangaProvider,
|
||||
Author: "Seanime",
|
||||
Description: "",
|
||||
Lang: "en",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/comick.webp",
|
||||
}, manga_providers.NewComicK(logger))
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: "comick-multi",
|
||||
Name: "ComicK (Multi)",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeMangaProvider,
|
||||
Author: "Seanime",
|
||||
Description: "",
|
||||
Lang: "multi",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/comick.webp",
|
||||
}, manga_providers.NewComicKMulti(logger))
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: "mangapill",
|
||||
Name: "Mangapill",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeMangaProvider,
|
||||
Author: "Seanime",
|
||||
Lang: "en",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/mangapill.png",
|
||||
}, manga_providers.NewMangapill(logger))
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: "weebcentral",
|
||||
Name: "WeebCentral",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeMangaProvider,
|
||||
Author: "Seanime",
|
||||
Lang: "en",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/weebcentral.png",
|
||||
}, manga_providers.NewWeebCentral(logger))
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: "mangadex",
|
||||
Name: "Mangadex",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeMangaProvider,
|
||||
Author: "Seanime",
|
||||
Lang: "en",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/mangadex.png",
|
||||
}, manga_providers.NewMangadex(logger))
|
||||
|
||||
//extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
// ID: "manganato",
|
||||
// Name: "Manganato",
|
||||
// Version: "",
|
||||
// ManifestURI: "builtin",
|
||||
// Language: extension.LanguageGo,
|
||||
// Type: extension.TypeMangaProvider,
|
||||
// Author: "Seanime",
|
||||
// Lang: "en",
|
||||
// Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/manganato.png",
|
||||
//}, manga_providers.NewManganato(logger))
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: manga_providers.LocalProvider,
|
||||
Name: "Local",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeMangaProvider,
|
||||
Author: "Seanime",
|
||||
Lang: "multi",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/local-manga.png",
|
||||
}, manga_providers.NewLocal(config.Manga.LocalDir, logger))
|
||||
|
||||
//
|
||||
// Built-in online stream providers
|
||||
//
|
||||
|
||||
//extensionRepository.LoadBuiltInOnlinestreamProviderExtension(extension.Extension{
|
||||
// ID: "gogoanime",
|
||||
// Name: "Gogoanime",
|
||||
// Version: "",
|
||||
// ManifestURI: "builtin",
|
||||
// Language: extension.LanguageGo,
|
||||
// Type: extension.TypeOnlinestreamProvider,
|
||||
// Author: "Seanime",
|
||||
// Lang: "en",
|
||||
// Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/gogoanime.png",
|
||||
//}, onlinestream_providers.NewGogoanime(logger))
|
||||
|
||||
//extensionRepository.LoadBuiltInOnlinestreamProviderExtension(extension.Extension{
|
||||
// ID: "zoro",
|
||||
// Name: "Hianime",
|
||||
// Version: "",
|
||||
// ManifestURI: "builtin",
|
||||
// Language: extension.LanguageGo,
|
||||
// Type: extension.TypeOnlinestreamProvider,
|
||||
// Author: "Seanime",
|
||||
// Lang: "en",
|
||||
// Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/hianime.png",
|
||||
//}, onlinestream_providers.NewZoro(logger))
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: "animepahe",
|
||||
Name: "Animepahe",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageTypescript,
|
||||
Type: extension.TypeOnlinestreamProvider,
|
||||
Author: "Seanime",
|
||||
Lang: "en",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/animepahe.png",
|
||||
Payload: onlinestream_providers.AnimepahePayload,
|
||||
}, nil)
|
||||
|
||||
//
|
||||
// Built-in torrent providers
|
||||
//
|
||||
|
||||
nyaaUserConfig := extension.UserConfig{
|
||||
Version: 1,
|
||||
Fields: []extension.ConfigField{
|
||||
{
|
||||
Name: "apiUrl",
|
||||
Label: "API URL",
|
||||
Type: extension.ConfigFieldTypeText,
|
||||
Default: util.Decode("aHR0cHM6Ly9ueWFhLnNpLz9wYWdlPXJzcyZxPSs="),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: "nyaa",
|
||||
Name: "Nyaa",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeAnimeTorrentProvider,
|
||||
Author: "Seanime",
|
||||
Lang: "en",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/nyaa.png",
|
||||
UserConfig: &nyaaUserConfig,
|
||||
}, nyaa.NewProvider(logger, "anime-eng"))
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: "nyaa-non-eng",
|
||||
Name: "Nyaa (Non-English)",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeAnimeTorrentProvider,
|
||||
Author: "Seanime",
|
||||
Lang: "multi",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/nyaa.png",
|
||||
UserConfig: &nyaaUserConfig,
|
||||
}, nyaa.NewProvider(logger, "anime-non-eng"))
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: "nyaa-sukebei",
|
||||
Name: "Nyaa Sukebei",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeAnimeTorrentProvider,
|
||||
Author: "Seanime",
|
||||
Lang: "en",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/nyaa.png",
|
||||
UserConfig: &extension.UserConfig{
|
||||
Version: 1,
|
||||
Fields: []extension.ConfigField{
|
||||
{
|
||||
Name: "apiUrl",
|
||||
Label: "API URL",
|
||||
Type: extension.ConfigFieldTypeText,
|
||||
Default: util.Decode("aHR0cHM6Ly9zdWtlYmVpLm55YWEuc2kvP3BhZ2U9cnNzJnE9Kw=="),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nyaa.NewSukebeiProvider(logger))
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: "animetosho",
|
||||
Name: "AnimeTosho",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeAnimeTorrentProvider,
|
||||
Author: "Seanime",
|
||||
Lang: "en",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/animetosho.png",
|
||||
}, animetosho.NewProvider(logger))
|
||||
|
||||
extensionRepository.ReloadBuiltInExtension(extension.Extension{
|
||||
ID: "seadex",
|
||||
Name: "SeaDex",
|
||||
Version: "",
|
||||
ManifestURI: "builtin",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeAnimeTorrentProvider,
|
||||
Author: "Seanime",
|
||||
Lang: "en",
|
||||
Icon: "https://raw.githubusercontent.com/5rahim/hibike/main/icons/seadex.png",
|
||||
UserConfig: &extension.UserConfig{
|
||||
Version: 1,
|
||||
Fields: []extension.ConfigField{
|
||||
{
|
||||
Name: "apiUrl",
|
||||
Label: "API URL",
|
||||
Type: extension.ConfigFieldTypeText,
|
||||
Default: util.Decode("aHR0cHM6Ly9yZWxlYXNlcy5tb2UvYXBpL2NvbGxlY3Rpb25zL2VudHJpZXMvcmVjb3Jkcw=="),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, seadex.NewProvider(logger))
|
||||
|
||||
extensionRepository.ReloadExternalExtensions()
|
||||
}
|
||||
|
||||
func (a *App) AddExtensionBankToConsumers() {
|
||||
|
||||
var consumers = []extension.Consumer{
|
||||
a.MangaRepository,
|
||||
a.OnlinestreamRepository,
|
||||
a.TorrentRepository,
|
||||
}
|
||||
|
||||
for _, consumer := range consumers {
|
||||
consumer.InitExtensionBank(a.ExtensionRepository.GetExtensionBank())
|
||||
}
|
||||
}
|
||||
36
seanime-2.9.10/internal/core/feature_flags.go
Normal file
36
seanime-2.9.10/internal/core/feature_flags.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type (
|
||||
FeatureFlags struct {
|
||||
MainServerTorrentStreaming bool
|
||||
}
|
||||
|
||||
ExperimentalFeatureFlags struct {
|
||||
}
|
||||
)
|
||||
|
||||
// NewFeatureFlags initializes the feature flags
|
||||
func NewFeatureFlags(cfg *Config, logger *zerolog.Logger) FeatureFlags {
|
||||
ff := FeatureFlags{
|
||||
MainServerTorrentStreaming: viper.GetBool("experimental.mainServerTorrentStreaming"),
|
||||
}
|
||||
|
||||
checkExperimentalFeatureFlags(&ff, cfg, logger)
|
||||
|
||||
return ff
|
||||
}
|
||||
|
||||
func checkExperimentalFeatureFlags(ff *FeatureFlags, cfg *Config, logger *zerolog.Logger) {
|
||||
if ff.MainServerTorrentStreaming {
|
||||
logger.Warn().Msg("app: [Feature flag] 'Main Server Torrent Streaming' experimental feature is enabled")
|
||||
}
|
||||
}
|
||||
|
||||
func (ff *FeatureFlags) IsMainServerTorrentStreamingEnabled() bool {
|
||||
return ff.MainServerTorrentStreaming
|
||||
}
|
||||
43
seanime-2.9.10/internal/core/flags.go
Normal file
43
seanime-2.9.10/internal/core/flags.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
SeanimeFlags struct {
|
||||
DataDir string
|
||||
Update bool
|
||||
IsDesktopSidecar bool
|
||||
}
|
||||
)
|
||||
|
||||
func GetSeanimeFlags() SeanimeFlags {
|
||||
// Help flag
|
||||
flag.Usage = func() {
|
||||
fmt.Printf("Self-hosted, user-friendly, media server for anime and manga enthusiasts.\n\n")
|
||||
fmt.Printf("Usage:\n seanime [flags]\n\n")
|
||||
fmt.Printf("Flags:\n")
|
||||
fmt.Printf(" -datadir, --datadir string")
|
||||
fmt.Printf(" directory that contains all Seanime data\n")
|
||||
fmt.Printf(" -update")
|
||||
fmt.Printf(" update the application\n")
|
||||
fmt.Printf(" -h show this help message\n")
|
||||
}
|
||||
// Parse flags
|
||||
var dataDir string
|
||||
flag.StringVar(&dataDir, "datadir", "", "Directory that contains all Seanime data")
|
||||
var update bool
|
||||
flag.BoolVar(&update, "update", false, "Update the application")
|
||||
var isDesktopSidecar bool
|
||||
flag.BoolVar(&isDesktopSidecar, "desktop-sidecar", false, "Run as the desktop sidecar")
|
||||
flag.Parse()
|
||||
|
||||
return SeanimeFlags{
|
||||
DataDir: strings.TrimSpace(dataDir),
|
||||
Update: update,
|
||||
IsDesktopSidecar: isDesktopSidecar,
|
||||
}
|
||||
}
|
||||
19
seanime-2.9.10/internal/core/hmac_auth.go
Normal file
19
seanime-2.9.10/internal/core/hmac_auth.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"seanime/internal/util"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetServerPasswordHMACAuth returns an HMAC authenticator using the hashed server password as the base secret
|
||||
// This is used for server endpoints that don't use Nakama
|
||||
func (a *App) GetServerPasswordHMACAuth() *util.HMACAuth {
|
||||
var secret string
|
||||
if a.Config != nil && a.Config.Server.Password != "" {
|
||||
secret = a.ServerPasswordHash
|
||||
} else {
|
||||
secret = "seanime-default-secret"
|
||||
}
|
||||
|
||||
return util.NewHMACAuth(secret, 24*time.Hour)
|
||||
}
|
||||
83
seanime-2.9.10/internal/core/logging.go
Normal file
83
seanime-2.9.10/internal/core/logging.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TrimLogEntries(dir string, logger *zerolog.Logger) {
|
||||
// Get all log files in the directory
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("core: Failed to read log directory")
|
||||
return
|
||||
}
|
||||
|
||||
// Get the total size of all log entries
|
||||
var totalSize int64
|
||||
for _, file := range entries {
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
totalSize += info.Size()
|
||||
}
|
||||
|
||||
var files []os.FileInfo
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
files = append(files, info)
|
||||
}
|
||||
|
||||
var serverLogFiles []os.FileInfo
|
||||
var scanLogFiles []os.FileInfo
|
||||
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file.Name(), "seanime-") {
|
||||
serverLogFiles = append(serverLogFiles, file)
|
||||
} else if strings.Contains(file.Name(), "-scan") {
|
||||
scanLogFiles = append(scanLogFiles, file)
|
||||
}
|
||||
}
|
||||
|
||||
for _, _files := range [][]os.FileInfo{serverLogFiles, scanLogFiles} {
|
||||
files := _files
|
||||
if len(files) <= 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Sort from newest to oldest
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].ModTime().After(files[j].ModTime())
|
||||
})
|
||||
|
||||
// Delete all log files older than 14 days
|
||||
deleted := 0
|
||||
for i := 1; i < len(files); i++ {
|
||||
if time.Since(files[i].ModTime()) > 14*24*time.Hour {
|
||||
err := os.Remove(filepath.Join(dir, files[i].Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
}
|
||||
if deleted > 0 {
|
||||
logger.Info().Msgf("app: Deleted %d log files older than 14 days", deleted)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
117
seanime-2.9.10/internal/core/migrations.go
Normal file
117
seanime-2.9.10/internal/core/migrations.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"seanime/internal/constants"
|
||||
"seanime/internal/util"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
)
|
||||
|
||||
func (a *App) runMigrations() {
|
||||
|
||||
go func() {
|
||||
done := false
|
||||
defer func() {
|
||||
if done {
|
||||
a.Logger.Info().Msg("app: Version migration complete")
|
||||
}
|
||||
}()
|
||||
defer util.HandlePanicThen(func() {
|
||||
a.Logger.Error().Msg("app: runMigrations failed")
|
||||
})
|
||||
|
||||
previousVersion, err := semver.NewVersion(a.previousVersion)
|
||||
if err != nil {
|
||||
a.Logger.Error().Err(err).Msg("app: Failed to parse previous version")
|
||||
return
|
||||
}
|
||||
|
||||
if a.previousVersion != constants.Version {
|
||||
|
||||
hasUpdated := util.VersionIsOlderThan(a.previousVersion, constants.Version)
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
// DEVNOTE: 1.2.0 uses an incorrect manga cache format for MangaSee pages
|
||||
// This migration will remove all manga cache files that start with "manga_"
|
||||
if a.previousVersion == "1.2.0" && hasUpdated {
|
||||
a.Logger.Debug().Msg("app: Executing version migration task")
|
||||
err := a.FileCacher.RemoveAllBy(func(filename string) bool {
|
||||
return strings.HasPrefix(filename, "manga_")
|
||||
})
|
||||
if err != nil {
|
||||
a.Logger.Error().Err(err).Msg("app: MIGRATION FAILED; READ THIS")
|
||||
a.Logger.Error().Msg("app: Failed to remove 'manga' cache files, please clear them manually by going to the settings. Ignore this message if you have no manga cache files.")
|
||||
}
|
||||
done = true
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
|
||||
c1, _ := semver.NewConstraint("<= 1.3.0, >= 1.2.0")
|
||||
if c1.Check(previousVersion) {
|
||||
a.Logger.Debug().Msg("app: Executing version migration task")
|
||||
err := a.FileCacher.RemoveAllBy(func(filename string) bool {
|
||||
return strings.HasPrefix(filename, "manga_")
|
||||
})
|
||||
if err != nil {
|
||||
a.Logger.Error().Err(err).Msg("app: MIGRATION FAILED; READ THIS")
|
||||
a.Logger.Error().Msg("app: Failed to remove 'manga' cache files, please clear them manually by going to the settings. Ignore this message if you have no manga cache files.")
|
||||
}
|
||||
done = true
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
|
||||
// DEVNOTE: 1.5.6 uses a different cache format for media streaming info
|
||||
// -> Delete the cache files when updated from any version between 1.5.0 and 1.5.5
|
||||
c2, _ := semver.NewConstraint("<= 1.5.5, >= 1.5.0")
|
||||
if c2.Check(previousVersion) {
|
||||
a.Logger.Debug().Msg("app: Executing version migration task")
|
||||
err := a.FileCacher.RemoveAllBy(func(filename string) bool {
|
||||
return strings.HasPrefix(filename, "mediastream_mediainfo_")
|
||||
})
|
||||
if err != nil {
|
||||
a.Logger.Error().Err(err).Msg("app: MIGRATION FAILED; READ THIS")
|
||||
a.Logger.Error().Msg("app: Failed to remove transcoding cache files, please clear them manually by going to the settings. Ignore this message if you have no transcoding cache files.")
|
||||
}
|
||||
done = true
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
|
||||
// DEVNOTE: 2.0.0 uses a different cache format for online streaming
|
||||
// -> Delete the cache files when updated from a version older than 2.0.0 and newer than 1.5.0
|
||||
c3, _ := semver.NewConstraint("< 2.0.0, >= 1.5.0")
|
||||
if c3.Check(previousVersion) {
|
||||
a.Logger.Debug().Msg("app: Executing version migration task")
|
||||
err := a.FileCacher.RemoveAllBy(func(filename string) bool {
|
||||
return strings.HasPrefix(filename, "onlinestream_")
|
||||
})
|
||||
if err != nil {
|
||||
a.Logger.Error().Err(err).Msg("app: MIGRATION FAILED; READ THIS")
|
||||
a.Logger.Error().Msg("app: Failed to remove online streaming cache files, please clear them manually by going to the settings. Ignore this message if you have no online streaming cache files.")
|
||||
}
|
||||
done = true
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------------------
|
||||
|
||||
// DEVNOTE: 2.1.0 refactored the manga cache format
|
||||
// -> Delete the cache files when updated from a version older than 2.1.0
|
||||
c4, _ := semver.NewConstraint("< 2.1.0")
|
||||
if c4.Check(previousVersion) {
|
||||
a.Logger.Debug().Msg("app: Executing version migration task")
|
||||
err := a.FileCacher.RemoveAllBy(func(filename string) bool {
|
||||
return strings.HasPrefix(filename, "manga_")
|
||||
})
|
||||
if err != nil {
|
||||
a.Logger.Error().Err(err).Msg("app: MIGRATION FAILED; READ THIS")
|
||||
a.Logger.Error().Msg("app: Failed to remove 'manga' cache files, please clear them manually by going to the settings. Ignore this message if you have no manga cache files.")
|
||||
}
|
||||
done = true
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
727
seanime-2.9.10/internal/core/modules.go
Normal file
727
seanime-2.9.10/internal/core/modules.go
Normal file
@@ -0,0 +1,727 @@
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
39
seanime-2.9.10/internal/core/offline.go
Normal file
39
seanime-2.9.10/internal/core/offline.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"seanime/internal/api/metadata"
|
||||
"seanime/internal/platforms/anilist_platform"
|
||||
"seanime/internal/platforms/offline_platform"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// SetOfflineMode changes the offline mode.
|
||||
// It updates the config and active AniList platform.
|
||||
func (a *App) SetOfflineMode(enabled bool) {
|
||||
// Update the config
|
||||
a.Config.Server.Offline = enabled
|
||||
viper.Set("server.offline", enabled)
|
||||
err := viper.WriteConfig()
|
||||
if err != nil {
|
||||
a.Logger.Err(err).Msg("app: Failed to write config after setting offline mode")
|
||||
}
|
||||
a.Logger.Info().Bool("enabled", enabled).Msg("app: Offline mode set")
|
||||
a.isOffline = &enabled
|
||||
|
||||
// Update the platform and metadata provider
|
||||
if enabled {
|
||||
a.AnilistPlatform, _ = offline_platform.NewOfflinePlatform(a.LocalManager, a.AnilistClient, a.Logger)
|
||||
a.MetadataProvider = a.LocalManager.GetOfflineMetadataProvider()
|
||||
} else {
|
||||
// DEVNOTE: We don't handle local platform since the feature doesn't allow offline mode
|
||||
a.AnilistPlatform = anilist_platform.NewAnilistPlatform(a.AnilistClient, a.Logger)
|
||||
a.MetadataProvider = metadata.NewProvider(&metadata.NewProviderImplOptions{
|
||||
Logger: a.Logger,
|
||||
FileCacher: a.FileCacher,
|
||||
})
|
||||
a.InitOrRefreshAnilistData()
|
||||
}
|
||||
|
||||
a.InitOrRefreshModules()
|
||||
}
|
||||
122
seanime-2.9.10/internal/core/tui.go
Normal file
122
seanime-2.9.10/internal/core/tui.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"seanime/internal/constants"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func PrintHeader() {
|
||||
// Get terminal width
|
||||
physicalWidth, _, _ := term.GetSize(int(os.Stdout.Fd()))
|
||||
|
||||
// Color scheme
|
||||
// primary := lipgloss.Color("#7B61FF")
|
||||
// secondary := lipgloss.Color("#5243CB")
|
||||
// highlight := lipgloss.Color("#14F9D5")
|
||||
// versionBgColor := lipgloss.Color("#8A2BE2")
|
||||
subtle := lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"}
|
||||
|
||||
// Base styles
|
||||
docStyle := lipgloss.NewStyle().Padding(1, 2)
|
||||
if physicalWidth > 0 {
|
||||
docStyle = docStyle.MaxWidth(physicalWidth)
|
||||
}
|
||||
|
||||
// Build the header
|
||||
doc := strings.Builder{}
|
||||
|
||||
// Logo with gradient effect
|
||||
logoStyle := lipgloss.NewStyle().Bold(true)
|
||||
logoLines := strings.Split(asciiLogo(), "\n")
|
||||
|
||||
// Create a gradient effect for the logo
|
||||
gradientColors := []string{"#9370DB", "#8A2BE2", "#7B68EE", "#6A5ACD", "#5243CB"}
|
||||
for i, line := range logoLines {
|
||||
colorIdx := i % len(gradientColors)
|
||||
coloredLine := logoStyle.Foreground(lipgloss.Color(gradientColors[colorIdx])).Render(line)
|
||||
doc.WriteString(coloredLine + "\n")
|
||||
}
|
||||
|
||||
// App name and version with box
|
||||
titleBox := lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(subtle).
|
||||
Foreground(lipgloss.Color("#FFF7DB")).
|
||||
// Background(secondary).
|
||||
Padding(0, 1).
|
||||
Bold(true).
|
||||
Render("Seanime")
|
||||
|
||||
versionBox := lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(subtle).
|
||||
Foreground(lipgloss.Color("#ed4760")).
|
||||
// Background(versionBgColor).
|
||||
Padding(0, 1).
|
||||
Bold(true).
|
||||
Render(constants.Version)
|
||||
|
||||
// Version name with different style
|
||||
versionName := lipgloss.NewStyle().
|
||||
Italic(true).
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(subtle).
|
||||
Foreground(lipgloss.Color("#FFF7DB")).
|
||||
// Background(versionBgColor).
|
||||
Padding(0, 1).
|
||||
Render(constants.VersionName)
|
||||
|
||||
// Combine title elements
|
||||
titleRow := lipgloss.JoinHorizontal(lipgloss.Center, titleBox, versionBox, versionName)
|
||||
|
||||
// Add a decorative line
|
||||
// lineWidth := min(80, physicalWidth-4)
|
||||
// line := lipgloss.NewStyle().
|
||||
// Foreground(subtle).
|
||||
// Render(strings.Repeat("─", lineWidth))
|
||||
|
||||
// Put it all together
|
||||
doc.WriteString("\n" +
|
||||
lipgloss.NewStyle().Align(lipgloss.Center).Render(titleRow))
|
||||
|
||||
// Print the result
|
||||
fmt.Println(docStyle.Render(doc.String()))
|
||||
}
|
||||
|
||||
// func asciiLogo() string {
|
||||
// return `⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⠀⠀⠀⢠⣾⣧⣤⡖⠀⠀⠀⠀⠀⠀⠀
|
||||
// ⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⠋⠀⠉⠀⢄⣸⣿⣿⣿⣿⣿⣥⡤⢶⣿⣦⣀⡀
|
||||
// ⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⡆⠀⠀⠀⣙⣛⣿⣿⣿⣿⡏⠀⠀⣀⣿⣿⣿⡟
|
||||
// ⠀⠀⠀⠀⠀⠀⠀⠀⠙⠻⠷⣦⣤⣤⣬⣽⣿⣿⣿⣿⣿⣿⣿⣟⠛⠿⠋⠀
|
||||
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠋⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⡆⠀⠀
|
||||
// ⠀⠀⠀⠀⣠⣶⣶⣶⣿⣦⡀⠘⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋⠈⢹⡏⠁⠀⠀
|
||||
// ⠀⠀⠀⢀⣿⡏⠉⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡆⠀⢀⣿⡇⠀⠀⠀
|
||||
// ⠀⠀⠀⢸⣿⠀⠀⠀⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣟⡘⣿⣿⣃⠀⠀⠀
|
||||
// ⣴⣷⣀⣸⣿⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⠹⣿⣯⣤⣾⠏⠉⠉⠉⠙⠢⠀
|
||||
// ⠈⠙⢿⣿⡟⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣄⠛⠉⢩⣷⣴⡆⠀⠀⠀⠀⠀
|
||||
// ⠀⠀⠀⠋⠀⠀⠀⠀⠀⠀⠀⠀⠈⣿⣿⣿⣿⣀⡠⠋⠈⢿⣇⠀⠀⠀⠀⠀
|
||||
// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠿⠿⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
// ⠀⠀⠀⠀⠀`
|
||||
// }
|
||||
|
||||
func asciiLogo() string {
|
||||
return `⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣴⡇⠀⠀⠀
|
||||
⠀⢸⣿⣿⣶⣦⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣶⣿⣿⣿⣿⣿⡇⠀⠀⠀
|
||||
⠀⠘⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠀⠀⠀
|
||||
⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠀⠀
|
||||
⠀⠀⠀⠘⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠏⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠉⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⡻⣿⣿⣿⠟⠋⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⣿⣿⣿⣿⣿⡌⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⢀⣠⣤⣴⣶⣶⣶⣦⣤⣤⣄⣉⡉⠛⠷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⢀⣴⣾⣿⣿⣿⣿⡿⠿⠿⠿⣿⣿⣿⣿⣿⣿⣶⣦⣤⣀⡀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀ ⠉⠉⠀⠀⠉⠉⠀⠀ ⠉ ⠉⠉⠉⠉⠉⠉⠉⠛⠛⠛⠲⠦⠄`
|
||||
}
|
||||
59
seanime-2.9.10/internal/core/watcher.go
Normal file
59
seanime-2.9.10/internal/core/watcher.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"seanime/internal/library/scanner"
|
||||
"seanime/internal/util"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// initLibraryWatcher will initialize the library watcher.
|
||||
// - Used by AutoScanner
|
||||
func (a *App) initLibraryWatcher(paths []string) {
|
||||
// Create a new watcher
|
||||
watcher, err := scanner.NewWatcher(&scanner.NewWatcherOptions{
|
||||
Logger: a.Logger,
|
||||
WSEventManager: a.WSEventManager,
|
||||
})
|
||||
if err != nil {
|
||||
a.Logger.Error().Err(err).Msg("app: Failed to initialize watcher")
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize library file watcher
|
||||
err = watcher.InitLibraryFileWatcher(&scanner.WatchLibraryFilesOptions{
|
||||
LibraryPaths: paths,
|
||||
})
|
||||
if err != nil {
|
||||
a.Logger.Error().Err(err).Msg("app: Failed to watch library files")
|
||||
return
|
||||
}
|
||||
|
||||
var dirSize uint64 = 0
|
||||
mu := sync.Mutex{}
|
||||
wg := sync.WaitGroup{}
|
||||
for _, path := range paths {
|
||||
wg.Add(1)
|
||||
go func(path string) {
|
||||
defer wg.Done()
|
||||
ds, _ := util.DirSize(path)
|
||||
mu.Lock()
|
||||
dirSize += ds
|
||||
mu.Unlock()
|
||||
}(path)
|
||||
}
|
||||
wg.Wait()
|
||||
a.TotalLibrarySize = dirSize
|
||||
|
||||
a.Logger.Info().Msgf("watcher: Library size: %s", util.Bytes(dirSize))
|
||||
|
||||
// Set the watcher
|
||||
a.Watcher = watcher
|
||||
|
||||
// Start watching
|
||||
a.Watcher.StartWatching(
|
||||
func() {
|
||||
// Notify the auto scanner when a file action occurs
|
||||
a.AutoScanner.Notify()
|
||||
})
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user