344 lines
11 KiB
Go
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
|
|
}
|