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

344 lines
11 KiB
Go

package extension_repo
import (
"context"
"net/http"
"os"
"seanime/internal/events"
"seanime/internal/extension"
hibikemanga "seanime/internal/extension/hibike/manga"
hibiketorrent "seanime/internal/extension/hibike/torrent"
"seanime/internal/goja/goja_runtime"
"seanime/internal/hook"
"seanime/internal/util"
"seanime/internal/util/filecache"
"seanime/internal/util/result"
"sync"
"time"
"github.com/rs/zerolog"
"github.com/samber/lo"
)
type (
// Repository manages all extensions
Repository struct {
logger *zerolog.Logger
fileCacher *filecache.Cacher
wsEventManager events.WSEventManagerInterface
// Absolute path to the directory containing all extensions
extensionDir string
// Store all active Goja VMs
// - When reloading extensions, all VMs are interrupted
gojaExtensions *result.Map[string, GojaExtension]
gojaRuntimeManager *goja_runtime.Manager
// Extension bank
// - When reloading extensions, external extensions are removed & re-added
extensionBank *extension.UnifiedBank
invalidExtensions *result.Map[string, *extension.InvalidExtension]
hookManager hook.Manager
client *http.Client
// Cache the of all built-in extensions when they're first loaded
// This is used to quickly determine if an extension is built-in or not and to reload them
builtinExtensions *result.Map[string, *builtinExtension]
updateData []UpdateData
updateDataMu sync.Mutex
// Called when the external extensions are loaded for the first time
firstExternalExtensionLoadedFunc context.CancelFunc
}
builtinExtension struct {
extension.Extension
provider interface{}
}
AllExtensions struct {
Extensions []*extension.Extension `json:"extensions"`
InvalidExtensions []*extension.InvalidExtension `json:"invalidExtensions"`
// List of extensions with invalid user config extensions, these extensions are still loaded
InvalidUserConfigExtensions []*extension.InvalidExtension `json:"invalidUserConfigExtensions"`
// List of extension IDs that have an update available
// This is only populated when the user clicks on "Check for updates"
HasUpdate []UpdateData `json:"hasUpdate"`
}
UpdateData struct {
ExtensionID string `json:"extensionID"`
ManifestURI string `json:"manifestURI"`
Version string `json:"version"`
}
MangaProviderExtensionItem struct {
ID string `json:"id"`
Name string `json:"name"`
Lang string `json:"lang"` // ISO 639-1 language code
Settings hibikemanga.Settings `json:"settings"`
}
OnlinestreamProviderExtensionItem struct {
ID string `json:"id"`
Name string `json:"name"`
Lang string `json:"lang"` // ISO 639-1 language code
EpisodeServers []string `json:"episodeServers"`
SupportsDub bool `json:"supportsDub"`
}
AnimeTorrentProviderExtensionItem struct {
ID string `json:"id"`
Name string `json:"name"`
Lang string `json:"lang"` // ISO 639-1 language code
Settings hibiketorrent.AnimeProviderSettings `json:"settings"`
}
)
type NewRepositoryOptions struct {
Logger *zerolog.Logger
ExtensionDir string
WSEventManager events.WSEventManagerInterface
FileCacher *filecache.Cacher
HookManager hook.Manager
}
func NewRepository(opts *NewRepositoryOptions) *Repository {
// Make sure the extension directory exists
_ = os.MkdirAll(opts.ExtensionDir, os.ModePerm)
ret := &Repository{
logger: opts.Logger,
extensionDir: opts.ExtensionDir,
wsEventManager: opts.WSEventManager,
gojaExtensions: result.NewResultMap[string, GojaExtension](),
gojaRuntimeManager: goja_runtime.NewManager(opts.Logger),
extensionBank: extension.NewUnifiedBank(),
invalidExtensions: result.NewResultMap[string, *extension.InvalidExtension](),
fileCacher: opts.FileCacher,
hookManager: opts.HookManager,
client: http.DefaultClient,
builtinExtensions: result.NewResultMap[string, *builtinExtension](),
updateData: make([]UpdateData, 0),
}
firstExtensionLoadedCtx, firstExtensionLoadedCancel := context.WithCancel(context.Background())
ret.firstExternalExtensionLoadedFunc = firstExtensionLoadedCancel
// Fetch extension updates at launch and every 12 hours
go func(firstExtensionLoadedCtx context.Context) {
defer util.HandlePanicInModuleThen("extension_repo/fetchExtensionUpdates", func() {
ret.firstExternalExtensionLoadedFunc = nil
})
for {
if ret.firstExternalExtensionLoadedFunc != nil {
// Block until the first external extensions are loaded
select {
case <-firstExtensionLoadedCtx.Done():
}
}
ret.firstExternalExtensionLoadedFunc = nil
ret.updateData = ret.checkForUpdates()
if len(ret.updateData) > 0 {
// Signal the frontend that there are updates available
ret.wsEventManager.SendEvent(events.ExtensionUpdatesFound, ret.updateData)
}
time.Sleep(12 * time.Hour)
}
}(firstExtensionLoadedCtx)
return ret
}
func (r *Repository) GetAllExtensions(withUpdates bool) (ret *AllExtensions) {
invalidExtensions := r.ListInvalidExtensions()
fatalInvalidExtensions := lo.Filter(invalidExtensions, func(ext *extension.InvalidExtension, _ int) bool {
return ext.Code != extension.InvalidExtensionUserConfigError
})
userConfigInvalidExtensions := lo.Filter(invalidExtensions, func(ext *extension.InvalidExtension, _ int) bool {
return ext.Code == extension.InvalidExtensionUserConfigError
})
ret = &AllExtensions{
Extensions: r.ListExtensionData(),
InvalidExtensions: fatalInvalidExtensions,
InvalidUserConfigExtensions: userConfigInvalidExtensions,
}
// Send the update data to the frontend if there are any updates
if len(r.updateData) > 0 {
ret.HasUpdate = r.updateData
}
if withUpdates {
ret.HasUpdate = r.checkForUpdates()
r.updateData = ret.HasUpdate
}
return
}
func (r *Repository) GetUpdateData() (ret []UpdateData) {
return r.updateData
}
func (r *Repository) ListExtensionData() (ret []*extension.Extension) {
r.extensionBank.Range(func(key string, ext extension.BaseExtension) bool {
retExt := extension.ToExtensionData(ext)
retExt.Payload = ""
ret = append(ret, retExt)
return true
})
return ret
}
func (r *Repository) ListDevelopmentModeExtensions() (ret []*extension.Extension) {
r.extensionBank.Range(func(key string, ext extension.BaseExtension) bool {
if ext.GetIsDevelopment() {
retExt := extension.ToExtensionData(ext)
retExt.Payload = ""
ret = append(ret, retExt)
}
return true
})
return ret
}
func (r *Repository) ListInvalidExtensions() (ret []*extension.InvalidExtension) {
r.invalidExtensions.Range(func(key string, ext *extension.InvalidExtension) bool {
ext.Extension.Payload = ""
ret = append(ret, ext)
return true
})
return ret
}
func (r *Repository) GetExtensionPayload(id string) (ret string) {
ext, found := r.extensionBank.Get(id)
if !found {
return ""
}
return ext.GetPayload()
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Lists
// - Lists are used to display available options to the user based on the extensions installed
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (r *Repository) ListMangaProviderExtensions() []*MangaProviderExtensionItem {
ret := make([]*MangaProviderExtensionItem, 0)
extension.RangeExtensions(r.extensionBank, func(key string, ext extension.MangaProviderExtension) bool {
settings := ext.GetProvider().GetSettings()
ret = append(ret, &MangaProviderExtensionItem{
ID: ext.GetID(),
Name: ext.GetName(),
Lang: extension.GetExtensionLang(ext.GetLang()),
Settings: settings,
})
return true
})
return ret
}
func (r *Repository) ListOnlinestreamProviderExtensions() []*OnlinestreamProviderExtensionItem {
ret := make([]*OnlinestreamProviderExtensionItem, 0)
extension.RangeExtensions(r.extensionBank, func(key string, ext extension.OnlinestreamProviderExtension) bool {
settings := ext.GetProvider().GetSettings()
ret = append(ret, &OnlinestreamProviderExtensionItem{
ID: ext.GetID(),
Name: ext.GetName(),
Lang: extension.GetExtensionLang(ext.GetLang()),
EpisodeServers: settings.EpisodeServers,
SupportsDub: settings.SupportsDub,
})
return true
})
return ret
}
func (r *Repository) ListAnimeTorrentProviderExtensions() []*AnimeTorrentProviderExtensionItem {
ret := make([]*AnimeTorrentProviderExtensionItem, 0)
extension.RangeExtensions(r.extensionBank, func(key string, ext extension.AnimeTorrentProviderExtension) bool {
settings := ext.GetProvider().GetSettings()
ret = append(ret, &AnimeTorrentProviderExtensionItem{
ID: ext.GetID(),
Name: ext.GetName(),
Lang: extension.GetExtensionLang(ext.GetLang()),
Settings: hibiketorrent.AnimeProviderSettings{
Type: settings.Type,
CanSmartSearch: settings.CanSmartSearch,
SupportsAdult: settings.SupportsAdult,
SmartSearchFilters: lo.Map(settings.SmartSearchFilters, func(value hibiketorrent.AnimeProviderSmartSearchFilter, _ int) hibiketorrent.AnimeProviderSmartSearchFilter {
return value
}),
},
})
return true
})
return ret
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// GetLoadedExtension returns the loaded extension by ID.
// It returns an extension.BaseExtension interface, so it can be used to get the extension's details.
func (r *Repository) GetLoadedExtension(id string) (extension.BaseExtension, bool) {
var ext extension.BaseExtension
ext, found := r.extensionBank.Get(id)
if found {
return ext, true
}
return nil, false
}
func (r *Repository) GetExtensionBank() *extension.UnifiedBank {
return r.extensionBank
}
func (r *Repository) GetMangaProviderExtensionByID(id string) (extension.MangaProviderExtension, bool) {
ext, found := extension.GetExtension[extension.MangaProviderExtension](r.extensionBank, id)
return ext, found
}
func (r *Repository) GetOnlinestreamProviderExtensionByID(id string) (extension.OnlinestreamProviderExtension, bool) {
ext, found := extension.GetExtension[extension.OnlinestreamProviderExtension](r.extensionBank, id)
return ext, found
}
func (r *Repository) GetAnimeTorrentProviderExtensionByID(id string) (extension.AnimeTorrentProviderExtension, bool) {
ext, found := extension.GetExtension[extension.AnimeTorrentProviderExtension](r.extensionBank, id)
return ext, found
}
func (r *Repository) loadPlugin(ext *extension.Extension) (err error) {
defer util.HandlePanicInModuleWithError("extension_repo/loadPlugin", &err)
err = r.loadPluginExtension(ext)
if err != nil {
r.logger.Error().Err(err).Str("id", ext.ID).Msg("extensions: Failed to load plugin")
return err
}
r.logger.Debug().Str("id", ext.ID).Msg("extensions: Loaded plugin")
return
}