From bffbe465205a83d7cbda3894de89512d12b30149 Mon Sep 17 00:00:00 2001 From: 5rahim Date: Mon, 29 Jul 2024 16:09:13 -0400 Subject: [PATCH] feat(extensions): bulk check for updates --- codegen/generated/handlers.json | 16 +- codegen/generated/public_structs.json | 172 +++++++++++++++--- internal/extension_repo/external.go | 52 +++++- internal/extension_repo/repository.go | 64 ++----- internal/extension_repo/utils.go | 41 +++-- internal/extension_repo/yaegi.go | 36 ++++ internal/handlers/extensions.go | 13 +- internal/handlers/routes.go | 2 +- seanime-web/public/custom-tosho.json | 4 +- .../src/api/generated/endpoint.types.ts | 11 ++ seanime-web/src/api/generated/endpoints.ts | 2 +- .../src/api/generated/hooks_template.ts | 8 +- seanime-web/src/api/generated/types.ts | 14 +- seanime-web/src/api/hooks/extensions.hooks.ts | 9 +- .../extensions/_containers/extension-card.tsx | 7 +- .../extensions/_containers/extension-list.tsx | 59 ++++-- .../src/app/(main)/extensions/page.tsx | 8 +- 17 files changed, 386 insertions(+), 132 deletions(-) diff --git a/codegen/generated/handlers.json b/codegen/generated/handlers.json index d8185f734..a93f68a04 100644 --- a/codegen/generated/handlers.json +++ b/codegen/generated/handlers.json @@ -1975,7 +1975,7 @@ "HandleGetAllExtensions", "", "\t@summary returns all loaded and invalid extensions.", - "\t@route /api/v1/extensions/all [GET]", + "\t@route /api/v1/extensions/all [POST]", "\t@returns extension_repo.AllExtensions", "" ], @@ -1986,10 +1986,20 @@ "descriptions": [], "endpoint": "/api/v1/extensions/all", "methods": [ - "GET" + "POST" ], "params": [], - "bodyFields": [], + "bodyFields": [ + { + "name": "WithUpdates", + "jsonName": "withUpdates", + "goType": "bool", + "usedStructType": "", + "typescriptType": "boolean", + "required": true, + "descriptions": [] + } + ], "returns": "extension_repo.AllExtensions", "returnGoType": "extension_repo.AllExtensions", "returnTypescriptType": "ExtensionRepo_AllExtensions" diff --git a/codegen/generated/public_structs.json b/codegen/generated/public_structs.json index 6e1ec6fe9..c3fef7c61 100644 --- a/codegen/generated/public_structs.json +++ b/codegen/generated/public_structs.json @@ -26190,6 +26190,53 @@ ], "comments": [] }, + { + "filepath": "../internal/extension/bank.go", + "filename": "bank.go", + "name": "UnifiedBank", + "formattedName": "Extension_UnifiedBank", + "package": "extension", + "fields": [ + { + "name": "extensions", + "jsonName": "extensions", + "goType": "", + "typescriptType": "any", + "required": false, + "public": false, + "comments": [] + }, + { + "name": "extensionAddedCh", + "jsonName": "extensionAddedCh", + "goType": "", + "typescriptType": "any", + "required": true, + "public": false, + "comments": [] + }, + { + "name": "extensionRemovedCh", + "jsonName": "extensionRemovedCh", + "goType": "", + "typescriptType": "any", + "required": true, + "public": false, + "comments": [] + }, + { + "name": "mu", + "jsonName": "mu", + "goType": "sync.RWMutex", + "typescriptType": "RWMutex", + "usedStructName": "sync.RWMutex", + "required": false, + "public": false, + "comments": [] + } + ], + "comments": [] + }, { "filepath": "../internal/extension/bank.go", "filename": "bank.go", @@ -26267,6 +26314,7 @@ "typescriptType": "string", "declaredValues": [ "\"javascript\"", + "\"typescript\"", "\"go\"" ] }, @@ -27626,6 +27674,42 @@ ], "comments": [] }, + { + "filepath": "../internal/extension_repo/goja_anime_torrent_provider.go", + "filename": "goja_anime_torrent_provider.go", + "name": "GojaAnimeTorrentProvider", + "formattedName": "ExtensionRepo_GojaAnimeTorrentProvider", + "package": "extension_repo", + "fields": [], + "comments": [], + "embeddedStructNames": [ + "extension_repo.gojaExtensionImpl" + ] + }, + { + "filepath": "../internal/extension_repo/goja_manga_provider.go", + "filename": "goja_manga_provider.go", + "name": "GojaMangaProvider", + "formattedName": "ExtensionRepo_GojaMangaProvider", + "package": "extension_repo", + "fields": [], + "comments": [], + "embeddedStructNames": [ + "extension_repo.gojaExtensionImpl" + ] + }, + { + "filepath": "../internal/extension_repo/goja_onlinestream_provider.go", + "filename": "goja_onlinestream_provider.go", + "name": "GojaOnlinestreamProvider", + "formattedName": "ExtensionRepo_GojaOnlinestreamProvider", + "package": "extension_repo", + "fields": [], + "comments": [], + "embeddedStructNames": [ + "extension_repo.gojaExtensionImpl" + ] + }, { "filepath": "../internal/extension_repo/repository.go", "filename": "repository.go", @@ -27673,8 +27757,8 @@ "comments": [] }, { - "name": "mangaProviderExtensionBank", - "jsonName": "mangaProviderExtensionBank", + "name": "gojaExtensions", + "jsonName": "gojaExtensions", "goType": "", "typescriptType": "any", "required": false, @@ -27682,19 +27766,11 @@ "comments": [] }, { - "name": "animeTorrentProviderExtensionBank", - "jsonName": "animeTorrentProviderExtensionBank", - "goType": "", - "typescriptType": "any", - "required": false, - "public": false, - "comments": [] - }, - { - "name": "onlinestreamProviderExtensionBank", - "jsonName": "onlinestreamProviderExtensionBank", - "goType": "", - "typescriptType": "any", + "name": "extensionBank", + "jsonName": "extensionBank", + "goType": "extension.UnifiedBank", + "typescriptType": "Extension_UnifiedBank", + "usedStructName": "extension.UnifiedBank", "required": false, "public": false, "comments": [] @@ -27737,6 +27813,53 @@ "required": false, "public": true, "comments": [] + }, + { + "name": "HasUpdate", + "jsonName": "hasUpdate", + "goType": "[]UpdateData", + "typescriptType": "Array\u003cExtensionRepo_UpdateData\u003e", + "usedStructName": "extension_repo.UpdateData", + "required": false, + "public": true, + "comments": [] + } + ], + "comments": [] + }, + { + "filepath": "../internal/extension_repo/repository.go", + "filename": "repository.go", + "name": "UpdateData", + "formattedName": "ExtensionRepo_UpdateData", + "package": "extension_repo", + "fields": [ + { + "name": "ExtensionID", + "jsonName": "extensionID", + "goType": "string", + "typescriptType": "string", + "required": true, + "public": true, + "comments": [] + }, + { + "name": "ManifestURI", + "jsonName": "manifestURI", + "goType": "string", + "typescriptType": "string", + "required": true, + "public": true, + "comments": [] + }, + { + "name": "Version", + "jsonName": "version", + "goType": "string", + "typescriptType": "string", + "required": true, + "public": true, + "comments": [] } ], "comments": [] @@ -35425,8 +35548,9 @@ { "name": "providerExtensionBank", "jsonName": "providerExtensionBank", - "goType": "", - "typescriptType": "any", + "goType": "extension.UnifiedBank", + "typescriptType": "Extension_UnifiedBank", + "usedStructName": "extension.UnifiedBank", "required": false, "public": false, "comments": [] @@ -40234,8 +40358,9 @@ { "name": "providerExtensionBank", "jsonName": "providerExtensionBank", - "goType": "", - "typescriptType": "any", + "goType": "extension.UnifiedBank", + "typescriptType": "Extension_UnifiedBank", + "usedStructName": "extension.UnifiedBank", "required": false, "public": false, "comments": [] @@ -46630,10 +46755,11 @@ "comments": [] }, { - "name": "animeProviderExtensionBank", - "jsonName": "animeProviderExtensionBank", - "goType": "", - "typescriptType": "any", + "name": "extensionBank", + "jsonName": "extensionBank", + "goType": "extension.UnifiedBank", + "typescriptType": "Extension_UnifiedBank", + "usedStructName": "extension.UnifiedBank", "required": false, "public": false, "comments": [] diff --git a/internal/extension_repo/external.go b/internal/extension_repo/external.go index da24b8f20..c189713af 100644 --- a/internal/extension_repo/external.go +++ b/internal/extension_repo/external.go @@ -12,6 +12,7 @@ import ( "seanime/internal/events" "seanime/internal/extension" "seanime/internal/util" + "sync" "time" ) @@ -149,17 +150,50 @@ func (r *Repository) UninstallExternalExtension(id string) error { return nil } -// CheckForUpdates checks all extensions for updates by querying their respective repositories. +// 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) { +func (r *Repository) checkForUpdates() (ret []UpdateData) { - //wg := sync.WaitGroup{} - // - //// Create a channel to receive the update data - //updateDataChan := make(chan UpdateData, 1) - //mu := sync.Mutex{} - // - //// Check for updates for all extensions + wg := sync.WaitGroup{} + mu := sync.Mutex{} + + // 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() + + if ext.GetManifestURI() == "builtin" || ext.GetManifestURI() == "" { + return + } + // Check for updates + extFromRepo, err := r.fetchExternalExtensionData(ext.GetManifestURI()) + 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 + } + + 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 there's an update, send the update data to the channel + if extFromRepo.Version != ext.GetVersion() { + updateData := UpdateData{ + ExtensionID: extFromRepo.ID, + Version: extFromRepo.Version, + ManifestURI: extFromRepo.ManifestURI, + } + mu.Lock() + ret = append(ret, updateData) + mu.Unlock() + } + }(ext) + return true + }) + + wg.Wait() return } diff --git a/internal/extension_repo/repository.go b/internal/extension_repo/repository.go index 239068073..e3fb32cd3 100644 --- a/internal/extension_repo/repository.go +++ b/internal/extension_repo/repository.go @@ -1,20 +1,17 @@ package extension_repo import ( + hibikemanga "github.com/5rahim/hibike/pkg/extension/manga" + hibikeonlinestream "github.com/5rahim/hibike/pkg/extension/onlinestream" + hibiketorrent "github.com/5rahim/hibike/pkg/extension/torrent" "github.com/rs/zerolog" "github.com/samber/lo" "github.com/traefik/yaegi/interp" - "github.com/traefik/yaegi/stdlib" "os" "seanime/internal/events" "seanime/internal/extension" "seanime/internal/extension/vendoring/torrent" "seanime/internal/util/result" - "seanime/internal/yaegi_interp" - - hibikemanga "github.com/5rahim/hibike/pkg/extension/manga" - hibikeonlinestream "github.com/5rahim/hibike/pkg/extension/onlinestream" - hibiketorrent "github.com/5rahim/hibike/pkg/extension/torrent" ) type ( @@ -84,14 +81,11 @@ func NewRepository(opts *NewRepositoryOptions) *Repository { _ = os.MkdirAll(opts.ExtensionDir, os.ModePerm) ret := &Repository{ - logger: opts.Logger, - extensionDir: opts.ExtensionDir, - wsEventManager: opts.WSEventManager, - gojaExtensions: result.NewResultMap[string, GojaExtension](), - extensionBank: extension.NewUnifiedBank(), - //mangaProviderExtensionBank: extension.NewBank[extension.MangaProviderExtension](), - //animeTorrentProviderExtensionBank: extension.NewBank[extension.AnimeTorrentProviderExtension](), - //onlinestreamProviderExtensionBank: extension.NewBank[extension.OnlinestreamProviderExtension](), + logger: opts.Logger, + extensionDir: opts.ExtensionDir, + wsEventManager: opts.WSEventManager, + gojaExtensions: result.NewResultMap[string, GojaExtension](), + extensionBank: extension.NewUnifiedBank(), invalidExtensions: result.NewResultMap[string, *extension.InvalidExtension](), } @@ -100,12 +94,14 @@ func NewRepository(opts *NewRepositoryOptions) *Repository { return ret } -func (r *Repository) GetAllExtensions() (ret *AllExtensions) { +func (r *Repository) GetAllExtensions(withUpdates bool) (ret *AllExtensions) { ret = &AllExtensions{ Extensions: r.ListExtensionData(), InvalidExtensions: r.ListInvalidExtensions(), } - + if withUpdates { + ret.HasUpdate = r.checkForUpdates() + } return } @@ -128,42 +124,6 @@ func (r *Repository) ListInvalidExtensions() (ret []*extension.InvalidExtension) return ret } -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -func (r *Repository) loadYaegiInterpreter() { - i := interp.New(interp.Options{ - Unrestricted: false, - }) - - symbols := stdlib.Symbols - // Remove symbols from stdlib that are risky to give to extensions - delete(symbols, "os/os") - delete(symbols, "io/fs/fs") - delete(symbols, "os/exec/exec") - delete(symbols, "os/signal/signal") - delete(symbols, "os/user/user") - delete(symbols, "os/signal/signal") - delete(symbols, "io/ioutil/ioutil") - delete(symbols, "runtime/runtime") - delete(symbols, "syscall/syscall") - delete(symbols, "archive/tar/tar") - delete(symbols, "archive/zip/zip") - delete(symbols, "compress/gzip/gzip") - delete(symbols, "compress/zlib/zlib") - - if err := i.Use(symbols); err != nil { - r.logger.Fatal().Err(err).Msg("extensions: Failed to load yaegi stdlib") - } - - // Load the extension symbols - err := i.Use(yaegi_interp.Symbols) - if err != nil { - r.logger.Fatal().Err(err).Msg("extensions: Failed to load extension symbols") - } - - r.yaegiInterp = i -} - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Lists // - Lists are used to display available options to the user based on the extensions installed diff --git a/internal/extension_repo/utils.go b/internal/extension_repo/utils.go index c60a86c17..84097b9e5 100644 --- a/internal/extension_repo/utils.go +++ b/internal/extension_repo/utils.go @@ -8,14 +8,14 @@ import ( "seanime/internal/util" ) -// extensionSanityCheck checks if the extension has all the required fields in the manifest. -func (r *Repository) extensionSanityCheck(ext *extension.Extension) error { +func manifestSanityCheck(ext *extension.Extension) error { if ext.ID == "" || ext.Name == "" || ext.Version == "" || ext.Language == "" || ext.Type == "" || ext.Author == "" || ext.Payload == "" { - return fmt.Errorf("extension is missing required fields") + return fmt.Errorf("extension is missing required fields, ID: %v, Name: %v, Version: %v, Language: %v, Type: %v, Author: %v, Payload: %v", + ext.ID, ext.Name, ext.Version, ext.Language, ext.Type, ext.Author, len(ext.Payload)) } // Check the ID - if err := r.isValidExtensionID(ext.ID); err != nil { + if err := isValidExtensionID(ext.ID); err != nil { return err } @@ -50,10 +50,25 @@ func (r *Repository) extensionSanityCheck(ext *extension.Extension) error { return nil } +// extensionSanityCheck checks if the extension has all the required fields in the manifest. +func (r *Repository) extensionSanityCheck(ext *extension.Extension) error { + + if err := manifestSanityCheck(ext); err != nil { + return err + } + + // Check that the ID is unique + if err := r.isUniqueExtensionID(ext.ID); err != nil { + return err + } + + return nil +} + // checks if the extension ID is valid // Note: The ID must start with a letter and contain only alphanumeric characters // because it can either be used as a package name or appear in a filename -func (r *Repository) isValidExtensionID(id string) error { +func isValidExtensionID(id string) error { if id == "" { return errors.New("extension ID is empty") } @@ -63,16 +78,11 @@ func (r *Repository) isValidExtensionID(id string) error { if len(id) < 3 { return errors.New("extension ID is too short") } + if !isValidExtensionIDString(id) { return errors.New("extension ID contains invalid characters") } - // Check if the ID is not a reserved built-in extension ID - _, found := r.extensionBank.Get(id) - if found { - return errors.New("extension ID is already in use") - } - return nil } @@ -86,3 +96,12 @@ func isValidExtensionIDString(id string) bool { } return true } + +func (r *Repository) isUniqueExtensionID(id string) error { + // Check if the ID is not a reserved built-in extension ID + _, found := r.extensionBank.Get(id) + if found { + return errors.New("extension ID is already in use") + } + return nil +} diff --git a/internal/extension_repo/yaegi.go b/internal/extension_repo/yaegi.go index b010fdf7b..d28e743da 100644 --- a/internal/extension_repo/yaegi.go +++ b/internal/extension_repo/yaegi.go @@ -3,7 +3,9 @@ package extension_repo import ( "context" "github.com/traefik/yaegi/interp" + "github.com/traefik/yaegi/stdlib" "reflect" + "seanime/internal/yaegi_interp" "time" ) @@ -12,6 +14,40 @@ const ( MsgYaegiFailedToInstantiateExtension = "extensions: Failed to instantiate extension, the extension is incompatible with the expected interface" ) +func (r *Repository) loadYaegiInterpreter() { + i := interp.New(interp.Options{ + Unrestricted: false, + }) + + symbols := stdlib.Symbols + // Remove symbols from stdlib that are risky to give to extensions + delete(symbols, "os/os") + delete(symbols, "io/fs/fs") + delete(symbols, "os/exec/exec") + delete(symbols, "os/signal/signal") + delete(symbols, "os/user/user") + delete(symbols, "os/signal/signal") + delete(symbols, "io/ioutil/ioutil") + delete(symbols, "runtime/runtime") + delete(symbols, "syscall/syscall") + delete(symbols, "archive/tar/tar") + delete(symbols, "archive/zip/zip") + delete(symbols, "compress/gzip/gzip") + delete(symbols, "compress/zlib/zlib") + + if err := i.Use(symbols); err != nil { + r.logger.Fatal().Err(err).Msg("extensions: Failed to load yaegi stdlib") + } + + // Load the extension symbols + err := i.Use(yaegi_interp.Symbols) + if err != nil { + r.logger.Fatal().Err(err).Msg("extensions: Failed to load extension symbols") + } + + r.yaegiInterp = i +} + func yaegiEval(i *interp.Interpreter, src string) (reflect.Value, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() diff --git a/internal/handlers/extensions.go b/internal/handlers/extensions.go index 9ec16188f..e82106429 100644 --- a/internal/handlers/extensions.go +++ b/internal/handlers/extensions.go @@ -92,10 +92,19 @@ func HandleListExtensionData(c *RouteCtx) error { // HandleGetAllExtensions // // @summary returns all loaded and invalid extensions. -// @route /api/v1/extensions/all [GET] +// @route /api/v1/extensions/all [POST] // @returns extension_repo.AllExtensions func HandleGetAllExtensions(c *RouteCtx) error { - extensions := c.App.ExtensionRepository.GetAllExtensions() + type body struct { + WithUpdates bool `json:"withUpdates"` + } + + var b body + if err := c.Fiber.BodyParser(&b); err != nil { + return c.RespondWithError(err) + } + + extensions := c.App.ExtensionRepository.GetAllExtensions(b.WithUpdates) return c.RespondWithData(extensions) } diff --git a/internal/handlers/routes.go b/internal/handlers/routes.go index 6705b1645..716117209 100644 --- a/internal/handlers/routes.go +++ b/internal/handlers/routes.go @@ -352,7 +352,7 @@ func InitRoutes(app *core.App, fiberApp *fiber.App) { v1Extensions.Post("/external/uninstall", makeHandler(app, HandleUninstallExternalExtension)) v1Extensions.Post("/external/reload", makeHandler(app, HandleReloadExternalExtensions)) - v1Extensions.Get("/all", makeHandler(app, HandleGetAllExtensions)) + v1Extensions.Post("/all", makeHandler(app, HandleGetAllExtensions)) v1Extensions.Get("/list", makeHandler(app, HandleListExtensionData)) v1Extensions.Get("/list/manga-provider", makeHandler(app, HandleListMangaProviderExtensions)) v1Extensions.Get("/list/onlinestream-provider", makeHandler(app, HandleListOnlinestreamProviderExtensions)) diff --git a/seanime-web/public/custom-tosho.json b/seanime-web/public/custom-tosho.json index eda9a7c53..b8fe364b5 100644 --- a/seanime-web/public/custom-tosho.json +++ b/seanime-web/public/custom-tosho.json @@ -1,8 +1,8 @@ { - "id": "animetosho-custom", + "id": "custom-tosho", "name": "AnimeTosho (Custom)", "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation.", - "version": "0.2.0", + "version": "0.3.0", "type": "anime-torrent-provider", "manifestURI": "http://127.0.0.1:43210/custom-tosho.json", "language": "typescript", diff --git a/seanime-web/src/api/generated/endpoint.types.ts b/seanime-web/src/api/generated/endpoint.types.ts index f97568564..76901f7ee 100644 --- a/seanime-web/src/api/generated/endpoint.types.ts +++ b/seanime-web/src/api/generated/endpoint.types.ts @@ -445,6 +445,17 @@ export type UninstallExternalExtension_Variables = { id: string } +/** + * - Filepath: internal/handlers/extensions.go + * - Filename: extensions.go + * - Endpoint: /api/v1/extensions/all + * @description + * Route returns all loaded and invalid extensions. + */ +export type GetAllExtensions_Variables = { + withUpdates: boolean +} + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // filecache ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/seanime-web/src/api/generated/endpoints.ts b/seanime-web/src/api/generated/endpoints.ts index 8acd039c0..c0c9f6270 100644 --- a/seanime-web/src/api/generated/endpoints.ts +++ b/seanime-web/src/api/generated/endpoints.ts @@ -432,7 +432,7 @@ export const API_ENDPOINTS = { }, GetAllExtensions: { key: "EXTENSIONS-get-all-extensions", - methods: ["GET"], + methods: ["POST"], endpoint: "/api/v1/extensions/all", }, ListMangaProviderExtensions: { diff --git a/seanime-web/src/api/generated/hooks_template.ts b/seanime-web/src/api/generated/hooks_template.ts index 7e26b380c..025b4dacc 100644 --- a/seanime-web/src/api/generated/hooks_template.ts +++ b/seanime-web/src/api/generated/hooks_template.ts @@ -510,11 +510,13 @@ // } // export function useGetAllExtensions() { -// return useServerQuery({ +// return useServerMutation({ // endpoint: API_ENDPOINTS.EXTENSIONS.GetAllExtensions.endpoint, // method: API_ENDPOINTS.EXTENSIONS.GetAllExtensions.methods[0], -// queryKey: [API_ENDPOINTS.EXTENSIONS.GetAllExtensions.key], -// enabled: true, +// mutationKey: [API_ENDPOINTS.EXTENSIONS.GetAllExtensions.key], +// onSuccess: async () => { +// +// }, // }) // } diff --git a/seanime-web/src/api/generated/types.ts b/seanime-web/src/api/generated/types.ts index f9efa1db3..69be74ed4 100644 --- a/seanime-web/src/api/generated/types.ts +++ b/seanime-web/src/api/generated/types.ts @@ -1629,7 +1629,7 @@ export type Extension_InvalidExtensionErrorCode = "invalid_manifest" | "invalid_ * - Filename: extension.go * - Package: extension */ -export type Extension_Language = "javascript" | "go" +export type Extension_Language = "javascript" | "typescript" | "go" /** * - Filepath: internal/extension/extension.go @@ -1660,6 +1660,7 @@ export type Extension_Type = "anime-torrent-provider" | "manga-provider" | "onli export type ExtensionRepo_AllExtensions = { extensions?: Array invalidExtensions?: Array + hasUpdate?: Array } /** @@ -1703,6 +1704,17 @@ export type ExtensionRepo_OnlinestreamProviderExtensionItem = { episodeServers?: Array } +/** + * - Filepath: internal/extension_repo/repository.go + * - Filename: repository.go + * - Package: extension_repo + */ +export type ExtensionRepo_UpdateData = { + extensionID: string + manifestURI: string + version: string +} + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Handlers ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/seanime-web/src/api/hooks/extensions.hooks.ts b/seanime-web/src/api/hooks/extensions.hooks.ts index 63550595b..b0c9e9e26 100644 --- a/seanime-web/src/api/hooks/extensions.hooks.ts +++ b/seanime-web/src/api/hooks/extensions.hooks.ts @@ -1,6 +1,7 @@ import { useServerMutation, useServerQuery } from "@/api/client/requests" import { FetchExternalExtensionData_Variables, + GetAllExtensions_Variables, InstallExternalExtension_Variables, UninstallExternalExtension_Variables, } from "@/api/generated/endpoint.types" @@ -16,11 +17,15 @@ import { } from "@/api/generated/types" import { toast } from "sonner" -export function useGetAllExtensions() { - return useServerQuery({ +export function useGetAllExtensions(withUpdates: boolean) { + return useServerQuery({ endpoint: API_ENDPOINTS.EXTENSIONS.GetAllExtensions.endpoint, method: API_ENDPOINTS.EXTENSIONS.GetAllExtensions.methods[0], queryKey: [API_ENDPOINTS.EXTENSIONS.GetAllExtensions.key], + data: { + withUpdates: withUpdates, + }, + gcTime: 0, enabled: true, }) } diff --git a/seanime-web/src/app/(main)/extensions/_containers/extension-card.tsx b/seanime-web/src/app/(main)/extensions/_containers/extension-card.tsx index be5187eea..3be03d88f 100644 --- a/seanime-web/src/app/(main)/extensions/_containers/extension-card.tsx +++ b/seanime-web/src/app/(main)/extensions/_containers/extension-card.tsx @@ -19,12 +19,14 @@ import { toast } from "sonner" type ExtensionCardProps = { extension: Extension_Extension + hasUpdate?: boolean } export function ExtensionCard(props: ExtensionCardProps) { const { extension, + hasUpdate, ...rest } = props @@ -91,7 +93,7 @@ export function ExtensionCard(props: ExtensionCardProps) { -
+
{!!extension.version && {extension.version} } @@ -101,6 +103,9 @@ export function ExtensionCard(props: ExtensionCardProps) { {capitalize(extension.language)} + {hasUpdate && + Update available + }
diff --git a/seanime-web/src/app/(main)/extensions/_containers/extension-list.tsx b/seanime-web/src/app/(main)/extensions/_containers/extension-list.tsx index d1714576d..b76dfbfa7 100644 --- a/seanime-web/src/app/(main)/extensions/_containers/extension-list.tsx +++ b/seanime-web/src/app/(main)/extensions/_containers/extension-list.tsx @@ -1,4 +1,5 @@ -import { Extension_Extension, ExtensionRepo_AllExtensions } from "@/api/generated/types" +import { Extension_Extension } from "@/api/generated/types" +import { useGetAllExtensions } from "@/api/hooks/extensions.hooks" import { AddExtensionModal } from "@/app/(main)/extensions/_containers/add-extension-modal" import { ExtensionCard } from "@/app/(main)/extensions/_containers/extension-card" import { InvalidExtensionCard } from "@/app/(main)/extensions/_containers/invalid-extension-card" @@ -12,22 +13,24 @@ import { CgMediaPodcast } from "react-icons/cg" import { GrInstallOption } from "react-icons/gr" import { PiBookFill } from "react-icons/pi" import { RiFolderDownloadFill } from "react-icons/ri" +import { TbReload } from "react-icons/tb" type ExtensionListProps = { children?: React.ReactNode - allExtensions?: ExtensionRepo_AllExtensions - isLoading: boolean } export function ExtensionList(props: ExtensionListProps) { const { children, - allExtensions, - isLoading, ...rest } = props + const [checkForUpdates, setCheckForUpdates] = React.useState(false) + + const { data: allExtensions, isPending: isLoading, refetch } = useGetAllExtensions(checkForUpdates) + + function orderExtensions(extensions: Extension_Extension[] | undefined) { return extensions?.sort((a, b) => b.name.localeCompare(a.name))?.sort((a, b) => { if (a.manifestURI === "builtin") return -1 @@ -43,41 +46,69 @@ export function ExtensionList(props: ExtensionListProps) { return ( -
+

Extensions

- +
- + + + +

Torrent providers

{orderExtensions(allExtensions.extensions).filter(n => n.type === "anime-torrent-provider").map(extension => ( - + n.extensionID === extension.id)} + /> ))}

Manga sources

{orderExtensions(allExtensions.extensions).filter(n => n.type === "manga-provider").map(extension => ( - + n.extensionID === extension.id)} + /> ))}

Online streaming sources

{orderExtensions(allExtensions.extensions).filter(n => n.type === "onlinestream-provider").map(extension => ( - + n.extensionID === extension.id)} + /> ))}
diff --git a/seanime-web/src/app/(main)/extensions/page.tsx b/seanime-web/src/app/(main)/extensions/page.tsx index 3d6781360..006ef26f0 100644 --- a/seanime-web/src/app/(main)/extensions/page.tsx +++ b/seanime-web/src/app/(main)/extensions/page.tsx @@ -1,20 +1,14 @@ "use client" -import { useGetAllExtensions } from "@/api/hooks/extensions.hooks" import { ExtensionList } from "@/app/(main)/extensions/_containers/extension-list" import { PageWrapper } from "@/components/shared/page-wrapper" import React from "react" export default function Page() { - const { data: allExtensions, isLoading } = useGetAllExtensions() - return ( - + )