diff --git a/internal/core/config.go b/internal/core/config.go index d9d301aad..a1aef3055 100644 --- a/internal/core/config.go +++ b/internal/core/config.go @@ -5,9 +5,11 @@ import ( "fmt" "os" "path/filepath" + "strconv" + "strings" + "seanime/internal/constants" "seanime/internal/util" - "strconv" "github.com/rs/zerolog" "github.com/spf13/viper" @@ -18,6 +20,7 @@ type Config struct { Server struct { Host string Port int + BaseURL string Offline bool UseBinaryPath bool // Makes $SEANIME_WORKING_DIR point to the binary's directory Systray bool @@ -90,6 +93,7 @@ func NewConfig(options *ConfigOptions, logger *zerolog.Logger) (*Config, error) defaultHost := "127.0.0.1" defaultPort := 43211 + defaultBaseURL := "/" // Environment variables override defaults if os.Getenv("SEANIME_SERVER_HOST") != "" { @@ -102,6 +106,13 @@ func NewConfig(options *ConfigOptions, logger *zerolog.Logger) (*Config, error) return nil, fmt.Errorf("invalid SEANIME_SERVER_PORT environment variable: %s", os.Getenv("SEANIME_SERVER_PORT")) } } + if os.Getenv("SEANIME_SERVER_BASE_URL") != "" { + defaultBaseURL = NormalizeBaseURLPath(os.Getenv("SEANIME_SERVER_BASE_URL")) + } else if os.Getenv("BASE_URL") != "" { + defaultBaseURL = NormalizeBaseURLPath(os.Getenv("BASE_URL")) + } else if os.Getenv("PUBLIC_URL") != "" { + defaultBaseURL = NormalizeBaseURLPath(os.Getenv("PUBLIC_URL")) + } // Flags override environment variables if flags.Host != "" { @@ -118,7 +129,7 @@ func NewConfig(options *ConfigOptions, logger *zerolog.Logger) (*Config, error) } // Create assets directory if it doesn't exist - _ = os.MkdirAll(filepath.Join(dataDir, "assets"), 0700) + _ = os.MkdirAll(filepath.Join(dataDir, "assets"), 0o700) // Set Seanime's default custom environment variables if err = setDataDirEnv(dataDir); err != nil { @@ -134,6 +145,7 @@ func NewConfig(options *ConfigOptions, logger *zerolog.Logger) (*Config, error) viper.SetDefault("version", constants.Version) viper.SetDefault("server.host", defaultHost) viper.SetDefault("server.port", defaultPort) + viper.SetDefault("server.baseUrl", defaultBaseURL) viper.SetDefault("server.offline", false) // Use the binary's directory as the working directory environment variable on macOS viper.SetDefault("server.useBinaryPath", true) @@ -162,8 +174,10 @@ func NewConfig(options *ConfigOptions, logger *zerolog.Logger) (*Config, error) // Check if host or port have been overridden and differ from config file existingHost := viper.GetString("server.host") existingPort := viper.GetInt("server.port") + existingBaseURL := NormalizeBaseURLPath(viper.GetString("server.baseUrl")) hostChanged := false portChanged := false + baseURLChanged := false if (flags.Host != "" || os.Getenv("SEANIME_SERVER_HOST") != "") && existingHost != defaultHost { viper.Set("server.host", defaultHost) @@ -173,6 +187,10 @@ func NewConfig(options *ConfigOptions, logger *zerolog.Logger) (*Config, error) viper.Set("server.port", defaultPort) portChanged = true } + if (os.Getenv("SEANIME_SERVER_BASE_URL") != "" || os.Getenv("BASE_URL") != "" || os.Getenv("PUBLIC_URL") != "") && existingBaseURL != defaultBaseURL { + viper.Set("server.baseUrl", defaultBaseURL) + baseURLChanged = true + } if flags.Password != "" { viper.Set("server.password", flags.Password) logger.Info().Msg("app: Set server password") @@ -183,16 +201,18 @@ func NewConfig(options *ConfigOptions, logger *zerolog.Logger) (*Config, error) } // Write config if host or port changed - if hostChanged || portChanged { + if hostChanged || portChanged || baseURLChanged { if err := viper.WriteConfig(); err != nil { - logger.Warn().Err(err).Msg("app: Failed to update config with new host/port") + logger.Warn().Err(err).Msg("app: Failed to update config with new host/port/base URL") } else { logger.Info(). Bool("hostChanged", hostChanged). Bool("portChanged", portChanged). + Bool("baseURLChanged", baseURLChanged). Str("host", defaultHost). Int("port", defaultPort). - Msg("app: Updated config with new host/port") + Str("baseUrl", defaultBaseURL). + Msg("app: Updated config with new host/port/base URL") } } @@ -214,6 +234,7 @@ func NewConfig(options *ConfigOptions, logger *zerolog.Logger) (*Config, error) // Expand the values, replacing environment variables expandEnvironmentValues(cfg) + cfg.Server.BaseURL = NormalizeBaseURLPath(cfg.Server.BaseURL) cfg.Data.AppDataDir = dataDir cfg.Data.WorkingDir = os.Getenv("SEANIME_WORKING_DIR") @@ -257,6 +278,24 @@ func (cfg *Config) GetServerURI(df ...string) string { return pAddr } +func (cfg *Config) GetBaseURLPath() string { + return NormalizeBaseURLPath(cfg.Server.BaseURL) +} + +func (cfg *Config) JoinBaseURLPath(path string) string { + base := cfg.GetBaseURLPath() + if path == "" || path == "/" { + return base + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + if base == "/" { + return path + } + return base + path +} + func getWorkingDir(useBinaryPath bool) (string, error) { // Get the working directory wd, err := os.Getwd() @@ -324,6 +363,9 @@ func validateConfig(cfg *Config, logger *zerolog.Logger) error { if cfg.Server.Port == 0 { return errInvalidConfigValue("server.port", "cannot be 0") } + if cfg.Server.BaseURL == "" { + return errInvalidConfigValue("server.baseUrl", "cannot be empty") + } if cfg.Database.Name == "" { return errInvalidConfigValue("database.name", "cannot be empty") } @@ -411,6 +453,7 @@ func checkIsValidPath(path string) error { 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) } @@ -457,7 +500,7 @@ func expandEnvironmentValues(cfg *Config) { func createConfigFile(configPath string) error { _, err := os.Stat(configPath) if os.IsNotExist(err) { - if err := os.MkdirAll(filepath.Dir(configPath), 0700); err != nil { + if err := os.MkdirAll(filepath.Dir(configPath), 0o700); err != nil { return err } if err := viper.WriteConfig(); err != nil { @@ -468,7 +511,6 @@ func createConfigFile(configPath string) error { } func initAppDataDir(definedDataDir string, logger *zerolog.Logger) (dataDir string, configPath string, err error) { - // User defined data directory if definedDataDir != "" { @@ -497,7 +539,7 @@ func initAppDataDir(definedDataDir string, logger *zerolog.Logger) (dataDir stri } // Create data dir if it doesn't exist - if err := os.MkdirAll(dataDir, 0700); err != nil { + if err := os.MkdirAll(dataDir, 0o700); err != nil { return "", "", err } @@ -510,6 +552,34 @@ func initAppDataDir(definedDataDir string, logger *zerolog.Logger) (dataDir stri return } +func NormalizeBaseURLPath(s string) string { + s = strings.TrimSpace(s) + if s == "" || s == "/" { + return "/" + } + + if idx := strings.Index(s, "://"); idx != -1 { + afterScheme := s[idx+3:] + parts := strings.SplitN(afterScheme, "/", 2) + if len(parts) == 2 { + s = "/" + parts[1] + } else { + s = "/" + } + } + + if !strings.HasPrefix(s, "/") { + s = "/" + s + } + + s = "/" + strings.Trim(strings.TrimPrefix(s, "/"), "/") + if s == "/" { + return "/" + } + + return strings.TrimRight(s, "/") +} + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// func loadLogo(embeddedLogo []byte, dataDir string) (err error) { @@ -521,7 +591,7 @@ func loadLogo(embeddedLogo []byte, dataDir string) (err error) { logoPath := filepath.Join(dataDir, "seanime-logo.png") if _, err = os.Stat(logoPath); os.IsNotExist(err) { - if err = os.WriteFile(logoPath, embeddedLogo, 0644); err != nil { + if err = os.WriteFile(logoPath, embeddedLogo, 0o644); err != nil { return err } } diff --git a/internal/core/echo.go b/internal/core/echo.go index 803243e35..7d886c986 100644 --- a/internal/core/echo.go +++ b/internal/core/echo.go @@ -29,30 +29,99 @@ func NewEchoApp(app *App, webFS *embed.FS) *echo.Echo { log.Fatal(err) } + basePath := app.Config.GetBaseURLPath() + patchedIndexHTML := []byte(nil) + if basePath != "/" { + if b, readErr := fs.ReadFile(distFS, "index.html"); readErr == nil { + patchedIndexHTML = []byte(patchIndexHTMLBasePath(string(b), basePath)) + } + } + if app.Config.Server.Tls.Enabled { app.Logger.Debug().Msg("app: TLS is enabled, adding security middleware") e.Use(middleware.Secure()) } + e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if basePath == "/" { + return next(c) + } + + req := c.Request() + path := req.URL.Path + if path == "" { + path = "/" + } + + trimPrefix := func(prefix string) bool { + if prefix == "" || prefix == "/" { + return false + } + + if path == prefix { + req.URL.Path = "/" + req.RequestURI = "/" + return true + } + withSlash := prefix + "/" + if strings.HasPrefix(path, withSlash) { + req.URL.Path = "/" + strings.TrimPrefix(path, withSlash) + if req.URL.RawQuery != "" { + req.RequestURI = req.URL.Path + "?" + req.URL.RawQuery + } else { + req.RequestURI = req.URL.Path + } + return true + } + + return false + } + + if trimPrefix(basePath) { + return next(c) + } + + forwardedPrefix := NormalizeBaseURLPath(req.Header.Get("X-Forwarded-Prefix")) + if forwardedPrefix != "/" { + _ = trimPrefix(forwardedPrefix) + } + + return next(c) + } + }) + if !constants.IsRspackFrontend { + if basePath != "/" && len(patchedIndexHTML) > 0 { + e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if c.Request().Method != http.MethodGet && c.Request().Method != http.MethodHead { + return next(c) + } + + if shouldServePatchedIndex(c.Request().URL.Path) { + return c.Blob(http.StatusOK, echo.MIMETextHTMLCharsetUTF8, patchedIndexHTML) + } + + return next(c) + } + }) + } + 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") { + cURL := c.Request().URL + if isReservedServerPath(cURL.RequestURI()) { return true // Continue to the next handler } - if !strings.HasSuffix(cUrl.Path, ".html") && filepath.Ext(cUrl.Path) == "" { - cUrl.Path = cUrl.Path + ".html" + if !strings.HasSuffix(cURL.Path, ".html") && filepath.Ext(cURL.Path) == "" { + cURL.Path = cURL.Path + ".html" } - if cUrl.Path == "/.html" { - cUrl.Path = "/index.html" + if cURL.Path == "/.html" { + cURL.Path = "/index.html" } return false // Continue to the filesystem handler }, @@ -60,13 +129,9 @@ func NewEchoApp(app *App, webFS *embed.FS) *echo.Echo { } else { e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - cUrl := c.Request().URL.RequestURI() + cURL := c.Request().URL.RequestURI() - if strings.HasPrefix(cUrl, "/api") || - strings.HasPrefix(cUrl, "/events") || - strings.HasPrefix(cUrl, "/assets") || - strings.HasPrefix(cUrl, "/manga-downloads") || - strings.HasPrefix(cUrl, "/offline-assets") { + if isReservedServerPath(cURL) { return next(c) } @@ -77,16 +142,28 @@ func NewEchoApp(app *App, webFS *embed.FS) *echo.Echo { } }) + if basePath != "/" && len(patchedIndexHTML) > 0 { + e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if c.Request().Method != http.MethodGet && c.Request().Method != http.MethodHead { + return next(c) + } + + if shouldServePatchedIndex(c.Request().URL.Path) { + return c.Blob(http.StatusOK, echo.MIMETextHTMLCharsetUTF8, patchedIndexHTML) + } + + return next(c) + } + }) + } + e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ Filesystem: http.FS(distFS), 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") { + cURL := c.Request().URL + if isReservedServerPath(cURL.RequestURI()) { return true } return false @@ -94,7 +171,11 @@ func NewEchoApp(app *App, webFS *embed.FS) *echo.Echo { })) } - app.Logger.Info().Msgf("app: Serving embedded web interface") + if basePath == "/" { + app.Logger.Info().Msgf("app: Serving embedded web interface") + } else { + app.Logger.Info().Msgf("app: Serving embedded web interface at base URL %s", basePath) + } // Serve web assets app.Logger.Info().Msgf("app: Web assets path: %s", app.Config.Web.AssetDir) @@ -125,6 +206,47 @@ func (j *CustomJSONSerializer) Deserialize(c echo.Context, i interface{}) error return dec.Decode(i) } +func patchIndexHTMLBasePath(indexHTML string, basePath string) string { + if basePath == "/" { + return indexHTML + } + + runtimeScript := `` + if strings.Contains(indexHTML, "") { + indexHTML = strings.Replace(indexHTML, "", "\n "+runtimeScript, 1) + } else { + indexHTML = runtimeScript + indexHTML + } + + indexHTML = strings.ReplaceAll(indexHTML, "href=\"/", "href=\""+basePath+"/") + indexHTML = strings.ReplaceAll(indexHTML, "src=\"/", "src=\""+basePath+"/") + return indexHTML +} + +func isReservedServerPath(path string) bool { + return strings.HasPrefix(path, "/api") || + strings.HasPrefix(path, "/events") || + strings.HasPrefix(path, "/assets") || + strings.HasPrefix(path, "/manga-downloads") || + strings.HasPrefix(path, "/offline-assets") +} + +func shouldServePatchedIndex(path string) bool { + if isReservedServerPath(path) { + return false + } + + if path == "" || path == "/" { + return true + } + + if strings.HasSuffix(path, ".html") { + return true + } + + return filepath.Ext(path) == "" +} + func RunEchoServer(app *App, e *echo.Echo) { serverAddr := app.Config.GetServerAddr() app.Logger.Info().Msgf("app: Server Address: %s", serverAddr) diff --git a/internal/handlers/routes.go b/internal/handlers/routes.go index 4120d1f6c..88999a265 100644 --- a/internal/handlers/routes.go +++ b/internal/handlers/routes.go @@ -20,6 +20,8 @@ type Handler struct { } func InitRoutes(app *core.App, e *echo.Echo) { + basePath := app.Config.GetBaseURLPath() + // CORS middleware e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"*"}, @@ -84,7 +86,7 @@ func InitRoutes(app *core.App, e *echo.Echo) { newCookie.Value = u newCookie.HttpOnly = false // Make the cookie accessible via JS newCookie.Expires = time.Now().Add(24 * time.Hour) - newCookie.Path = "/" + newCookie.Path = basePath newCookie.Domain = "" newCookie.SameSite = http.SameSiteDefaultMode newCookie.Secure = false @@ -149,6 +151,8 @@ func InitRoutes(app *core.App, e *echo.Echo) { // Settings v1.GET("/settings", h.HandleGetSettings) v1.PATCH("/settings", h.HandleSaveSettings) + v1.GET("/server/config", h.HandleGetServerConfig) + v1.PATCH("/server/config/base-url", h.HandleSaveServerConfigBaseURL) v1.POST("/start", h.HandleGettingStarted) v1.PATCH("/settings/auto-downloader", h.HandleSaveAutoDownloaderSettings) v1.PATCH("/settings/media-player", h.HandleSaveMediaPlayerSettings) diff --git a/internal/handlers/server_config.go b/internal/handlers/server_config.go new file mode 100644 index 000000000..36bb9115c --- /dev/null +++ b/internal/handlers/server_config.go @@ -0,0 +1,37 @@ +package handlers + +import ( + "seanime/internal/core" + + "github.com/labstack/echo/v4" + "github.com/spf13/viper" +) + +type ServerConfigResponse struct { + BaseURL string `json:"baseUrl"` +} + +func (h *Handler) HandleGetServerConfig(c echo.Context) error { + return h.RespondWithData(c, &ServerConfigResponse{BaseURL: h.App.Config.GetBaseURLPath()}) +} + +func (h *Handler) HandleSaveServerConfigBaseURL(c echo.Context) error { + type body struct { + BaseURL string `json:"baseUrl"` + } + + var b body + if err := c.Bind(&b); err != nil { + return h.RespondWithError(c, err) + } + + normalized := core.NormalizeBaseURLPath(b.BaseURL) + viper.Set("server.baseUrl", normalized) + if err := viper.WriteConfig(); err != nil { + return h.RespondWithError(c, err) + } + + h.App.Config.Server.BaseURL = normalized + + return h.RespondWithData(c, true) +} diff --git a/internal/handlers/status.go b/internal/handlers/status.go index 4b66b1f59..37dfaf8c4 100644 --- a/internal/handlers/status.go +++ b/internal/handlers/status.go @@ -48,6 +48,7 @@ type Status struct { ServerReady bool `json:"serverReady"` ServerHasPassword bool `json:"serverHasPassword"` ShowChangelogTour string `json:"showChangelogTour"` + BaseURL string `json:"baseUrl"` } var clientInfoCache = result.NewMap[string, util.ClientInfo]() @@ -110,6 +111,7 @@ func (h *Handler) NewStatus(c echo.Context) *Status { ServerHasPassword: h.App.Config.Server.Password != "", DisabledFeatures: h.App.FeatureManager.DisabledFeatures, ShowChangelogTour: h.App.ShowTour, + BaseURL: h.App.Config.GetBaseURLPath(), } if c.Get("unauthenticated") != nil && c.Get("unauthenticated").(bool) { @@ -119,6 +121,7 @@ func (h *Handler) NewStatus(c echo.Context) *Status { ServerReady: h.App.ServerReady, ServerHasPassword: h.App.Config.Server.Password != "", Settings: &models.Settings{}, + BaseURL: h.App.Config.GetBaseURLPath(), } } diff --git a/seanime-web/rsbuild.config.ts b/seanime-web/rsbuild.config.ts index 1c95933a0..10b35e3ea 100644 --- a/seanime-web/rsbuild.config.ts +++ b/seanime-web/rsbuild.config.ts @@ -12,6 +12,7 @@ const { publicVars } = loadEnv({ prefixes: ["SEA_"] }) const isElectronDesktop = process.env.SEA_PUBLIC_DESKTOP === "electron" const distPath = isElectronDesktop ? "out-denshi" : "out" +const baseURL = process.env.SEA_PUBLIC_BASE_URL || "/" export default defineConfig({ plugins: [ @@ -84,6 +85,7 @@ export default defineConfig({ output: { cleanDistPath: true, sourceMap: !!process.env.RSDOCTOR, + assetPrefix: baseURL, distPath: { root: distPath, }, diff --git a/seanime-web/src/api/client/requests.ts b/seanime-web/src/api/client/requests.ts index da33b812d..e74756b68 100644 --- a/seanime-web/src/api/client/requests.ts +++ b/seanime-web/src/api/client/requests.ts @@ -1,5 +1,6 @@ import { getServerBaseUrl } from "@/api/client/server-url" +import { getAppUrl } from "@/api/client/server-url" import { serverAuthTokenAtom } from "@/app/(main)/_atoms/server-status.atoms" import { useMutation, UseMutationOptions, useQuery, UseQueryOptions } from "@tanstack/react-query" import axios, { AxiosError, InternalAxiosRequestConfig } from "axios" @@ -152,7 +153,7 @@ export function useServerQuery( if (!muteError && props.isError) { if (props.error?.response?.data?.error === "UNAUTHENTICATED" && pathname !== "/public/auth") { setPassword(undefined) - window.location.href = "/public/auth" + window.location.href = getAppUrl("/public/auth") return } console.log("Server error", props.error) diff --git a/seanime-web/src/api/client/server-url.ts b/seanime-web/src/api/client/server-url.ts index c6d8df8c6..54bba9c86 100644 --- a/seanime-web/src/api/client/server-url.ts +++ b/seanime-web/src/api/client/server-url.ts @@ -1,4 +1,5 @@ import { __DEV_SERVER_PORT, TESTONLY__DEV_SERVER_PORT2, TESTONLY__DEV_SERVER_PORT3 } from "@/lib/server/config" +import { APP_BASE_PATH, withBasePath } from "@/lib/base-path" import { __isDesktop__ } from "@/types/constants" function devOrProd(dev: string, prod: string): string { @@ -40,3 +41,11 @@ export function getServerBaseUrl(removeProtocol: boolean = false): string { } return ret } + +export function getServerBasePath(): string { + return APP_BASE_PATH +} + +export function getAppUrl(path: string): string { + return withBasePath(path) +} diff --git a/seanime-web/src/api/hooks/local.hooks.ts b/seanime-web/src/api/hooks/local.hooks.ts index dc437033b..b2cd35f61 100644 --- a/seanime-web/src/api/hooks/local.hooks.ts +++ b/seanime-web/src/api/hooks/local.hooks.ts @@ -1,4 +1,5 @@ import { useServerMutation, useServerQuery } from "@/api/client/requests" +import { getAppUrl } from "@/api/client/server-url" import { API_ENDPOINTS } from "@/api/generated/endpoints" import { Local_QueueState, Local_TrackedMediaItem } from "@/api/generated/types" import { useQueryClient } from "@tanstack/react-query" @@ -155,10 +156,10 @@ export function useSetOfflineMode() { onSuccess: async (data) => { if (data) { toast.success("Offline mode enabled") - window.location.href = "/offline" + window.location.href = getAppUrl("/offline") } else { toast.success("Offline mode disabled") - window.location.href = "/" + window.location.href = getAppUrl("/") } }, }) diff --git a/seanime-web/src/app/(main)/_electron/electron-update-modal.tsx b/seanime-web/src/app/(main)/_electron/electron-update-modal.tsx index 8b76e1e6a..ba8066113 100644 --- a/seanime-web/src/app/(main)/_electron/electron-update-modal.tsx +++ b/seanime-web/src/app/(main)/_electron/electron-update-modal.tsx @@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button" import { Modal } from "@/components/ui/modal" import { VerticalMenu } from "@/components/ui/vertical-menu" import { logger } from "@/lib/helpers/debug" +import { withBasePath } from "@/lib/base-path" import { WSEvents } from "@/lib/server/ws-events" import { atom } from "jotai" import { useAtom } from "jotai/react" @@ -244,7 +245,7 @@ export function ElectronUpdateModal(props: UpdateModalProps) {
- logo + logo

Update installed. Restart the app. diff --git a/seanime-web/src/app/(main)/_features/getting-started/getting-started-page.tsx b/seanime-web/src/app/(main)/_features/getting-started/getting-started-page.tsx index 12b006d0d..98798461f 100644 --- a/seanime-web/src/app/(main)/_features/getting-started/getting-started-page.tsx +++ b/seanime-web/src/app/(main)/_features/getting-started/getting-started-page.tsx @@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button" import { Card, CardProps } from "@/components/ui/card" import { cn } from "@/components/ui/core/styling" import { Field, Form } from "@/components/ui/form" +import { withBasePath } from "@/lib/base-path" import { useRouter } from "@/lib/navigation" import { DEFAULT_TORRENT_PROVIDER, @@ -126,7 +127,7 @@ function StepIndicator({ currentStep, totalSteps, onStepClick }: { currentStep:

bg logo diff --git a/seanime-web/src/app/(main)/_features/navigation/offline-sidebar.tsx b/seanime-web/src/app/(main)/_features/navigation/offline-sidebar.tsx index 4fd097a23..894386b42 100644 --- a/seanime-web/src/app/(main)/_features/navigation/offline-sidebar.tsx +++ b/seanime-web/src/app/(main)/_features/navigation/offline-sidebar.tsx @@ -6,6 +6,7 @@ import { AppSidebar, useAppSidebarContext } from "@/components/ui/app-layout" import { Avatar } from "@/components/ui/avatar" import { cn } from "@/components/ui/core/styling" import { VerticalMenu } from "@/components/ui/vertical-menu" +import { withBasePath } from "@/lib/base-path" import { usePathname } from "@/lib/navigation" import { useThemeSettings } from "@/lib/theme/theme-hooks" import React from "react" @@ -70,7 +71,7 @@ export function OfflineSidebar() {
logo diff --git a/seanime-web/src/app/(main)/_features/sea-command/sea-command-search.tsx b/seanime-web/src/app/(main)/_features/sea-command/sea-command-search.tsx index a68f57346..da4f842a0 100644 --- a/seanime-web/src/app/(main)/_features/sea-command/sea-command-search.tsx +++ b/seanime-web/src/app/(main)/_features/sea-command/sea-command-search.tsx @@ -7,6 +7,7 @@ import { SeaImage } from "@/components/shared/sea-image" import { CommandGroup, CommandItem } from "@/components/ui/command" import { LoadingSpinner } from "@/components/ui/loading-spinner" import { useDebounce } from "@/hooks/use-debounce" +import { withBasePath } from "@/lib/base-path" import { useRouter } from "@/lib/navigation" import { atom } from "jotai" import { useAtom } from "jotai/react" @@ -118,7 +119,7 @@ export function SeaCommandSearch() { className="h-[10rem] w-[10rem] mx-auto flex-none rounded-[--radius-md] object-cover object-center relative overflow-hidden" > { diff --git a/seanime-web/src/app/(main)/_features/video-core/video-core-subtitles.ts b/seanime-web/src/app/(main)/_features/video-core/video-core-subtitles.ts index 27b975c25..f6775892a 100644 --- a/seanime-web/src/app/(main)/_features/video-core/video-core-subtitles.ts +++ b/seanime-web/src/app/(main)/_features/video-core/video-core-subtitles.ts @@ -6,13 +6,14 @@ import { VideoCore_VideoPlaybackInfo, VideoCore_VideoSubtitleTrack, VideoCoreSet import { logger } from "@/lib/helpers/debug" import { detectTrackLanguage } from "@/lib/helpers/language" import { getAssetUrl } from "@/lib/server/assets" +import { withBasePath } from "@/lib/base-path" import JASSUB from "jassub" import type { ASSEvent } from "jassub/dist/worker/util" import { toast } from "sonner" -const modernWasmUrl = "/jassub/jassub-worker-modern.wasm" -const wasmUrl = "/jassub/jassub-worker.wasm" -const workerUrl = "/jassub/jassub-worker.js" +const modernWasmUrl = withBasePath("/jassub/jassub-worker-modern.wasm") +const wasmUrl = withBasePath("/jassub/jassub-worker.wasm") +const workerUrl = withBasePath("/jassub/jassub-worker.js") const subtitleLog = logger("VIDEO CORE SUBTITLES") diff --git a/seanime-web/src/app/(main)/server-data-wrapper.tsx b/seanime-web/src/app/(main)/server-data-wrapper.tsx index d8919c120..72721a69a 100644 --- a/seanime-web/src/app/(main)/server-data-wrapper.tsx +++ b/seanime-web/src/app/(main)/server-data-wrapper.tsx @@ -10,6 +10,8 @@ import { Card } from "@/components/ui/card" import { defineSchema, Field, Form } from "@/components/ui/form" import { logger } from "@/lib/helpers/debug" import { usePathname, useRouter } from "@/lib/navigation" +import { getAppUrl } from "@/api/client/server-url" +import { withBasePath } from "@/lib/base-path" import { ANILIST_OAUTH_URL, ANILIST_PIN_URL } from "@/lib/server/config" import { WSEvents } from "@/lib/server/ws-events" import { __isDesktop__ } from "@/types/constants" @@ -57,7 +59,7 @@ export function ServerDataWrapper(props: ServerDataWrapperProps) { React.useEffect(() => { if (serverStatus) { if (serverStatus?.serverHasPassword && !password && pathname !== "/public/auth") { - window.location.href = "/public/auth" + window.location.href = getAppUrl("/public/auth") setAuthenticated(false) console.warn("Redirecting to auth") } else { @@ -109,7 +111,7 @@ export function ServerDataWrapper(props: ServerDataWrapperProps) { if (serverStatus?.updating) { return
- logo + logo

Seanime is currently updating. Refresh the page once the update is complete and the connection has been reestablished. @@ -131,7 +133,7 @@ export function ServerDataWrapper(props: ServerDataWrapperProps) {

- logo + logo

Welcome!

+ + {/**/} - +
) diff --git a/seanime-web/src/app/public/auth/server-auth.tsx b/seanime-web/src/app/public/auth/server-auth.tsx index 1979961e1..33c3f7589 100644 --- a/seanime-web/src/app/public/auth/server-auth.tsx +++ b/seanime-web/src/app/public/auth/server-auth.tsx @@ -1,4 +1,5 @@ import { serverAuthTokenAtom } from "@/app/(main)/_atoms/server-status.atoms" +import { getAppUrl } from "@/api/client/server-url" import { defineSchema, Field, Form } from "@/components/ui/form" import { Modal } from "@/components/ui/modal" import { useAtom } from "jotai" @@ -29,7 +30,7 @@ export function ServerAuth() { const hash = sha256(data.password) setAuthToken(hash) React.startTransition(() => { - window.location.href = "/" + window.location.href = getAppUrl("/") setLoading(false) }) }} diff --git a/seanime-web/src/app/websocket-provider.tsx b/seanime-web/src/app/websocket-provider.tsx index a944668ce..a5e40378a 100644 --- a/seanime-web/src/app/websocket-provider.tsx +++ b/seanime-web/src/app/websocket-provider.tsx @@ -5,6 +5,7 @@ import { ElectronRestartServerPrompt } from "@/app/(main)/_electron/electron-res import { __openDrawersAtom } from "@/components/ui/drawer" import { useMainTab } from "@/hooks/use-main-tab" import { logger } from "@/lib/helpers/debug" +import { withBasePath } from "@/lib/base-path" import { usePathname } from "@/lib/navigation.ts" import { useRouter } from "@/lib/navigation.ts" import { __isElectronDesktop__ } from "@/types/constants" @@ -168,7 +169,7 @@ function WebsocketManagement() { } } - const wsUrl = `${document.location.protocol == "https:" ? "wss" : "ws"}://${getServerBaseUrl(true)}/events` + const wsUrl = `${document.location.protocol == "https:" ? "wss" : "ws"}://${getServerBaseUrl(true)}${withBasePath("/events")}` const clientId = cookies["Seanime-Client-Id"] || uuidv4() try { diff --git a/seanime-web/src/components/shared/loading-overlay-with-logo.tsx b/seanime-web/src/components/shared/loading-overlay-with-logo.tsx index 31c27ceba..b73390701 100644 --- a/seanime-web/src/components/shared/loading-overlay-with-logo.tsx +++ b/seanime-web/src/components/shared/loading-overlay-with-logo.tsx @@ -4,12 +4,13 @@ import { Button } from "@/components/ui/button" import { LoadingOverlay } from "@/components/ui/loading-spinner" import { __isDesktop__ } from "@/types/constants" import { SeaImage } from "@/components/shared/sea-image" +import { withBasePath } from "@/lib/base-path" import React from "react" export function LoadingOverlayWithLogo({ refetch, title }: { refetch?: () => void, title?: string }) { return = (props) => { > & { @@ -24,7 +25,7 @@ export const SeaImage = forwardRef