673 lines
22 KiB
Go
673 lines
22 KiB
Go
package extension_repo
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"seanime/internal/constants"
|
|
"seanime/internal/events"
|
|
"seanime/internal/extension"
|
|
"seanime/internal/util"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/google/uuid"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// External extensions
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func (r *Repository) FetchExternalExtensionData(manifestURI string) (*extension.Extension, error) {
|
|
return r.fetchExternalExtensionData(manifestURI)
|
|
}
|
|
|
|
// fetchExternalExtensionData fetches the extension data from the manifest URI.
|
|
// noPayloadDownload is an optional argument to skip downloading the payload from the payload URI if it exists (e.g when checking for updates)
|
|
func (r *Repository) fetchExternalExtensionData(manifestURI string, noPayloadDownload ...bool) (*extension.Extension, error) {
|
|
|
|
// Fetch the manifest file
|
|
client := &http.Client{}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, manifestURI, nil)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("uri", manifestURI).Msg("extensions: Failed to create HTTP request")
|
|
return nil, fmt.Errorf("failed to create HTTP request, %w", err)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("uri", manifestURI).Msg("extensions: Failed to fetch extension manifest")
|
|
return nil, fmt.Errorf("failed to fetch extension manifest, %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Parse the response
|
|
var ext extension.Extension
|
|
err = json.NewDecoder(resp.Body).Decode(&ext)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("uri", manifestURI).Msg("extensions: Failed to parse extension manifest")
|
|
return nil, fmt.Errorf("failed to parse extension manifest, %w", err)
|
|
}
|
|
|
|
// Before sanity check, fetch the payload if needed
|
|
if ext.PayloadURI != "" && !lo.Contains(noPayloadDownload, true) {
|
|
r.logger.Debug().Str("id", ext.ID).Msg("extensions: Downloading payload")
|
|
payloadFromURI, err := r.downloadPayload(ext.PayloadURI)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("id", ext.ID).Msg("extensions: Failed to download payload")
|
|
return nil, fmt.Errorf("failed to download payload, %w", err)
|
|
}
|
|
if payloadFromURI == "" {
|
|
r.logger.Error().Str("id", ext.ID).Msg("extensions: Downloaded payload is empty")
|
|
return nil, fmt.Errorf("downloaded payload is empty")
|
|
}
|
|
ext.Payload = payloadFromURI
|
|
}
|
|
|
|
// Check manifest
|
|
if err = manifestSanityCheck(&ext); err != nil {
|
|
r.logger.Error().Err(err).Str("uri", manifestURI).Msg("extensions: Failed sanity check")
|
|
return nil, fmt.Errorf("failed sanity check, %w", err)
|
|
}
|
|
|
|
// Check if the extension is development mode
|
|
if ext.IsDevelopment {
|
|
r.logger.Error().Str("id", ext.ID).Msg("extensions: Development mode enabled, cannot install development mode extensions for security reasons")
|
|
return nil, fmt.Errorf("cannot install development mode extensions for security reasons")
|
|
}
|
|
|
|
return &ext, nil
|
|
}
|
|
|
|
func (r *Repository) downloadPayload(uri string) (string, error) {
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
client := &http.Client{}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create HTTP request, %w", err)
|
|
}
|
|
|
|
// Download the payload
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to download payload, %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Read the payload
|
|
payload, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read payload, %w", err)
|
|
}
|
|
|
|
return string(payload), nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Install external extension
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type ExtensionInstallResponse struct {
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func (r *Repository) InstallExternalExtension(manifestURI string) (*ExtensionInstallResponse, error) {
|
|
|
|
ext, err := r.fetchExternalExtensionData(manifestURI)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("uri", manifestURI).Msg("extensions: Failed to fetch extension data")
|
|
return nil, fmt.Errorf("failed to fetch extension data, %w", err)
|
|
}
|
|
|
|
filename := filepath.Join(r.extensionDir, ext.ID+".json")
|
|
|
|
update := false
|
|
|
|
// Check if the extension is already installed
|
|
// i.e. a file with the same ID exists
|
|
if _, err := os.Stat(filename); err == nil {
|
|
r.logger.Debug().Str("id", ext.ID).Msg("extensions: Updating extension")
|
|
// Delete the old extension
|
|
err := os.Remove(filename)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("id", ext.ID).Msg("extensions: Failed to remove old extension")
|
|
return nil, fmt.Errorf("failed to remove old extension, %w", err)
|
|
}
|
|
update = true
|
|
}
|
|
|
|
// Add the extension as a json file
|
|
file, err := os.Create(filename)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("id", ext.ID).Msg("extensions: Failed to create extension file")
|
|
return nil, fmt.Errorf("failed to create extension file, %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Write the extension to the file
|
|
enc := json.NewEncoder(file)
|
|
err = enc.Encode(ext)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("id", ext.ID).Msg("extensions: Failed to write extension to file")
|
|
return nil, fmt.Errorf("failed to write extension to file, %w", err)
|
|
}
|
|
|
|
// Reload the extensions
|
|
//r.loadExternalExtensions()
|
|
|
|
r.reloadExtension(ext.ID)
|
|
|
|
if update {
|
|
r.updateDataMu.Lock()
|
|
r.updateData = lo.Filter(r.updateData, func(item UpdateData, _ int) bool {
|
|
return item.ExtensionID != ext.ID
|
|
})
|
|
r.updateDataMu.Unlock()
|
|
return &ExtensionInstallResponse{
|
|
Message: fmt.Sprintf("Successfully updated %s", ext.Name),
|
|
}, nil
|
|
}
|
|
|
|
return &ExtensionInstallResponse{
|
|
Message: fmt.Sprintf("Successfully installed %s", ext.Name),
|
|
}, nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Uninstall external extension
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func (r *Repository) UninstallExternalExtension(id string) error {
|
|
|
|
// Check if the extension exists
|
|
// Parse the ext
|
|
ext, err := extractExtensionFromFile(filepath.Join(r.extensionDir, id+".json"))
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("filepath", filepath.Join(r.extensionDir, id+".json")).Msg("extensions: Failed to read extension file")
|
|
return fmt.Errorf("failed to read extension file, %w", err)
|
|
}
|
|
|
|
// Uninstall the extension
|
|
err = os.Remove(filepath.Join(r.extensionDir, id+".json"))
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("id", id).Msg("extensions: Failed to uninstall extension")
|
|
return fmt.Errorf("failed to uninstall extension, %w", err)
|
|
}
|
|
|
|
// Reload the extensions
|
|
//r.loadExternalExtensions()
|
|
|
|
go func() {
|
|
_ = r.deleteExtensionUserConfig(id)
|
|
|
|
// Delete the plugin data if it was a plugin
|
|
if ext.Type == extension.TypePlugin {
|
|
r.deletePluginData(id)
|
|
r.removePluginFromStoredSettings(id)
|
|
}
|
|
}()
|
|
|
|
r.reloadExtension(id)
|
|
|
|
return nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Check for updates
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// checkForUpdates checks all extensions for updates by querying their respective repositories.
|
|
// It returns a list of extension update data containing IDs and versions.
|
|
func (r *Repository) checkForUpdates() (ret []UpdateData) {
|
|
|
|
wg := sync.WaitGroup{}
|
|
mu := sync.Mutex{}
|
|
|
|
r.logger.Trace().Msg("extensions: Checking for updates")
|
|
|
|
// Check for updates for all extensions
|
|
r.extensionBank.Range(func(key string, ext extension.BaseExtension) bool {
|
|
wg.Add(1)
|
|
go func(ext extension.BaseExtension) {
|
|
defer wg.Done()
|
|
|
|
// Skip built-in extensions
|
|
if ext.GetManifestURI() == "builtin" || ext.GetManifestURI() == "" {
|
|
return
|
|
}
|
|
|
|
// Get the extension data from the repository
|
|
extFromRepo, err := r.fetchExternalExtensionData(ext.GetManifestURI(), true)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("id", ext.GetID()).Str("url", ext.GetManifestURI()).Msg("extensions: Failed to fetch extension data while checking for update")
|
|
return
|
|
}
|
|
|
|
// Sanity check, this checks for the version too
|
|
if err = manifestSanityCheck(extFromRepo); err != nil {
|
|
r.logger.Error().Err(err).Str("id", ext.GetID()).Str("url", ext.GetManifestURI()).Msg("extensions: Failed sanity check while checking for update")
|
|
return
|
|
}
|
|
|
|
if extFromRepo.ID != ext.GetID() {
|
|
r.logger.Warn().Str("id", ext.GetID()).Str("newID", extFromRepo.ID).Str("url", ext.GetManifestURI()).Msg("extensions: Extension ID changed while checking for update")
|
|
return
|
|
}
|
|
|
|
// If there's an update, send the update data to the channel
|
|
if extFromRepo.Version != ext.GetVersion() {
|
|
mu.Lock()
|
|
ret = append(ret, UpdateData{
|
|
ExtensionID: extFromRepo.ID,
|
|
Version: extFromRepo.Version,
|
|
ManifestURI: extFromRepo.ManifestURI,
|
|
})
|
|
mu.Unlock()
|
|
}
|
|
}(ext)
|
|
return true
|
|
})
|
|
|
|
wg.Wait()
|
|
|
|
r.logger.Debug().Int("haveUpdates", len(ret)).Msg("extensions: Retrieved update info")
|
|
|
|
return
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Update extension code
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// UpdateExtensionCode updates the code of an external application
|
|
func (r *Repository) UpdateExtensionCode(id string, payload string) error {
|
|
|
|
if id == "" {
|
|
r.logger.Error().Msg("extensions: ID is empty")
|
|
return fmt.Errorf("id is empty")
|
|
}
|
|
|
|
if payload == "" {
|
|
r.logger.Error().Msg("extensions: Payload is empty")
|
|
return fmt.Errorf("payload is empty")
|
|
}
|
|
|
|
// We don't check if the extension existed in "loaded" extensions since the extension might be invalid
|
|
// We check if the file exists
|
|
|
|
filename := id + ".json"
|
|
extensionFilepath := filepath.Join(r.extensionDir, filename)
|
|
|
|
if _, err := os.Stat(extensionFilepath); err != nil {
|
|
r.logger.Error().Err(err).Str("id", id).Msg("extensions: Extension not found")
|
|
return fmt.Errorf("extension not found")
|
|
}
|
|
|
|
ext, err := extractExtensionFromFile(extensionFilepath)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("id", id).Msg("extensions: Failed to read extension file")
|
|
return fmt.Errorf("failed to read extension file, %w", err)
|
|
}
|
|
|
|
// Update the payload
|
|
ext.Payload = payload
|
|
|
|
// Write the extension to the file
|
|
file, err := os.Create(extensionFilepath)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("id", id).Msg("extensions: Failed to create extension file")
|
|
return fmt.Errorf("failed to create extension file, %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
enc := json.NewEncoder(file)
|
|
err = enc.Encode(ext)
|
|
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("id", id).Msg("extensions: Failed to write extension to file")
|
|
return fmt.Errorf("failed to write extension to file, %w", err)
|
|
}
|
|
|
|
// Call reload extension to unload it
|
|
r.reloadExtension(id)
|
|
|
|
return nil
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Loading/Reloading external extensions
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func (r *Repository) ReloadExternalExtensions() {
|
|
r.loadExternalExtensions()
|
|
}
|
|
|
|
func (r *Repository) ReloadExternalExtension(id string) {
|
|
r.reloadExtension(id)
|
|
|
|
runtime.GC()
|
|
}
|
|
|
|
// interruptExternalGojaExtensionVMs kills all VMs from currently loaded external Goja extensions & clears the Goja extensions map.
|
|
func (r *Repository) interruptExternalGojaExtensionVMs() {
|
|
defer util.HandlePanicInModuleThen("extension_repo/interruptExternalGojaExtensionVMs", func() {})
|
|
|
|
r.logger.Trace().Msg("extensions: Interrupting Goja VMs")
|
|
|
|
count := 0
|
|
// Remove external extensions from the Goja extensions map
|
|
//r.gojaExtensions.Clear()
|
|
for _, key := range r.gojaExtensions.Keys() {
|
|
if gojaExt, ok := r.gojaExtensions.Get(key); ok {
|
|
if gojaExt.GetExtension().ManifestURI != "builtin" {
|
|
gojaExt.ClearInterrupt()
|
|
r.gojaExtensions.Delete(key)
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
|
|
r.logger.Debug().Int("count", count).Msg("extensions: Killed Goja VMs")
|
|
}
|
|
|
|
// unloadExternalExtensions unloads all external extensions from the extension banks.
|
|
func (r *Repository) unloadExternalExtensions() {
|
|
r.logger.Trace().Msg("extensions: Unloading external extensions")
|
|
// We also clear the invalid extensions list, assuming the extensions are reloaded
|
|
//r.invalidExtensions.Clear()
|
|
|
|
count := 0
|
|
|
|
for _, key := range r.invalidExtensions.Keys() {
|
|
if invalidExt, ok := r.invalidExtensions.Get(key); ok {
|
|
if invalidExt.Extension.ManifestURI != "builtin" {
|
|
r.invalidExtensions.Delete(key)
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
r.extensionBank.RemoveExternalExtensions()
|
|
|
|
r.logger.Debug().Int("count", count).Msg("extensions: Unloaded external extensions")
|
|
}
|
|
|
|
// loadExternalExtensions loads all external extensions from the extension directory.
|
|
// This should be called after the built-in extensions are loaded.
|
|
func (r *Repository) loadExternalExtensions() {
|
|
r.logger.Trace().Msg("extensions: Loading external extensions")
|
|
|
|
// Interrupt all Goja VMs
|
|
r.interruptExternalGojaExtensionVMs()
|
|
|
|
// Unload all external extensions
|
|
r.unloadExternalExtensions()
|
|
|
|
//
|
|
// Load external extensions
|
|
//
|
|
|
|
err := filepath.WalkDir(r.extensionDir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Msg("extensions: Failed to walk directory")
|
|
return err
|
|
}
|
|
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
// Check if the file is a .json file
|
|
// If it is, parse the json and install the extension
|
|
if filepath.Ext(path) != ".json" {
|
|
return nil
|
|
}
|
|
|
|
r.loadExternalExtension(path)
|
|
|
|
return nil
|
|
|
|
})
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Msg("extensions: Failed to load extensions")
|
|
return
|
|
}
|
|
|
|
r.logger.Debug().Msg("extensions: Loaded external extensions")
|
|
|
|
if r.firstExternalExtensionLoadedFunc != nil {
|
|
r.firstExternalExtensionLoadedFunc()
|
|
}
|
|
|
|
r.wsEventManager.SendEvent(events.ExtensionsReloaded, nil)
|
|
}
|
|
|
|
// Loads an external extension from a file path
|
|
func (r *Repository) loadExternalExtension(filePath string) {
|
|
// Parse the ext
|
|
ext, err := extractExtensionFromFile(filePath)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("filepath", filePath).Msg("extensions: Failed to read extension file")
|
|
return
|
|
}
|
|
|
|
ext.Lang = extension.GetExtensionLang(ext.Lang)
|
|
|
|
var manifestError error
|
|
|
|
// +
|
|
// | Manifest sanity check
|
|
// +
|
|
|
|
// Sanity check
|
|
if err = r.extensionSanityCheck(ext); err != nil {
|
|
r.logger.Error().Err(err).Str("filepath", filePath).Msg("extensions: Failed sanity check")
|
|
manifestError = err
|
|
}
|
|
|
|
invalidExtensionID := ext.ID
|
|
if invalidExtensionID == "" {
|
|
invalidExtensionID = uuid.NewString()
|
|
}
|
|
|
|
// If there was an error with the manifest, skip loading the extension,
|
|
// add the extension to the InvalidExtensions list and return
|
|
// The extension should be added to the InvalidExtensions list with an auto-generated ID.
|
|
if manifestError != nil {
|
|
r.invalidExtensions.Set(invalidExtensionID, &extension.InvalidExtension{
|
|
ID: invalidExtensionID,
|
|
Reason: manifestError.Error(),
|
|
Path: filePath,
|
|
Code: extension.InvalidExtensionManifestError,
|
|
Extension: *ext,
|
|
})
|
|
r.logger.Error().Err(manifestError).Str("filepath", filePath).Msg("extensions: Failed to load extension, manifest error")
|
|
return
|
|
}
|
|
|
|
if ext.SemverConstraint != "" {
|
|
c, err := semver.NewConstraint(ext.SemverConstraint)
|
|
v, _ := semver.NewVersion(constants.Version)
|
|
if err == nil {
|
|
if !c.Check(v) {
|
|
r.invalidExtensions.Set(invalidExtensionID, &extension.InvalidExtension{
|
|
ID: invalidExtensionID,
|
|
Reason: fmt.Sprintf("Incompatible with this version of Seanime (%s): %s", constants.Version, ext.SemverConstraint),
|
|
Path: filePath,
|
|
Code: extension.InvalidExtensionSemverConstraintError,
|
|
Extension: *ext,
|
|
})
|
|
r.logger.Error().Str("id", ext.ID).Msg("extensions: Failed to load extension, semver constraint error")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
var loadingErr error
|
|
|
|
// +
|
|
// | Load payload
|
|
// +
|
|
|
|
// Load the payload URI if the extension is development mode.
|
|
// The payload URI is a path to the payload file.
|
|
if ext.IsDevelopment && ext.PayloadURI != "" {
|
|
if _, err := os.Stat(ext.PayloadURI); errors.Is(err, os.ErrNotExist) {
|
|
r.logger.Error().Err(err).Str("id", ext.ID).Msg("extensions: Failed to read payload file")
|
|
return
|
|
}
|
|
payload, err := os.ReadFile(ext.PayloadURI)
|
|
if err != nil {
|
|
r.logger.Error().Err(err).Str("id", ext.ID).Msg("extensions: Failed to read payload file")
|
|
return
|
|
}
|
|
ext.Payload = string(payload)
|
|
r.logger.Debug().Str("id", ext.ID).Msg("extensions: Loaded payload from file")
|
|
}
|
|
|
|
// +
|
|
// | Check plugin permissions
|
|
// +
|
|
|
|
if ext.Type == extension.TypePlugin && !ext.IsDevelopment {
|
|
if ext.Plugin == nil { // Shouldn't happen because of sanity check, but just in case
|
|
r.logger.Error().Str("id", ext.ID).Msg("extensions: Plugin manifest is missing plugin object")
|
|
return
|
|
}
|
|
permissionErr := r.checkPluginPermissions(ext)
|
|
if permissionErr != nil {
|
|
r.invalidExtensions.Set(invalidExtensionID, &extension.InvalidExtension{
|
|
ID: invalidExtensionID,
|
|
Reason: permissionErr.Error(),
|
|
Path: filePath,
|
|
Code: extension.InvalidExtensionPluginPermissionsNotGranted,
|
|
Extension: *ext,
|
|
PluginPermissionDescription: ext.Plugin.Permissions.GetDescription(),
|
|
})
|
|
r.logger.Warn().Err(permissionErr).Str("id", ext.ID).Msg("extensions: Plugin permissions not granted. Please grant the permissions in the extension page.")
|
|
return
|
|
}
|
|
}
|
|
|
|
// +
|
|
// | Load user config
|
|
// +
|
|
|
|
// Load user config
|
|
configErr := r.loadUserConfig(ext)
|
|
|
|
// If there was an error loading the user config, we add it to the InvalidExtensions list
|
|
// BUT we still load the extension
|
|
// DEVNOTE: Failure to load the user config is not a critical error
|
|
if configErr != nil {
|
|
r.invalidExtensions.Set(invalidExtensionID, &extension.InvalidExtension{
|
|
ID: invalidExtensionID,
|
|
Reason: configErr.Error(),
|
|
Path: filePath,
|
|
Code: extension.InvalidExtensionUserConfigError,
|
|
Extension: *ext,
|
|
})
|
|
r.logger.Warn().Err(configErr).Str("id", invalidExtensionID).Msg("extensions: Failed to load user config")
|
|
}
|
|
|
|
// +
|
|
// | Load extension
|
|
// +
|
|
|
|
// Load extension
|
|
switch ext.Type {
|
|
case extension.TypeMangaProvider:
|
|
// Load manga provider
|
|
loadingErr = r.loadExternalMangaExtension(ext)
|
|
case extension.TypeOnlinestreamProvider:
|
|
// Load online streaming provider
|
|
loadingErr = r.loadExternalOnlinestreamProviderExtension(ext)
|
|
case extension.TypeAnimeTorrentProvider:
|
|
// Load torrent provider
|
|
loadingErr = r.loadExternalAnimeTorrentProviderExtension(ext)
|
|
case extension.TypePlugin:
|
|
// Load plugin
|
|
loadingErr = r.loadPlugin(ext)
|
|
default:
|
|
r.logger.Error().Str("type", string(ext.Type)).Msg("extensions: Extension type not supported")
|
|
loadingErr = fmt.Errorf("extension type not supported")
|
|
}
|
|
|
|
// If there was an error loading the extension, skip adding it to the extension bank
|
|
// and add the extension to the InvalidExtensions list
|
|
if loadingErr != nil {
|
|
r.invalidExtensions.Set(invalidExtensionID, &extension.InvalidExtension{
|
|
ID: invalidExtensionID,
|
|
Reason: loadingErr.Error(),
|
|
Path: filePath,
|
|
Code: extension.InvalidExtensionPayloadError,
|
|
Extension: *ext,
|
|
})
|
|
r.logger.Error().Err(loadingErr).Str("filepath", filePath).Msg("extensions: Failed to load extension")
|
|
return
|
|
}
|
|
|
|
r.logger.Debug().Str("id", ext.ID).Msg("extensions: Loaded external extension")
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Reload specific extension
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func (r *Repository) reloadExtension(id string) {
|
|
r.logger.Trace().Str("id", id).Msg("extensions: Reloading extension")
|
|
|
|
// 1. Unload the extension
|
|
|
|
// Remove extension from bank
|
|
r.extensionBank.Delete(id)
|
|
|
|
// Kill Goja VM if it exists
|
|
gojaExtension, ok := r.gojaExtensions.Get(id)
|
|
if ok {
|
|
// Interrupt the extension's runtime and running processed before unloading
|
|
gojaExtension.ClearInterrupt()
|
|
r.logger.Trace().Str("id", id).Msg("extensions: Killed extension's runtime")
|
|
r.gojaExtensions.Delete(id)
|
|
}
|
|
// Remove from invalid extensions
|
|
r.invalidExtensions.Delete(id)
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// 2. Load the extension back
|
|
|
|
// Load the extension from the file
|
|
extensionFilepath := filepath.Join(r.extensionDir, id+".json")
|
|
|
|
// Check if the extension still exists
|
|
if _, err := os.Stat(extensionFilepath); err != nil {
|
|
// If the extension doesn't exist anymore, return silently - it was uninstalled
|
|
r.wsEventManager.SendEvent(events.ExtensionsReloaded, nil)
|
|
r.logger.Debug().Str("id", id).Msg("extensions: Extension removed")
|
|
return
|
|
}
|
|
|
|
// If the extension still exist, load it back
|
|
r.loadExternalExtension(extensionFilepath)
|
|
|
|
r.logger.Debug().Str("id", id).Msg("extensions: Reloaded extension")
|
|
r.wsEventManager.SendEvent(events.ExtensionsReloaded, nil)
|
|
}
|