diff --git a/server/dlna/dlna.go b/server/dlna/dlna.go index 6a0599d62..fe3616e67 100644 --- a/server/dlna/dlna.go +++ b/server/dlna/dlna.go @@ -9,6 +9,7 @@ import ( "runtime" "sort" "strconv" + "strings" "time" "github.com/anacrolix/dms/dlna/dms" @@ -121,7 +122,13 @@ func onBrowse(path, rootObjectPath, host, userAgent string) (ret []interface{}, ret = getRoot() return } else if path == "/TR" { - ret = getTorrents() + ret = getTorrentCategories() + return + } else if strings.HasPrefix(path, "/TR/") { + ret = getTorrentsByCategory(path) + return + } else if path == "/FS" || strings.HasPrefix(path, "/FS/") { + ret, err = browseFS(path, host) return } else if isHashPath(path) { ret = getTorrent(path, host) @@ -133,6 +140,12 @@ func onBrowse(path, rootObjectPath, host, userAgent string) (ret []interface{}, } func onBrowseMeta(path string, rootObjectPath string, host, userAgent string) (ret interface{}, err error) { + // FS meta + if path == "/FS" || strings.HasPrefix(path, "/FS/") { + ret, err = getFSMetadata(path, host) + return + } + // Torrents meta (existing) ret = getTorrentMeta(path, host) if ret == nil { err = fmt.Errorf("meta not found") diff --git a/server/dlna/fs.go b/server/dlna/fs.go new file mode 100644 index 000000000..47fda9ae6 --- /dev/null +++ b/server/dlna/fs.go @@ -0,0 +1,281 @@ +package dlna + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/anacrolix/dms/dlna" + "github.com/anacrolix/dms/upnpav" + + "server/log" + mt "server/mimetype" + "server/settings" +) + +func localDLNARootDir() (string, error) { + if settings.BTsets == nil { + return "", fmt.Errorf("settings not initialized") + } + + if settings.BTsets.DLNALocalRoot != "" { + return settings.BTsets.DLNALocalRoot, nil + } + + // Fallback to executable directory ("where server is installed") + exe, err := os.Executable() + if err != nil { + return "", err + } + return filepath.Dir(exe), nil +} + +// dlnaPath is "/FS" or "/FS/sub/dir" +func fsRelFromDLNAPath(dlnaPath string) (string, error) { + if dlnaPath == "/FS" { + return "", nil + } + if !strings.HasPrefix(dlnaPath, "/FS/") { + return "", fmt.Errorf("not a FS path: %s", dlnaPath) + } + rel := strings.TrimPrefix(dlnaPath, "/FS/") + rel = filepath.FromSlash(rel) + rel = filepath.Clean(rel) + if rel == "." { + rel = "" + } + return rel, nil +} + +func secureJoin(root, rel string) (string, error) { + rootAbs, err := filepath.Abs(root) + if err != nil { + return "", err + } + + full := filepath.Join(rootAbs, rel) + fullAbs, err := filepath.Abs(full) + if err != nil { + return "", err + } + + if fullAbs != rootAbs { + prefix := rootAbs + string(os.PathSeparator) + if !strings.HasPrefix(fullAbs, prefix) { + return "", fmt.Errorf("path escapes root") + } + } + + return fullAbs, nil +} + +func browseFS(dlnaPath, host string) (ret []interface{}, err error) { + if settings.BTsets == nil || !settings.BTsets.EnableDLNALocal { + return nil, nil + } + + root, err := localDLNARootDir() + if err != nil { + return nil, err + } + + rel, err := fsRelFromDLNAPath(dlnaPath) + if err != nil { + return nil, err + } + + full, err := secureJoin(root, rel) + if err != nil { + return nil, err + } + + st, err := os.Stat(full) + if err != nil { + return nil, err + } + + // If a file is browsed directly, return its item. + if !st.IsDir() { + item, ok := makeItemFromLocalFile(dlnaPath, host, full, st) + if ok { + ret = append(ret, item) + } + return + } + + entries, err := os.ReadDir(full) + if err != nil { + return nil, err + } + + // Deterministic order: dirs first, then files; both alphabetical. + type wrap struct { + e os.DirEntry + name string + isDir bool + } + list := make([]wrap, 0, len(entries)) + for _, e := range entries { + name := e.Name() + if strings.HasPrefix(name, ".") { + continue + } + list = append(list, wrap{e: e, name: name, isDir: e.IsDir()}) + } + + sort.SliceStable(list, func(i, j int) bool { + if list[i].isDir != list[j].isDir { + return list[i].isDir + } + return strings.ToLower(list[i].name) < strings.ToLower(list[j].name) + }) + + currentID := url.PathEscape(dlnaPath) + + for _, w := range list { + name := w.name + + childDlnaPath := dlnaPath + if childDlnaPath == "/FS" { + childDlnaPath = "/FS/" + name + } else { + childDlnaPath = dlnaPath + "/" + name + } + + if w.isDir { + obj := upnpav.Object{ + ID: url.PathEscape(childDlnaPath), + ParentID: currentID, + Restricted: 1, + Title: name, + Class: "object.container.storageFolder", + Date: upnpav.Timestamp{Time: time.Now()}, + } + cnt := upnpav.Container{Object: obj, ChildCount: 0} + ret = append(ret, cnt) + continue + } + + info, err := w.e.Info() + if err != nil { + continue + } + + fullChild, err := secureJoin(root, filepath.Join(rel, filepath.FromSlash(name))) + if err != nil { + continue + } + + item, ok := makeItemFromLocalFile(childDlnaPath, host, fullChild, info) + if !ok { + continue + } + item.ParentID = currentID + ret = append(ret, item) + } + + return +} + +func getFSMetadata(dlnaPath, host string) (ret interface{}, err error) { + if settings.BTsets == nil || !settings.BTsets.EnableDLNALocal { + return nil, fmt.Errorf("local dlna disabled") + } + + root, err := localDLNARootDir() + if err != nil { + return nil, err + } + + rel, err := fsRelFromDLNAPath(dlnaPath) + if err != nil { + return nil, err + } + + full, err := secureJoin(root, rel) + if err != nil { + return nil, err + } + + st, err := os.Stat(full) + if err != nil { + return nil, err + } + + if st.IsDir() { + title := "Local files" + if dlnaPath != "/FS" { + title = filepath.Base(dlnaPath) + } + + obj := upnpav.Object{ + ID: url.PathEscape(dlnaPath), + ParentID: url.PathEscape(filepath.Dir(dlnaPath)), + Restricted: 1, + Searchable: 1, + Title: title, + Class: "object.container.storageFolder", + Date: upnpav.Timestamp{Time: time.Now()}, + } + meta := upnpav.Container{Object: obj, ChildCount: 0} + return meta, nil + } + + item, ok := makeItemFromLocalFile(dlnaPath, host, full, st) + if !ok { + return nil, fmt.Errorf("unsupported file type") + } + return item, nil +} + +func makeItemFromLocalFile(dlnaPath, host, fullPath string, st os.FileInfo) (item upnpav.Item, ok bool) { + mime, err := mt.MimeTypeByPath(fullPath) + if err != nil { + if settings.BTsets != nil && settings.BTsets.EnableDebug { + log.TLogln("Can't detect mime type", err) + } + return upnpav.Item{}, false + } + + // Same behavior as torrents: only media + if !mime.IsMedia() { + return upnpav.Item{}, false + } + + obj := upnpav.Object{ + ID: url.PathEscape(dlnaPath), + ParentID: url.PathEscape(filepath.Dir(dlnaPath)), + Restricted: 1, + Title: filepath.Base(fullPath), + Class: "object.item." + mime.Type() + "Item", + Date: upnpav.Timestamp{Time: time.Now()}, + } + + item = upnpav.Item{ + Object: obj, + Res: make([]upnpav.Resource, 0, 1), + } + + rel, err := fsRelFromDLNAPath(dlnaPath) + if err != nil { + return upnpav.Item{}, false + } + + // IMPORTANT: path-based endpoint; more compatible than query params for some TVs. + resourceURL := getLink(host, "dlna/fs/"+url.PathEscape(filepath.ToSlash(rel))) + + item.Res = append(item.Res, upnpav.Resource{ + URL: resourceURL, + ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mime, dlna.ContentFeatures{ + SupportRange: true, + SupportTimeSeek: true, + }.String()), + Size: uint64(st.Size()), + }) + + return item, true +} diff --git a/server/dlna/list.go b/server/dlna/list.go index 466b8dfd7..37f4ffac5 100644 --- a/server/dlna/list.go +++ b/server/dlna/list.go @@ -35,6 +35,159 @@ func getRoot() (ret []interface{}) { cnt := upnpav.Container{Object: tObj, ChildCount: vol} ret = append(ret, cnt) + // Local files Object (ROOT) + if settings.BTsets != nil && settings.BTsets.EnableDLNALocal { + fsObj := upnpav.Object{ + ID: "%2FFS", + ParentID: "0", + Restricted: 1, + Title: "Local files", + Class: "object.container.storageFolder", + Date: upnpav.Timestamp{Time: time.Now()}, + } + // ChildCount not required; avoid scanning filesystem here. + fsCnt := upnpav.Container{Object: fsObj, ChildCount: 0} + ret = append(ret, fsCnt) + } + + return +} + +func normalizeCategory(c string) string { + c = strings.TrimSpace(strings.ToLower(c)) + if c == "" { + return "uncategorized" + } + return c +} + +func categoryTitle(c string) string { + switch c { + case "movie": + return "Movies" + case "tv": + return "TV Shows" + case "music": + return "Music" + case "other": + return "Other" + case "uncategorized": + return "Uncategorized" + default: + return c + } +} + +func getTorrentCategories() (ret []interface{}) { + torrs := torr.ListTorrent() + + catCounts := make(map[string]int) + for _, t := range torrs { + cat := normalizeCategory(t.Category) + catCounts[cat]++ + } + + known := []string{"movie", "tv", "music", "other", "uncategorized"} + seen := make(map[string]struct{}) + order := make([]string, 0, len(catCounts)) + + for _, k := range known { + if _, ok := catCounts[k]; ok { + order = append(order, k) + seen[k] = struct{}{} + } + } + + var rest []string + for c := range catCounts { + if _, ok := seen[c]; ok { + continue + } + rest = append(rest, c) + } + sort.Strings(rest) + order = append(order, rest...) + + if len(order) == 0 { + obj := upnpav.Object{ + ID: "%2FNT", + ParentID: "%2FTR", + Restricted: 1, + Title: "No Torrents", + Class: "object.container.storageFolder", + Date: upnpav.Timestamp{Time: time.Now()}, + } + cnt := upnpav.Container{Object: obj, ChildCount: 0} + ret = append(ret, cnt) + return + } + + for _, cat := range order { + title := categoryTitle(cat) + id := url.PathEscape("/TR/" + cat) + + obj := upnpav.Object{ + ID: id, + ParentID: "%2FTR", + Restricted: 1, + Title: title, + Class: "object.container.storageFolder", + Date: upnpav.Timestamp{Time: time.Now()}, + } + cnt := upnpav.Container{Object: obj, ChildCount: catCounts[cat]} + ret = append(ret, cnt) + } + + return +} + +func getTorrentsByCategory(path string) (ret []interface{}) { + cat := strings.TrimPrefix(path, "/TR/") + cat, _ = url.PathUnescape(cat) + cat = normalizeCategory(cat) + + parentID := url.PathEscape("/TR/" + cat) + + torrs := torr.ListTorrent() + filtered := make([]*torr.Torrent, 0, len(torrs)) + for _, t := range torrs { + if normalizeCategory(t.Category) == cat { + filtered = append(filtered, t) + } + } + + sort.Slice(filtered, func(i, j int) bool { + return filtered[i].Title < filtered[j].Title + }) + + if len(filtered) == 0 { + obj := upnpav.Object{ + ID: "%2FNT", + ParentID: parentID, + Restricted: 1, + Title: "Empty", + Class: "object.container.storageFolder", + Date: upnpav.Timestamp{Time: time.Now()}, + } + cnt := upnpav.Container{Object: obj, ChildCount: 0} + ret = append(ret, cnt) + return + } + + for _, t := range filtered { + obj := upnpav.Object{ + ID: "%2F" + t.TorrentSpec.InfoHash.HexString(), + ParentID: parentID, + Restricted: 1, + Title: strings.ReplaceAll(t.Title, "/", "|"), + Class: "object.container.storageFolder", + Icon: t.Poster, + AlbumArtURI: t.Poster, + Date: upnpav.Timestamp{Time: time.Unix(t.Timestamp, 0)}, + } + cnt := upnpav.Container{Object: obj, ChildCount: 1} + ret = append(ret, cnt) + } return } @@ -141,6 +294,29 @@ func getTorrentMeta(path, host string) (ret interface{}) { vol := len(torrs) meta := upnpav.Container{Object: trObj, ChildCount: vol} return meta + } else if strings.HasPrefix(path, "/TR/") { + cat := strings.TrimPrefix(path, "/TR/") + cat, _ = url.PathUnescape(cat) + cat = normalizeCategory(cat) + + vol := 0 + for _, t := range torr.ListTorrent() { + if normalizeCategory(t.Category) == cat { + vol++ + } + } + + obj := upnpav.Object{ + ID: url.PathEscape(path), + ParentID: "%2FTR", + Restricted: 1, + Searchable: 1, + Title: categoryTitle(cat), + Date: upnpav.Timestamp{Time: time.Now()}, + Class: "object.container.storageFolder", + } + meta := upnpav.Container{Object: obj, ChildCount: vol} + return meta } else if isHashPath(path) { // find torrent without load torrs := torr.ListTorrent() diff --git a/server/settings/btsets.go b/server/settings/btsets.go index 3829cbf96..5d32407f2 100644 --- a/server/settings/btsets.go +++ b/server/settings/btsets.go @@ -45,6 +45,9 @@ type BTSets struct { EnableDLNA bool FriendlyName string + EnableDLNALocal bool + DLNALocalRoot string + // Rutor EnableRutorSearch bool @@ -161,6 +164,8 @@ func SetDefaultConfig() { sets.ResponsiveMode = true sets.ShowFSActiveTorr = true sets.StoreSettingsInJson = true + sets.EnableDLNALocal = false + sets.DLNALocalRoot = "" // Set default TMDB settings sets.TMDBSettings = TMDBConfig{ APIKey: "", diff --git a/server/web/api/dlna_fs.go b/server/web/api/dlna_fs.go new file mode 100644 index 000000000..e6b289252 --- /dev/null +++ b/server/web/api/dlna_fs.go @@ -0,0 +1,81 @@ +package api + +import ( + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/gin-gonic/gin" + + cfg "server/settings" +) + +func dlnaFS(c *gin.Context) { + if cfg.BTsets == nil || !cfg.BTsets.EnableDLNALocal { + c.String(http.StatusForbidden, "DLNA local is disabled") + return + } + + root := cfg.BTsets.DLNALocalRoot + if root == "" { + // Fallback to "where the server is installed" + exe, err := os.Executable() + if err != nil { + c.String(http.StatusBadRequest, "DLNA local root is not configured") + return + } + root = filepath.Dir(exe) + } + + // Gin wildcard includes leading slash: "/Movies/film.mkv" + rel := strings.TrimPrefix(c.Param("path"), "/") + + full, err := dlnaSecureJoin(root, rel) + if err != nil { + c.String(http.StatusForbidden, "Forbidden") + return + } + + st, err := os.Stat(full) + if err != nil { + c.Status(http.StatusNotFound) + return + } + if st.IsDir() { + c.String(http.StatusBadRequest, "Not a file") + return + } + + // Supports Range requests (required by many DLNA clients). + c.File(full) +} + +func dlnaSecureJoin(root, rel string) (string, error) { + rootAbs, err := filepath.Abs(root) + if err != nil { + return "", err + } + + rel = filepath.FromSlash(rel) + rel = filepath.Clean(rel) + if rel == "." { + rel = "" + } + + full := filepath.Join(rootAbs, rel) + fullAbs, err := filepath.Abs(full) + if err != nil { + return "", err + } + + // Ensure fullAbs is inside rootAbs + if fullAbs != rootAbs { + prefix := rootAbs + string(os.PathSeparator) + if !strings.HasPrefix(fullAbs, prefix) { + return "", os.ErrPermission + } + } + + return fullAbs, nil +} diff --git a/server/web/api/route.go b/server/web/api/route.go index e5b349bcb..b93249060 100644 --- a/server/web/api/route.go +++ b/server/web/api/route.go @@ -35,6 +35,9 @@ func SetupRoute(route gin.IRouter) { route.HEAD("/play/:hash/:id", play) route.GET("/play/:hash/:id", play) + route.HEAD("/dlna/fs/*path", dlnaFS) + route.GET("/dlna/fs/*path", dlnaFS) + authorized.POST("/viewed", viewed) authorized.GET("/playlistall/all.m3u", allPlayList) diff --git a/server/web/pages/template/html.go b/server/web/pages/template/html.go index 6f9be98f5..73ff14666 100644 --- a/server/web/pages/template/html.go +++ b/server/web/pages/template/html.go @@ -118,20 +118,20 @@ var Mstile150x150png []byte //go:embed pages/site.webmanifest var Sitewebmanifest []byte -//go:embed pages/static/js/2.ad0b109e.chunk.js -var Staticjs2ad0b109echunkjs []byte +//go:embed pages/static/js/2.6f6c7bde.chunk.js +var Staticjs26f6c7bdechunkjs []byte -//go:embed pages/static/js/2.ad0b109e.chunk.js.LICENSE.txt -var Staticjs2ad0b109echunkjsLICENSEtxt []byte +//go:embed pages/static/js/2.6f6c7bde.chunk.js.LICENSE.txt +var Staticjs26f6c7bdechunkjsLICENSEtxt []byte -//go:embed pages/static/js/2.ad0b109e.chunk.js.map -var Staticjs2ad0b109echunkjsmap []byte +//go:embed pages/static/js/2.6f6c7bde.chunk.js.map +var Staticjs26f6c7bdechunkjsmap []byte -//go:embed pages/static/js/main.69a66258.chunk.js -var Staticjsmain69a66258chunkjs []byte +//go:embed pages/static/js/main.a91ab135.chunk.js +var Staticjsmaina91ab135chunkjs []byte -//go:embed pages/static/js/main.69a66258.chunk.js.map -var Staticjsmain69a66258chunkjsmap []byte +//go:embed pages/static/js/main.a91ab135.chunk.js.map +var Staticjsmaina91ab135chunkjsmap []byte //go:embed pages/static/js/runtime-main.5ed86a79.js var Staticjsruntimemain5ed86a79js []byte diff --git a/server/web/pages/template/pages/asset-manifest.json b/server/web/pages/template/pages/asset-manifest.json index 794f94dc4..ca7765a45 100644 --- a/server/web/pages/template/pages/asset-manifest.json +++ b/server/web/pages/template/pages/asset-manifest.json @@ -1,17 +1,17 @@ { "files": { - "main.js": "./static/js/main.69a66258.chunk.js", - "main.js.map": "./static/js/main.69a66258.chunk.js.map", + "main.js": "./static/js/main.a91ab135.chunk.js", + "main.js.map": "./static/js/main.a91ab135.chunk.js.map", "runtime-main.js": "./static/js/runtime-main.5ed86a79.js", "runtime-main.js.map": "./static/js/runtime-main.5ed86a79.js.map", - "static/js/2.ad0b109e.chunk.js": "./static/js/2.ad0b109e.chunk.js", - "static/js/2.ad0b109e.chunk.js.map": "./static/js/2.ad0b109e.chunk.js.map", + "static/js/2.6f6c7bde.chunk.js": "./static/js/2.6f6c7bde.chunk.js", + "static/js/2.6f6c7bde.chunk.js.map": "./static/js/2.6f6c7bde.chunk.js.map", "index.html": "./index.html", - "static/js/2.ad0b109e.chunk.js.LICENSE.txt": "./static/js/2.ad0b109e.chunk.js.LICENSE.txt" + "static/js/2.6f6c7bde.chunk.js.LICENSE.txt": "./static/js/2.6f6c7bde.chunk.js.LICENSE.txt" }, "entrypoints": [ "static/js/runtime-main.5ed86a79.js", - "static/js/2.ad0b109e.chunk.js", - "static/js/main.69a66258.chunk.js" + "static/js/2.6f6c7bde.chunk.js", + "static/js/main.a91ab135.chunk.js" ] } \ No newline at end of file diff --git a/server/web/pages/template/pages/index.html b/server/web/pages/template/pages/index.html index 9ec67264a..ecd6d9b6d 100644 --- a/server/web/pages/template/pages/index.html +++ b/server/web/pages/template/pages/index.html @@ -1 +1 @@ -
0)&&n.host.split("@"))&&(n.auth=_.shift(),n.hostname=_.shift(),n.host=n.hostname);return n.search=e.search,n.query=e.query,null===n.pathname&&null===n.search||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.href=n.format(),n}if(!O.length)return n.pathname=null,n.search?n.path="/"+n.search:n.path=null,n.href=n.format(),n;for(var E=O.slice(-1)[0],k=(n.host||e.host||O.length>1)&&("."===E||".."===E)||""===E,j=0,C=O.length;C>=0;C--)"."===(E=O[C])?O.splice(C,1):".."===E?(O.splice(C,1),j++):j&&(O.splice(C,1),j--);if(!w&&!x)for(;j--;j)O.unshift("..");!w||""===O[0]||O[0]&&"/"===O[0].charAt(0)||O.unshift(""),k&&"/"!==O.join("/").substr(-1)&&O.push("");var _,R=""===O[0]||O[0]&&"/"===O[0].charAt(0);S&&(n.hostname=R?"":O.length?O.shift():"",n.host=n.hostname,(_=!!(n.host&&n.host.indexOf("@")>0)&&n.host.split("@"))&&(n.auth=_.shift(),n.hostname=_.shift(),n.host=n.hostname));return(w=w||n.host&&O.length)&&!R&&O.unshift(""),O.length>0?n.pathname=O.join("/"):(n.pathname=null,n.path=null),null===n.pathname&&null===n.search||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.auth=e.auth||n.auth,n.slashes=n.slashes||e.slashes,n.href=n.format(),n},o.prototype.parseHost=function(){var e=this.host,t=a.exec(e);t&&(":"!==(t=t[0])&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)},t.parse=y,t.resolve=function(e,t){return y(e,!1,!0).resolve(t)},t.resolveObject=function(e,t){return e?y(e,!1,!0).resolveObject(t):t},t.format=function(e){return"string"===typeof e&&(e=y(e)),e instanceof o?e.format():o.prototype.format.call(e)},t.Url=o},function(e,t,n){"use strict";var r,o=n(183),i=n(389),a=n(390),s=n(391),u=n(392),l=n(393),c=n(82),f=n(394),d=n(395),p=n(396),h=n(397),m=n(398),v=n(399),g=n(400),y=n(401),b=Function,w=function(e){try{return b('"use strict"; return ('+e+").constructor;")()}catch(t){}},x=n(184),O=n(404),S=function(){throw new c},E=x?function(){try{return S}catch(e){try{return x(arguments,"callee").get}catch(t){return S}}}():S,k=n(405)(),j=n(407),C=n(186),_=n(185),R=n(188),P=n(139),T={},A="undefined"!==typeof Uint8Array&&j?j(Uint8Array):r,L={__proto__:null,"%AggregateError%":"undefined"===typeof AggregateError?r:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"===typeof ArrayBuffer?r:ArrayBuffer,"%ArrayIteratorPrototype%":k&&j?j([][Symbol.iterator]()):r,"%AsyncFromSyncIteratorPrototype%":r,"%AsyncFunction%":T,"%AsyncGenerator%":T,"%AsyncGeneratorFunction%":T,"%AsyncIteratorPrototype%":T,"%Atomics%":"undefined"===typeof Atomics?r:Atomics,"%BigInt%":"undefined"===typeof BigInt?r:BigInt,"%BigInt64Array%":"undefined"===typeof BigInt64Array?r:BigInt64Array,"%BigUint64Array%":"undefined"===typeof BigUint64Array?r:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"===typeof DataView?r:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":i,"%eval%":eval,"%EvalError%":a,"%Float32Array%":"undefined"===typeof Float32Array?r:Float32Array,"%Float64Array%":"undefined"===typeof Float64Array?r:Float64Array,"%FinalizationRegistry%":"undefined"===typeof FinalizationRegistry?r:FinalizationRegistry,"%Function%":b,"%GeneratorFunction%":T,"%Int8Array%":"undefined"===typeof Int8Array?r:Int8Array,"%Int16Array%":"undefined"===typeof Int16Array?r:Int16Array,"%Int32Array%":"undefined"===typeof Int32Array?r:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":k&&j?j(j([][Symbol.iterator]())):r,"%JSON%":"object"===typeof JSON?JSON:r,"%Map%":"undefined"===typeof Map?r:Map,"%MapIteratorPrototype%":"undefined"!==typeof Map&&k&&j?j((new Map)[Symbol.iterator]()):r,"%Math%":Math,"%Number%":Number,"%Object%":o,"%Object.getOwnPropertyDescriptor%":x,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"===typeof Promise?r:Promise,"%Proxy%":"undefined"===typeof Proxy?r:Proxy,"%RangeError%":s,"%ReferenceError%":u,"%Reflect%":"undefined"===typeof Reflect?r:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"===typeof Set?r:Set,"%SetIteratorPrototype%":"undefined"!==typeof Set&&k&&j?j((new Set)[Symbol.iterator]()):r,"%SharedArrayBuffer%":"undefined"===typeof SharedArrayBuffer?r:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":k&&j?j(""[Symbol.iterator]()):r,"%Symbol%":k?Symbol:r,"%SyntaxError%":l,"%ThrowTypeError%":E,"%TypedArray%":A,"%TypeError%":c,"%Uint8Array%":"undefined"===typeof Uint8Array?r:Uint8Array,"%Uint8ClampedArray%":"undefined"===typeof Uint8ClampedArray?r:Uint8ClampedArray,"%Uint16Array%":"undefined"===typeof Uint16Array?r:Uint16Array,"%Uint32Array%":"undefined"===typeof Uint32Array?r:Uint32Array,"%URIError%":f,"%WeakMap%":"undefined"===typeof WeakMap?r:WeakMap,"%WeakRef%":"undefined"===typeof WeakRef?r:WeakRef,"%WeakSet%":"undefined"===typeof WeakSet?r:WeakSet,"%Function.prototype.call%":P,"%Function.prototype.apply%":R,"%Object.defineProperty%":O,"%Object.getPrototypeOf%":C,"%Math.abs%":d,"%Math.floor%":p,"%Math.max%":h,"%Math.min%":m,"%Math.pow%":v,"%Math.round%":g,"%Math.sign%":y,"%Reflect.getPrototypeOf%":_};if(j)try{null.error}catch(K){var M=j(j(K));L["%Error.prototype%"]=M}var N=function e(t){var n;if("%AsyncFunction%"===t)n=w("async function () {}");else if("%GeneratorFunction%"===t)n=w("function* () {}");else if("%AsyncGeneratorFunction%"===t)n=w("async function* () {}");else if("%AsyncGenerator%"===t){var r=e("%AsyncGeneratorFunction%");r&&(n=r.prototype)}else if("%AsyncIteratorPrototype%"===t){var o=e("%AsyncGenerator%");o&&j&&(n=j(o.prototype))}return L[t]=n,n},I={__proto__:null,"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},D=n(107),F=n(412),z=D.call(P,Array.prototype.concat),B=D.call(R,Array.prototype.splice),U=D.call(P,String.prototype.replace),H=D.call(P,String.prototype.slice),W=D.call(P,RegExp.prototype.exec),V=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,q=/\\(\\)?/g,$=function(e,t){var n,r=e;if(F(I,r)&&(r="%"+(n=I[r])[0]+"%"),F(L,r)){var o=L[r];if(o===T&&(o=N(r)),"undefined"===typeof o&&!t)throw new c("intrinsic "+e+" exists, but is not available. Please file an issue!");return{alias:n,name:r,value:o}}throw new l("intrinsic "+e+" does not exist!")};e.exports=function(e,t){if("string"!==typeof e||0===e.length)throw new c("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!==typeof t)throw new c('"allowMissing" argument must be a boolean');if(null===W(/^%?[^%]*%?$/,e))throw new l("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var n=function(e){var t=H(e,0,1),n=H(e,-1);if("%"===t&&"%"!==n)throw new l("invalid intrinsic syntax, expected closing `%`");if("%"===n&&"%"!==t)throw new l("invalid intrinsic syntax, expected opening `%`");var r=[];return U(e,V,(function(e,t,n,o){r[r.length]=n?U(o,q,"$1"):t||e})),r}(e),r=n.length>0?n[0]:"",o=$("%"+r+"%",t),i=o.name,a=o.value,s=!1,u=o.alias;u&&(r=u[0],B(n,z([0,1],u)));for(var f=1,d=!0;fr.charCodeAt(0)&&(r=r.trim()),r=[r],0s[h]&&(e.offsets.popper[d]+=u[d]+m-s[h]),e.offsets.popper=S(e.offsets.popper);var v=u[d]+u[c]/2-m/2,g=a(e.instance.popper),y=parseFloat(g["margin"+f]),b=parseFloat(g["border"+f+"Width"]),w=v-e.offsets.popper[d]-y-b;return w=Math.max(Math.min(s[c]-m,w),0),e.arrowElement=r,e.offsets.arrow=(x(n={},d,Math.round(w)),x(n,p,""),n),e},element:"[x-arrow]"},flip:{order:600,enabled:!0,fn:function(e,t){if(D(e.instance.modifiers,"inner"))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var n=_(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement,e.positionFixed),r=e.placement.split("-")[0],o=A(r),i=e.placement.split("-")[1]||"",a=[];switch(t.behavior){case J:a=[r,o];break;case Z:a=X(r);break;case ee:a=X(r,!0);break;default:a=t.behavior}return a.forEach((function(s,u){if(r!==s||a.length===u+1)return e;r=e.placement.split("-")[0],o=A(r);var l=e.offsets.popper,c=e.offsets.reference,f=Math.floor,d="left"===r&&f(l.right)>f(c.left)||"right"===r&&f(l.left)>1,c=-7,f=n?o-1:0,d=n?-1:1,p=e[t+f];for(f+=d,i=p&(1<<-c)-1,p>>=-c,c+=s;c>0;i=256*i+e[t+f],f+=d,c-=8);for(a=i&(1<<-c)-1,i>>=-c,c+=r;c>0;a=256*a+e[t+f],f+=d,c-=8);if(0===i)i=1-l;else{if(i===u)return a?NaN:1/0*(p?-1:1);a+=Math.pow(2,r),i-=l}return(p?-1:1)*a*Math.pow(2,i-r)},t.write=function(e,t,n,r,o,i){var a,s,u,l=8*i-o-1,c=(1<=i?a(u,e):(n=u,r||(r=setTimeout((()=>{r=null,a(n)}),i-t)))},()=>n&&a(n)]};const H=function(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:3,r=0;const o=B(50,250);return U((n=>{const i=n.loaded,a=n.lengthComputable?n.total:void 0,s=i-r,u=o(s);r=i;e({loaded:i,total:a,progress:a?i/a:void 0,bytes:s,rate:u||void 0,estimated:u&&a&&i<=a?(a-i)/u:void 0,event:n,lengthComputable:null!=a,[t?"download":"upload"]:!0})}),n)},W=(e,t)=>{const n=null!=e;return[r=>t[0]({lengthComputable:n,total:e,loaded:r}),t[1]]},V=e=>function(){for(var t=arguments.length,n=new Array(t),r=0;r