From 0dbdd1669ceb864a9a031c6eb997b70154391ea8 Mon Sep 17 00:00:00 2001 From: "github-action[bot]" Date: Fri, 17 May 2024 20:30:03 +0200 Subject: [PATCH] Update On Fri May 17 20:30:02 CEST 2024 --- .github/update.log | 1 + .../component/updater/update_core.go | 2 +- .../updater}/update_geo.go | 94 +++- .../updater}/update_ui.go | 6 +- .../component/updater/utils.go | 23 + clash-meta/config/config.go | 19 +- clash-meta/config/utils.go | 23 - clash-meta/hub/route/configs.go | 31 +- clash-meta/hub/route/upgrade.go | 10 +- clash-meta/main.go | 50 +- .../frontend/interface/ipc/useNyanpasu.ts | 36 +- .../frontend/interface/package.json | 2 +- .../frontend/interface/service/tauri.ts | 15 + .../frontend/interface/service/types.ts | 5 +- clash-nyanpasu/frontend/nyanpasu/package.json | 10 +- .../src/components/profiles/profile-item.tsx | 71 +++ .../nyanpasu/src/components/profiles/utils.ts | 36 ++ .../frontend/nyanpasu/src/pages/profiles.tsx | 469 ++---------------- .../materialYou/components/sidePage/index.tsx | 41 +- .../components/sidePage/style.module.scss | 3 +- clash-nyanpasu/frontend/ui/package.json | 8 +- clash-nyanpasu/package.json | 2 +- clash-nyanpasu/pnpm-lock.yaml | 165 +++--- .../src/assets/image/component/match_case.svg | 6 + .../image/component/match_whole_word.svg | 6 + .../component/use_regular_expression.svg | 9 + .../src/components/base/base-search-box.tsx | 143 ++++++ .../src/components/proxy/proxy-item-mini.tsx | 11 +- .../components/setting/mods/config-viewer.tsx | 2 +- .../src/components/setting/setting-verge.tsx | 2 +- clash-verge-rev/src/locales/en.json | 3 + clash-verge-rev/src/locales/ru.json | 5 +- clash-verge-rev/src/locales/zh.json | 3 + clash-verge-rev/src/pages/connections.tsx | 14 +- clash-verge-rev/src/pages/logs.tsx | 60 +-- clash-verge-rev/src/pages/rules.tsx | 13 +- echo/internal/cmgr/cmgr.go | 8 + echo/internal/relay/conf/cfg.go | 3 +- echo/internal/transporter/base.go | 7 + echo/pkg/node_metric/utils.go | 18 +- echo/test/echo/echo.go | 36 +- echo/test/relay_test.go | 30 +- .../component/updater/update_core.go | 2 +- .../updater}/update_geo.go | 94 +++- .../updater}/update_ui.go | 6 +- .../component/updater/utils.go | 23 + mihomo/config/config.go | 19 +- mihomo/config/utils.go | 23 - mihomo/hub/route/configs.go | 31 +- mihomo/hub/route/upgrade.go | 10 +- mihomo/main.go | 50 +- .../luci-app-filebrowser/po/sv/filebrowser.po | 20 + .../com/v2ray/ang/ui/MainRecyclerAdapter.kt | 2 +- .../kotlin/com/v2ray/ang/util/MmkvManager.kt | 6 +- .../com/v2ray/ang/viewmodel/MainViewModel.kt | 29 +- yass/src/cli/cli_connection.cpp | 138 +++--- yass/src/cli/cli_connection.hpp | 1 + yass/src/net/c-ares.cpp | 4 +- yass/src/net/http_parser.cpp | 58 +-- yass/src/net/http_parser.hpp | 16 +- yass/src/server/server_connection.cpp | 63 +-- yass/src/server/server_connection.hpp | 1 + yass/third_party/libc++/gcc-mac.patch | 28 -- .../libc++/gcc-using-if-exist.patch | 98 ---- .../third_party/nghttp2/lib/nghttp2_session.c | 9 - yt-dlp/README.md | 8 +- yt-dlp/yt_dlp/cookies.py | 6 +- yt-dlp/yt_dlp/extractor/bbc.py | 438 +++++++++++----- yt-dlp/yt_dlp/extractor/cda.py | 62 ++- yt-dlp/yt_dlp/extractor/common.py | 17 +- yt-dlp/yt_dlp/extractor/tiktok.py | 28 +- yt-dlp/yt_dlp/extractor/twitter.py | 27 +- yt-dlp/yt_dlp/extractor/youtube.py | 98 +++- 73 files changed, 1554 insertions(+), 1362 deletions(-) rename mihomo/hub/updater/updater.go => clash-meta/component/updater/update_core.go (99%) rename clash-meta/{config => component/updater}/update_geo.go (52%) rename clash-meta/{config => component/updater}/update_ui.go (97%) rename mihomo/hub/updater/limitedreader.go => clash-meta/component/updater/utils.go (70%) create mode 100644 clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx create mode 100644 clash-nyanpasu/frontend/nyanpasu/src/components/profiles/utils.ts create mode 100644 clash-verge-rev/src/assets/image/component/match_case.svg create mode 100644 clash-verge-rev/src/assets/image/component/match_whole_word.svg create mode 100644 clash-verge-rev/src/assets/image/component/use_regular_expression.svg create mode 100644 clash-verge-rev/src/components/base/base-search-box.tsx rename clash-meta/hub/updater/updater.go => mihomo/component/updater/update_core.go (99%) rename mihomo/{config => component/updater}/update_geo.go (52%) rename mihomo/{config => component/updater}/update_ui.go (97%) rename clash-meta/hub/updater/limitedreader.go => mihomo/component/updater/utils.go (70%) create mode 100644 openwrt-packages/luci-app-filebrowser/po/sv/filebrowser.po delete mode 100644 yass/third_party/libc++/gcc-mac.patch delete mode 100644 yass/third_party/libc++/gcc-using-if-exist.patch diff --git a/.github/update.log b/.github/update.log index 638c31f501..c891f65e78 100644 --- a/.github/update.log +++ b/.github/update.log @@ -649,3 +649,4 @@ Update On Mon May 13 20:26:53 CEST 2024 Update On Tue May 14 20:30:05 CEST 2024 Update On Wed May 15 20:30:36 CEST 2024 Update On Thu May 16 20:28:24 CEST 2024 +Update On Fri May 17 20:29:52 CEST 2024 diff --git a/mihomo/hub/updater/updater.go b/clash-meta/component/updater/update_core.go similarity index 99% rename from mihomo/hub/updater/updater.go rename to clash-meta/component/updater/update_core.go index df5da3f4d1..0070fbb110 100644 --- a/mihomo/hub/updater/updater.go +++ b/clash-meta/component/updater/update_core.go @@ -67,7 +67,7 @@ func (e *updateError) Error() string { // Update performs the auto-updater. It returns an error if the updater failed. // If firstRun is true, it assumes the configuration file doesn't exist. -func Update(execPath string) (err error) { +func UpdateCore(execPath string) (err error) { mu.Lock() defer mu.Unlock() diff --git a/clash-meta/config/update_geo.go b/clash-meta/component/updater/update_geo.go similarity index 52% rename from clash-meta/config/update_geo.go rename to clash-meta/component/updater/update_geo.go index 43cac25c8d..a98d94dc7a 100644 --- a/clash-meta/config/update_geo.go +++ b/clash-meta/component/updater/update_geo.go @@ -1,18 +1,29 @@ -package config +package updater import ( + "errors" "fmt" + "os" "runtime" + "sync" + "time" + "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/component/geodata" _ "github.com/metacubex/mihomo/component/geodata/standard" "github.com/metacubex/mihomo/component/mmdb" C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" "github.com/oschwald/maxminddb-golang" ) -func UpdateGeoDatabases() error { +var ( + updateGeoMux sync.Mutex + UpdatingGeo atomic.Bool +) + +func updateGeoDatabases() error { defer runtime.GC() geoLoader, err := geodata.GetGeoDataLoader("standard") if err != nil { @@ -88,3 +99,82 @@ func UpdateGeoDatabases() error { return nil } + +func UpdateGeoDatabases() error { + log.Infoln("[GEO] Start updating GEO database") + + updateGeoMux.Lock() + + if UpdatingGeo.Load() { + updateGeoMux.Unlock() + return errors.New("GEO database is updating, skip") + } + + UpdatingGeo.Store(true) + updateGeoMux.Unlock() + + defer func() { + UpdatingGeo.Store(false) + }() + + log.Infoln("[GEO] Updating GEO database") + + if err := updateGeoDatabases(); err != nil { + log.Errorln("[GEO] update GEO database error: %s", err.Error()) + return err + } + + return nil +} + +func getUpdateTime() (err error, time time.Time) { + var fileInfo os.FileInfo + if C.GeodataMode { + fileInfo, err = os.Stat(C.Path.GeoIP()) + if err != nil { + return err, time + } + } else { + fileInfo, err = os.Stat(C.Path.MMDB()) + if err != nil { + return err, time + } + } + + return nil, fileInfo.ModTime() +} + +func RegisterGeoUpdater() { + if C.GeoUpdateInterval <= 0 { + log.Errorln("[GEO] Invalid update interval: %d", C.GeoUpdateInterval) + return + } + + ticker := time.NewTicker(time.Duration(C.GeoUpdateInterval) * time.Hour) + defer ticker.Stop() + + log.Infoln("[GEO] update GEO database every %d hours", C.GeoUpdateInterval) + go func() { + err, lastUpdate := getUpdateTime() + if err != nil { + log.Errorln("[GEO] Get GEO database update time error: %s", err.Error()) + return + } + + log.Infoln("[GEO] last update time %s", lastUpdate) + if lastUpdate.Add(time.Duration(C.GeoUpdateInterval) * time.Hour).Before(time.Now()) { + log.Infoln("[GEO] Database has not been updated for %v, update now", time.Duration(C.GeoUpdateInterval)*time.Hour) + if err := UpdateGeoDatabases(); err != nil { + log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) + return + } + } + + for range ticker.C { + if err := UpdateGeoDatabases(); err != nil { + log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) + return + } + } + }() +} diff --git a/clash-meta/config/update_ui.go b/clash-meta/component/updater/update_ui.go similarity index 97% rename from clash-meta/config/update_ui.go rename to clash-meta/component/updater/update_ui.go index cff1d6d7bd..85452ba550 100644 --- a/clash-meta/config/update_ui.go +++ b/clash-meta/component/updater/update_ui.go @@ -1,4 +1,4 @@ -package config +package updater import ( "archive/zip" @@ -29,7 +29,7 @@ func UpdateUI() error { xdMutex.Lock() defer xdMutex.Unlock() - err := prepare() + err := prepare_ui() if err != nil { return err } @@ -64,7 +64,7 @@ func UpdateUI() error { return nil } -func prepare() error { +func prepare_ui() error { if ExternalUIPath == "" || ExternalUIURL == "" { return ErrIncompleteConf } diff --git a/mihomo/hub/updater/limitedreader.go b/clash-meta/component/updater/utils.go similarity index 70% rename from mihomo/hub/updater/limitedreader.go rename to clash-meta/component/updater/utils.go index c31db601d0..0eecfc6cdc 100644 --- a/mihomo/hub/updater/limitedreader.go +++ b/clash-meta/component/updater/utils.go @@ -1,12 +1,35 @@ package updater import ( + "context" "fmt" "io" + "net/http" + "os" + "time" + + mihomoHttp "github.com/metacubex/mihomo/component/http" + C "github.com/metacubex/mihomo/constant" "golang.org/x/exp/constraints" ) +func downloadForBytes(url string) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return io.ReadAll(resp.Body) +} + +func saveFile(bytes []byte, path string) error { + return os.WriteFile(path, bytes, 0o644) +} + // LimitReachedError records the limit and the operation that caused it. type LimitReachedError struct { Limit int64 diff --git a/clash-meta/config/config.go b/clash-meta/config/config.go index 311fd2e285..9bc0afc824 100644 --- a/clash-meta/config/config.go +++ b/clash-meta/config/config.go @@ -28,6 +28,7 @@ import ( SNIFF "github.com/metacubex/mihomo/component/sniffer" tlsC "github.com/metacubex/mihomo/component/tls" "github.com/metacubex/mihomo/component/trie" + "github.com/metacubex/mihomo/component/updater" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" providerTypes "github.com/metacubex/mihomo/constant/provider" @@ -640,28 +641,28 @@ func parseGeneral(cfg *RawConfig) (*General, error) { N.KeepAliveInterval = time.Duration(cfg.KeepAliveInterval) * time.Second } - ExternalUIPath = cfg.ExternalUI + updater.ExternalUIPath = cfg.ExternalUI // checkout externalUI exist - if ExternalUIPath != "" { - ExternalUIPath = C.Path.Resolve(ExternalUIPath) - if _, err := os.Stat(ExternalUIPath); os.IsNotExist(err) { + if updater.ExternalUIPath != "" { + updater.ExternalUIPath = C.Path.Resolve(updater.ExternalUIPath) + if _, err := os.Stat(updater.ExternalUIPath); os.IsNotExist(err) { defaultUIpath := path.Join(C.Path.HomeDir(), "ui") - log.Warnln("external-ui: %s does not exist, creating folder in %s", ExternalUIPath, defaultUIpath) + log.Warnln("external-ui: %s does not exist, creating folder in %s", updater.ExternalUIPath, defaultUIpath) if err := os.MkdirAll(defaultUIpath, os.ModePerm); err != nil { return nil, err } - ExternalUIPath = defaultUIpath + updater.ExternalUIPath = defaultUIpath cfg.ExternalUI = defaultUIpath } } // checkout UIpath/name exist if cfg.ExternalUIName != "" { - ExternalUIName = cfg.ExternalUIName + updater.ExternalUIName = cfg.ExternalUIName } else { - ExternalUIFolder = ExternalUIPath + updater.ExternalUIFolder = updater.ExternalUIPath } if cfg.ExternalUIURL != "" { - ExternalUIURL = cfg.ExternalUIURL + updater.ExternalUIURL = cfg.ExternalUIURL } cfg.Tun.RedirectToTun = cfg.EBpf.RedirectToTun diff --git a/clash-meta/config/utils.go b/clash-meta/config/utils.go index 66bf3441f2..f87fb34131 100644 --- a/clash-meta/config/utils.go +++ b/clash-meta/config/utils.go @@ -1,38 +1,15 @@ package config import ( - "context" "fmt" - "io" "net" - "net/http" "net/netip" - "os" "strings" - "time" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/structure" - mihomoHttp "github.com/metacubex/mihomo/component/http" - C "github.com/metacubex/mihomo/constant" ) -func downloadForBytes(url string) ([]byte, error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - return io.ReadAll(resp.Body) -} - -func saveFile(bytes []byte, path string) error { - return os.WriteFile(path, bytes, 0o644) -} - func trimArr(arr []string) (r []string) { for _, e := range arr { r = append(r, strings.Trim(e, " ")) diff --git a/clash-meta/hub/route/configs.go b/clash-meta/hub/route/configs.go index 653e43519b..47fd26e0a1 100644 --- a/clash-meta/hub/route/configs.go +++ b/clash-meta/hub/route/configs.go @@ -4,11 +4,11 @@ import ( "net/http" "net/netip" "path/filepath" - "sync" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/hub/executor" @@ -21,11 +21,6 @@ import ( "github.com/go-chi/render" ) -var ( - updateGeoMux sync.Mutex - updatingGeo = false -) - func configRouter() http.Handler { r := chi.NewRouter() r.Get("/", getConfigs) @@ -369,30 +364,20 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) { } func updateGeoDatabases(w http.ResponseWriter, r *http.Request) { - updateGeoMux.Lock() - - if updatingGeo { - updateGeoMux.Unlock() + if updater.UpdatingGeo.Load() { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("updating...")) return } - updatingGeo = true - updateGeoMux.Unlock() + err := updater.UpdateGeoDatabases() + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError(err.Error())) + return + } go func() { - defer func() { - updatingGeo = false - }() - - log.Warnln("[REST-API] updating GEO databases...") - - if err := config.UpdateGeoDatabases(); err != nil { - log.Errorln("[REST-API] update GEO databases failed: %v", err) - return - } - cfg, err := executor.ParseWithPath(C.Path.Config()) if err != nil { log.Errorln("[REST-API] update GEO databases failed: %v", err) diff --git a/clash-meta/hub/route/upgrade.go b/clash-meta/hub/route/upgrade.go index ea371798f3..db00af5c8a 100644 --- a/clash-meta/hub/route/upgrade.go +++ b/clash-meta/hub/route/upgrade.go @@ -6,8 +6,7 @@ import ( "net/http" "os" - "github.com/metacubex/mihomo/config" - "github.com/metacubex/mihomo/hub/updater" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/log" "github.com/go-chi/chi/v5" @@ -18,6 +17,7 @@ func upgradeRouter() http.Handler { r := chi.NewRouter() r.Post("/", upgradeCore) r.Post("/ui", updateUI) + r.Post("/geo", updateGeoDatabases) return r } @@ -31,7 +31,7 @@ func upgradeCore(w http.ResponseWriter, r *http.Request) { return } - err = updater.Update(execPath) + err = updater.UpdateCore(execPath) if err != nil { log.Warnln("%s", err) render.Status(r, http.StatusInternalServerError) @@ -48,9 +48,9 @@ func upgradeCore(w http.ResponseWriter, r *http.Request) { } func updateUI(w http.ResponseWriter, r *http.Request) { - err := config.UpdateUI() + err := updater.UpdateUI() if err != nil { - if errors.Is(err, config.ErrIncompleteConf) { + if errors.Is(err, updater.ErrIncompleteConf) { log.Warnln("%s", err) render.Status(r, http.StatusNotImplemented) render.JSON(w, r, newError(fmt.Sprintf("%s", err))) diff --git a/clash-meta/main.go b/clash-meta/main.go index afe9cfd244..1d16f8bfc7 100644 --- a/clash-meta/main.go +++ b/clash-meta/main.go @@ -8,10 +8,9 @@ import ( "path/filepath" "runtime" "strings" - "sync" "syscall" - "time" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" @@ -32,8 +31,6 @@ var ( externalController string externalControllerUnix string secret string - updateGeoMux sync.Mutex - updatingGeo = false ) func init() { @@ -116,14 +113,7 @@ func main() { } if C.GeoAutoUpdate { - ticker := time.NewTicker(time.Duration(C.GeoUpdateInterval) * time.Hour) - - log.Infoln("[GEO] Start update GEO database every %d hours", C.GeoUpdateInterval) - go func() { - for range ticker.C { - updateGeoDatabases() - } - }() + updater.RegisterGeoUpdater() } defer executor.Shutdown() @@ -145,39 +135,3 @@ func main() { } } } - -func updateGeoDatabases() { - log.Infoln("[GEO] Start updating GEO database") - updateGeoMux.Lock() - - if updatingGeo { - updateGeoMux.Unlock() - log.Infoln("[GEO] GEO database is updating, skip") - return - } - - updatingGeo = true - updateGeoMux.Unlock() - - go func() { - defer func() { - updatingGeo = false - }() - - log.Infoln("[GEO] Updating GEO database") - - if err := config.UpdateGeoDatabases(); err != nil { - log.Errorln("[GEO] update GEO database error: %s", err.Error()) - return - } - - cfg, err := executor.ParseWithPath(C.Path.Config()) - if err != nil { - log.Errorln("[GEO] update GEO database failed: %s", err.Error()) - return - } - - log.Infoln("[GEO] Update GEO database success, apply new config") - executor.ApplyConfig(cfg, false) - }() -} diff --git a/clash-nyanpasu/frontend/interface/ipc/useNyanpasu.ts b/clash-nyanpasu/frontend/interface/ipc/useNyanpasu.ts index 8cfd7bea72..12f169cf0c 100644 --- a/clash-nyanpasu/frontend/interface/ipc/useNyanpasu.ts +++ b/clash-nyanpasu/frontend/interface/ipc/useNyanpasu.ts @@ -1,9 +1,10 @@ import useSWR from "swr"; import * as service from "@/service"; -import { VergeConfig, restartSidecar, getCoreVersion } from "@/service"; +import { VergeConfig } from "@/service"; import { fetchCoreVersion, fetchLatestCore } from "@/service/core"; import { useClash } from "./useClash"; import { useMemo } from "react"; +import * as tauri from "@/service/tauri"; /** * useNyanpasu with swr. @@ -103,15 +104,40 @@ export const useNyanpasu = (options?: { return modes; }, [data?.clash_core, getConfigs.data?.mode]); + const getProfiles = useSWR("getProfiles", tauri.getProfiles); + + const createProfile = async ( + item: Partial, + data?: string, + ) => { + await tauri.createProfile(item, data); + + await getProfiles.mutate(); + }; + + const getProfileFile = async (id?: string) => { + if (id) { + const result = await tauri.readProfileFile(id); + + if (result) { + return result; + } else { + return ""; + } + } else { + return ""; + } + }; + return { nyanpasuConfig: data, isLoading: !data && !error, isError: error, setNyanpasuConfig, - getCoreVersion, + getCoreVersion: tauri.getCoreVersion, getClashCore, setClashCore, - restartSidecar, + restartSidecar: tauri.restartSidecar, getLatestCore, updateCore, getSystemProxy, @@ -119,5 +145,9 @@ export const useNyanpasu = (options?: { setServiceStatus, getCurrentMode, setCurrentMode, + createProfile, + getProfiles, + getProfileFile, + setProfileFile: tauri.saveProfileFile, }; }; diff --git a/clash-nyanpasu/frontend/interface/package.json b/clash-nyanpasu/frontend/interface/package.json index 89138a209a..402c152477 100644 --- a/clash-nyanpasu/frontend/interface/package.json +++ b/clash-nyanpasu/frontend/interface/package.json @@ -4,7 +4,7 @@ "main": "index.ts", "module": "index.ts", "dependencies": { - "@tauri-apps/api": "1.5.5", + "@tauri-apps/api": "1.5.6", "ofetch": "1.3.4", "react": "18.3.1", "swr": "2.2.5" diff --git a/clash-nyanpasu/frontend/interface/service/tauri.ts b/clash-nyanpasu/frontend/interface/service/tauri.ts index 62b8c9aec7..daf7e9e825 100644 --- a/clash-nyanpasu/frontend/interface/service/tauri.ts +++ b/clash-nyanpasu/frontend/interface/service/tauri.ts @@ -29,6 +29,13 @@ export const getRuntimeExists = async () => { return await invoke("get_runtime_exists"); }; +export const createProfile = async ( + item: Partial, + fileData?: string | null, +) => { + return await invoke("create_profile", { item, fileData }); +}; + export const getProfiles = async () => { return await invoke("get_profiles"); }; @@ -44,6 +51,14 @@ export const setProfilesConfig = async (profiles: Profile.Config) => { return await invoke("patch_profiles_config", { profiles }); }; +export const readProfileFile = async (index: string) => { + return await invoke("read_profile_file", { index }); +}; + +export const saveProfileFile = async (index: string, fileData: string) => { + return await invoke("save_profile_file", { index, fileData }); +}; + export const getCoreVersion = async ( coreType: Required["clash_core"], ) => { diff --git a/clash-nyanpasu/frontend/interface/service/types.ts b/clash-nyanpasu/frontend/interface/service/types.ts index c8541a0e74..f4077f44d8 100644 --- a/clash-nyanpasu/frontend/interface/service/types.ts +++ b/clash-nyanpasu/frontend/interface/service/types.ts @@ -75,9 +75,11 @@ export namespace Profile { items?: Item[]; } + export type ScriptType = "javascript" | "lua"; + export interface Item { uid: string; - type?: "local" | "remote" | "merge" | "script"; + type?: "local" | "remote" | "merge" | { script: ScriptType }; name?: string; desc?: string; file?: string; @@ -94,6 +96,7 @@ export namespace Profile { expire: number; }; option?: Option; + chains?: string[]; } export interface Option { diff --git a/clash-nyanpasu/frontend/nyanpasu/package.json b/clash-nyanpasu/frontend/nyanpasu/package.json index 200843b120..e4d6142b1d 100644 --- a/clash-nyanpasu/frontend/nyanpasu/package.json +++ b/clash-nyanpasu/frontend/nyanpasu/package.json @@ -16,17 +16,17 @@ "@generouted/react-router": "1.19.4", "@juggle/resize-observer": "3.4.0", "@material/material-color-utilities": "0.2.7", - "@mui/icons-material": "5.15.17", + "@mui/icons-material": "5.15.18", "@mui/lab": "5.0.0-alpha.170", - "@mui/material": "5.15.17", + "@mui/material": "5.15.18", "@mui/x-data-grid": "7.4.0", "@nyanpasu/interface": "workspace:^", "@nyanpasu/ui": "workspace:^", - "@tauri-apps/api": "1.5.5", + "@tauri-apps/api": "1.5.6", "ahooks": "3.7.11", "axios": "1.6.8", "dayjs": "1.11.11", - "framer-motion": "11.2.0", + "framer-motion": "11.2.4", "i18next": "23.11.4", "jotai": "2.8.0", "monaco-editor": "0.48.0", @@ -55,7 +55,7 @@ "@typescript-eslint/eslint-plugin": "7.9.0", "@typescript-eslint/parser": "7.9.0", "@vitejs/plugin-react": "4.2.1", - "sass": "1.77.1", + "sass": "1.77.2", "shiki": "1.5.2", "vite": "5.2.11", "vite-plugin-monaco-editor": "1.1.3", diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx new file mode 100644 index 0000000000..a16f5a0b96 --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/profile-item.tsx @@ -0,0 +1,71 @@ +import parseTraffic from "@/utils/parse-traffic"; +import { Update, MoreVert } from "@mui/icons-material"; +import { Paper, Button, LinearProgress } from "@mui/material"; +import { Profile } from "@nyanpasu/interface"; +import dayjs from "dayjs"; +import { memo } from "react"; +import Marquee from "react-fast-marquee"; + +export interface ProfileItemProps { + item: Profile.Item; +} + +export const ProfileItem = memo(function ProfileItem({ + item, +}: ProfileItemProps) { + const calc = () => { + let progress = 0; + let total = 0; + let used = 0; + + if (item.extra) { + const { download, upload, total: t } = item.extra; + + total = t; + + used = download + upload; + + progress = (used / total) * 100; + } + + return { progress, total, used }; + }; + + const { progress, total, used } = calc(); + + return ( + +
+
{item.name}
+ +
+ + + +
+
+ + +
{item.url}
+
+ +
+
+ {item.updated! > 0 ? dayjs(item.updated! * 1000).fromNow() : ""} +
+ +
+ {`${parseTraffic(used)} / ${parseTraffic(total)}`} +
+
+ + +
+ ); +}); + +export default ProfileItem; diff --git a/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/utils.ts b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/utils.ts new file mode 100644 index 0000000000..bd9108add0 --- /dev/null +++ b/clash-nyanpasu/frontend/nyanpasu/src/components/profiles/utils.ts @@ -0,0 +1,36 @@ +import { Profile } from "@nyanpasu/interface"; + +export const filterProfiles = (items?: Profile.Item[]) => { + const getItems = (types: (string | { script: string })[]) => { + return items?.filter((i) => { + if (!i) return false; + + if (typeof i.type === "string") { + return types.includes(i.type); + } + + if (typeof i.type === "object" && i.type !== null) { + return types.some( + (type) => + typeof type === "object" && + (i.type as { script: string }).script === type.script, + ); + } + + return false; + }); + }; + + const profiles = getItems(["local", "remote"]); + + const scripts = getItems([ + "merge", + { script: "javascript" }, + { script: "lua" }, + ]); + + return { + profiles, + scripts, + }; +}; diff --git a/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx b/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx index f4885fbf95..b317a51370 100644 --- a/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx +++ b/clash-nyanpasu/frontend/nyanpasu/src/pages/profiles.tsx @@ -1,441 +1,50 @@ -import { BasePage, DialogRef } from "@/components/base"; -import { ProfileItem } from "@/components/profile/profile-item"; -import { ProfileMore } from "@/components/profile/profile-more"; -import { - ProfileViewer, - ProfileViewerRef, -} from "@/components/profile/profile-viewer"; -import { ConfigViewer } from "@/components/setting/mods/config-viewer"; -import { NotificationType, useNotification } from "@/hooks/use-notification"; -import { useProfiles } from "@/hooks/use-profiles"; -import { closeAllConnections } from "@/services/api"; -import { - deleteProfile, - enhanceProfiles, - getProfiles, - getRuntimeLogs, - importProfile, - reorderProfile, - updateProfile, -} from "@/services/cmds"; -import { atomLoadingCache } from "@/store"; -import { - DndContext, - DragEndEvent, - KeyboardSensor, - PointerSensor, - closestCenter, - useSensor, - useSensors, -} from "@dnd-kit/core"; -import { - SortableContext, - sortableKeyboardCoordinates, -} from "@dnd-kit/sortable"; -import { - ClearRounded, - ContentCopyRounded, - LocalFireDepartmentRounded, - RefreshRounded, - TextSnippetOutlined, -} from "@mui/icons-material"; -import LoadingButton from "@mui/lab/LoadingButton"; -import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material"; -import { useLockFn } from "ahooks"; -import { useSetAtom } from "jotai"; -import { throttle } from "lodash-es"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { Button, alpha, useTheme } from "@mui/material"; +import { useNyanpasu } from "@nyanpasu/interface"; +import { SidePage } from "@nyanpasu/ui"; import { useTranslation } from "react-i18next"; -import { useLocation } from "react-router-dom"; -import useSWR, { mutate } from "swr"; +import Grid from "@mui/material/Unstable_Grid2"; +import { Add } from "@mui/icons-material"; +import ProfileItem from "@/components/profiles/profile-item"; +import { filterProfiles } from "@/components/profiles/utils"; -export default function ProfilePage() { +export const ProfilePage = () => { const { t } = useTranslation(); - const location = useLocation(); - const [url, setUrl] = useState(""); - const [disabled, setDisabled] = useState(false); - const [activating, setActivating] = useState(""); - const [loading, setLoading] = useState(false); - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }), - ); + const { getProfiles } = useNyanpasu(); - const { - profiles = {}, - activateSelected, - patchProfiles, - mutateProfiles, - } = useProfiles(); + const { profiles } = filterProfiles(getProfiles.data?.items); - const { data: chainLogs = {}, mutate: mutateLogs } = useSWR( - "getRuntimeLogs", - getRuntimeLogs, - ); - - const chain = profiles.chain || []; - const viewerRef = useRef(null); - const configRef = useRef(null); - - // distinguish type - const { regularItems, enhanceItems } = useMemo(() => { - const items = profiles.items || []; - const chain = profiles.chain || []; - - const type1 = ["local", "remote"]; - const type2 = ["merge", "script"]; - - const regularItems = items.filter((i) => i && type1.includes(i.type!)); - const restItems = items.filter((i) => i && type2.includes(i.type!)); - const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i])); - const enhanceItems = chain - .map((i) => restMap[i]!) - .filter(Boolean) - .concat(restItems.filter((i) => !chain.includes(i.uid))); - - return { regularItems, enhanceItems }; - }, [profiles]); - - useEffect(() => { - if (location.state != null) { - console.log(location.state.scheme); - viewerRef.current?.create(); - } - }, [location]); - - const onImport = async () => { - if (!url) return; - setLoading(true); - - try { - await importProfile(url); - useNotification({ - title: t("Success"), - body: "Successfully import profile.", - type: NotificationType.Success, - }); - setUrl(""); - setLoading(false); - - getProfiles().then((newProfiles) => { - mutate("getProfiles", newProfiles); - - const remoteItem = newProfiles.items?.find((e) => e.type === "remote"); - if (!newProfiles.current && remoteItem) { - const current = remoteItem.uid; - patchProfiles({ current }); - mutateLogs(); - setTimeout(() => activateSelected(), 2000); - } - }); - } catch (err: any) { - useNotification({ - title: t("Error"), - body: err.message || err.toString(), - type: NotificationType.Error, - }); - setLoading(false); - } finally { - setDisabled(false); - setLoading(false); - } - }; - - const onDragEnd = async (event: DragEndEvent) => { - const { active, over } = event; - if (over) { - if (active.id !== over.id) { - await reorderProfile(active.id.toString(), over.id.toString()); - mutateProfiles(); - } - } - }; - - const onSelect = useLockFn(async (current: string, force: boolean) => { - if (!force && current === profiles.current) return; - // 避免大多数情况下loading态闪烁 - const reset = setTimeout(() => setActivating(current), 100); - try { - await patchProfiles({ current }); - mutateLogs(); - closeAllConnections(); - setTimeout(() => activateSelected(), 2000); - useNotification({ - title: t("Success"), - body: "Refresh Clash Config", - type: NotificationType.Success, - }); - } catch (err: any) { - useNotification({ - title: t("Error"), - body: err.message || err.toString(), - type: NotificationType.Error, - }); - } finally { - clearTimeout(reset); - setActivating(""); - } - }); - - const onEnhance = useLockFn(async () => { - try { - await enhanceProfiles(); - mutateLogs(); - useNotification({ - title: t("Success"), - body: "Refresh Clash Config", - type: NotificationType.Success, - }); - } catch (err: any) { - useNotification({ - title: t("Error"), - body: err.message || err.toString(), - type: NotificationType.Error, - }); - } - }); - - const onEnable = useLockFn(async (uid: string) => { - if (chain.includes(uid)) return; - const newChain = [...chain, uid]; - await patchProfiles({ chain: newChain }); - mutateLogs(); - }); - - const onDisable = useLockFn(async (uid: string) => { - if (!chain.includes(uid)) return; - const newChain = chain.filter((i) => i !== uid); - await patchProfiles({ chain: newChain }); - mutateLogs(); - }); - - const onDelete = useLockFn(async (uid: string) => { - try { - await onDisable(uid); - await deleteProfile(uid); - mutateProfiles(); - mutateLogs(); - } catch (err: any) { - useNotification({ - title: t("Error"), - body: err.message || err.toString(), - type: NotificationType.Error, - }); - } - }); - - const onMoveTop = useLockFn(async (uid: string) => { - if (!chain.includes(uid)) return; - const newChain = [uid].concat(chain.filter((i) => i !== uid)); - await patchProfiles({ chain: newChain }); - mutateLogs(); - }); - - const onMoveEnd = useLockFn(async (uid: string) => { - if (!chain.includes(uid)) return; - const newChain = chain.filter((i) => i !== uid).concat([uid]); - await patchProfiles({ chain: newChain }); - mutateLogs(); - }); - - // 更新所有配置 - const setLoadingCache = useSetAtom(atomLoadingCache); - const onUpdateAll = useLockFn(async () => { - const throttleMutate = throttle(mutateProfiles, 2000, { - trailing: true, - }); - const updateOne = async (uid: string) => { - try { - await updateProfile(uid); - throttleMutate(); - } finally { - setLoadingCache((cache) => ({ ...cache, [uid]: false })); - } - }; - - return new Promise((resolve) => { - setLoadingCache((cache) => { - // 获取没有正在更新的配置 - const items = regularItems.filter( - (e) => e.type === "remote" && !cache[e.uid], - ); - const change = Object.fromEntries(items.map((e) => [e.uid, true])); - - Promise.allSettled(items.map((e) => updateOne(e.uid))).then(resolve); - return { ...cache, ...change }; - }); - }); - }); - - const onCopyLink = async () => { - const text = await navigator.clipboard.readText(); - if (text) setUrl(text); - }; - - const [sectionOverflowStatus, setSectionOverflowStatus] = useState(false); + const { palette } = useTheme(); return ( - - - - - - configRef.current?.open()} - > - - - - - - - - } - > - - setUrl(e.target.value)} - sx={{ input: { py: 0.65, px: 1.25 } }} - placeholder={t("Profile URL")} - InputProps={{ - sx: { - borderRadius: 4, - pr: 1, - }, - endAdornment: !url ? ( - - - - ) : ( - setUrl("")} - > - - - ), - }} - /> - - {t("Import")} - - - - - setSectionOverflowStatus(true)} - onDragOver={() => setSectionOverflowStatus(false)} - > - - - { - return x.uid; - })} - > - {regularItems.map((item) => ( - - onSelect(item.uid, f)} - onEdit={() => viewerRef.current?.edit(item)} - /> - - ))} - - - - - - {enhanceItems.length > 0 && ( - - {enhanceItems.map((item) => ( - - onEnable(item.uid)} - onDisable={() => onDisable(item.uid)} - onDelete={() => onDelete(item.uid)} - onMoveTop={() => onMoveTop(item.uid)} - onMoveEnd={() => onMoveEnd(item.uid)} - onEdit={() => viewerRef.current?.edit(item)} - /> - - ))} + +
+ + {profiles?.map((item, index) => { + return ( + + + + ); + })} - )} +
- mutateProfiles()} - /> - -
+ + ); -} +}; + +export default ProfilePage; diff --git a/clash-nyanpasu/frontend/ui/materialYou/components/sidePage/index.tsx b/clash-nyanpasu/frontend/ui/materialYou/components/sidePage/index.tsx index 42278bceac..e04e589f48 100644 --- a/clash-nyanpasu/frontend/ui/materialYou/components/sidePage/index.tsx +++ b/clash-nyanpasu/frontend/ui/materialYou/components/sidePage/index.tsx @@ -4,6 +4,7 @@ import Toolbar from "@mui/material/Toolbar"; import Typography from "@mui/material/Typography"; import { BaseErrorBoundary } from "../basePage/baseErrorBoundary"; import style from "./style.module.scss"; +import { motion } from "framer-motion"; interface Props { title?: ReactNode; @@ -13,6 +14,7 @@ interface Props { side?: ReactNode; toolBar?: ReactNode; noChildrenScroll?: boolean; + flexReverse?: boolean; } const Header: FC<{ title?: ReactNode; header?: ReactNode }> = memo( @@ -43,6 +45,7 @@ export const SidePage: FC = ({ side, toolBar, noChildrenScroll, + flexReverse, }) => { return ( @@ -50,16 +53,38 @@ export const SidePage: FC = ({
-
- {side && ( -
- {sideBar &&
{sideBar}
} +
+ + {sideBar &&
{sideBar}
} -
-
{side}
-
+
+
{side}
- )} +
{toolBar && ( diff --git a/clash-nyanpasu/frontend/ui/materialYou/components/sidePage/style.module.scss b/clash-nyanpasu/frontend/ui/materialYou/components/sidePage/style.module.scss index 5c48c02f48..a6652b3716 100644 --- a/clash-nyanpasu/frontend/ui/materialYou/components/sidePage/style.module.scss +++ b/clash-nyanpasu/frontend/ui/materialYou/components/sidePage/style.module.scss @@ -23,14 +23,13 @@ gap: 16px; width: 100%; height: 100%; + transition: gap 0.3s; .LeftContainer { display: flex; flex-direction: column; gap: 16px; width: 40%; - min-width: 200px; - max-width: 348px; height: 100%; .LeftContainer-Content { diff --git a/clash-nyanpasu/frontend/ui/package.json b/clash-nyanpasu/frontend/ui/package.json index e505fd1440..8de5da6b81 100644 --- a/clash-nyanpasu/frontend/ui/package.json +++ b/clash-nyanpasu/frontend/ui/package.json @@ -5,17 +5,17 @@ "module": "index.ts", "dependencies": { "@material/material-color-utilities": "0.2.7", - "@mui/icons-material": "5.15.17", + "@mui/icons-material": "5.15.18", "@mui/lab": "5.0.0-alpha.170", - "@mui/material": "5.15.17", + "@mui/material": "5.15.18", "@types/react": "18.3.2", "ahooks": "3.7.11", - "framer-motion": "11.2.0", + "framer-motion": "11.2.4", "react": "18.3.1", "react-error-boundary": "4.0.13" }, "devDependencies": { - "sass": "1.77.1", + "sass": "1.77.2", "typescript-plugin-css-modules": "5.1.0" } } diff --git a/clash-nyanpasu/package.json b/clash-nyanpasu/package.json index d32b384e6a..5ec462d01b 100644 --- a/clash-nyanpasu/package.json +++ b/clash-nyanpasu/package.json @@ -105,7 +105,7 @@ "stylelint-order": "6.0.4", "stylelint-scss": "6.3.0", "tailwindcss": "3.4.3", - "tsx": "4.10.2", + "tsx": "4.10.4", "typescript": "5.4.5" }, "packageManager": "pnpm@9.1.1", diff --git a/clash-nyanpasu/pnpm-lock.yaml b/clash-nyanpasu/pnpm-lock.yaml index 3821d90c4e..49ab1a1b61 100644 --- a/clash-nyanpasu/pnpm-lock.yaml +++ b/clash-nyanpasu/pnpm-lock.yaml @@ -134,8 +134,8 @@ importers: specifier: 3.4.3 version: 3.4.3 tsx: - specifier: 4.10.2 - version: 4.10.2 + specifier: 4.10.4 + version: 4.10.4 typescript: specifier: 5.4.5 version: 5.4.5 @@ -143,8 +143,8 @@ importers: frontend/interface: dependencies: '@tauri-apps/api': - specifier: 1.5.5 - version: 1.5.5 + specifier: 1.5.6 + version: 1.5.6 ofetch: specifier: 1.3.4 version: 1.3.4 @@ -175,7 +175,7 @@ importers: version: 11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@generouted/react-router': specifier: 1.19.4 - version: 1.19.4(react-router-dom@6.23.1(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0)) + version: 1.19.4(react-router-dom@6.23.1(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0)) '@juggle/resize-observer': specifier: 3.4.0 version: 3.4.0 @@ -183,17 +183,17 @@ importers: specifier: 0.2.7 version: 0.2.7 '@mui/icons-material': - specifier: 5.15.17 - version: 5.15.17(@mui/material@5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + specifier: 5.15.18 + version: 5.15.18(@mui/material@5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@mui/lab': specifier: 5.0.0-alpha.170 - version: 5.0.0-alpha.170(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + version: 5.0.0-alpha.170(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@mui/material': - specifier: 5.15.17 - version: 5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + specifier: 5.15.18 + version: 5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@mui/x-data-grid': specifier: 7.4.0 - version: 7.4.0(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + version: 7.4.0(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@nyanpasu/interface': specifier: workspace:^ version: link:../interface @@ -201,8 +201,8 @@ importers: specifier: workspace:^ version: link:../ui '@tauri-apps/api': - specifier: 1.5.5 - version: 1.5.5 + specifier: 1.5.6 + version: 1.5.6 ahooks: specifier: 3.7.11 version: 3.7.11(react@19.0.0-beta-04b058868c-20240508) @@ -213,8 +213,8 @@ importers: specifier: 1.11.11 version: 1.11.11 framer-motion: - specifier: 11.2.0 - version: 11.2.0(@emotion/is-prop-valid@1.2.2)(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508) + specifier: 11.2.4 + version: 11.2.4(@emotion/is-prop-valid@1.2.2)(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508) i18next: specifier: 23.11.4 version: 23.11.4 @@ -226,7 +226,7 @@ importers: version: 0.48.0 mui-color-input: specifier: 2.0.3 - version: 2.0.3(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + version: 2.0.3(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) react: specifier: npm:react@beta version: 19.0.0-beta-04b058868c-20240508 @@ -293,28 +293,28 @@ importers: version: 7.9.0(eslint@8.57.0)(typescript@5.4.5) '@vitejs/plugin-react': specifier: 4.2.1 - version: 4.2.1(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0)) + version: 4.2.1(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0)) sass: - specifier: 1.77.1 - version: 1.77.1 + specifier: 1.77.2 + version: 1.77.2 shiki: specifier: 1.5.2 version: 1.5.2 vite: specifier: 5.2.11 - version: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0) + version: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0) vite-plugin-monaco-editor: specifier: npm:vite-plugin-monaco-editor-new@1.1.3 version: vite-plugin-monaco-editor-new@1.1.3(monaco-editor@0.48.0) vite-plugin-sass-dts: specifier: 1.3.22 - version: 1.3.22(postcss@8.4.38)(prettier@3.2.5)(sass@1.77.1)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0)) + version: 1.3.22(postcss@8.4.38)(prettier@3.2.5)(sass@1.77.2)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0)) vite-plugin-svgr: specifier: 4.2.0 - version: 4.2.0(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0)) + version: 4.2.0(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0)) vite-tsconfig-paths: specifier: 4.3.2 - version: 4.3.2(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0)) + version: 4.3.2(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0)) frontend/ui: dependencies: @@ -322,14 +322,14 @@ importers: specifier: 0.2.7 version: 0.2.7 '@mui/icons-material': - specifier: 5.15.17 - version: 5.15.17(@mui/material@5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + specifier: 5.15.18 + version: 5.15.18(@mui/material@5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@mui/lab': specifier: 5.0.0-alpha.170 - version: 5.0.0-alpha.170(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + version: 5.0.0-alpha.170(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@mui/material': - specifier: 5.15.17 - version: 5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + specifier: 5.15.18 + version: 5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@types/react': specifier: npm:types-react@beta version: types-react@19.0.0-beta.1 @@ -337,8 +337,8 @@ importers: specifier: 3.7.11 version: 3.7.11(react@19.0.0-beta-04b058868c-20240508) framer-motion: - specifier: 11.2.0 - version: 11.2.0(@emotion/is-prop-valid@1.2.2)(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508) + specifier: 11.2.4 + version: 11.2.4(@emotion/is-prop-valid@1.2.2)(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508) react: specifier: npm:react@beta version: 19.0.0-beta-04b058868c-20240508 @@ -347,8 +347,8 @@ importers: version: 4.0.13(react@19.0.0-beta-04b058868c-20240508) devDependencies: sass: - specifier: 1.77.1 - version: 1.77.1 + specifier: 1.77.2 + version: 1.77.2 typescript-plugin-css-modules: specifier: 5.1.0 version: 5.1.0(typescript@5.4.5) @@ -1060,11 +1060,11 @@ packages: '@types/react': optional: true - '@mui/core-downloads-tracker@5.15.17': - resolution: {integrity: sha512-DVAejDQkjNnIac7MfP8sLzuo7fyrBPxNdXe+6bYqOqg1z2OPTlfFAejSNzWe7UenRMuFu9/AyFXj/X2vN2w6dA==} + '@mui/core-downloads-tracker@5.15.18': + resolution: {integrity: sha512-/9pVk+Al8qxAjwFUADv4BRZgMpZM4m5E+2Q/20qhVPuIJWqKp4Ie4tGExac6zu93rgPTYVQGgu+1vjiT0E+cEw==} - '@mui/icons-material@5.15.17': - resolution: {integrity: sha512-xVzl2De7IY36s/keHX45YMiCpsIx3mNv2xwDgtBkRSnZQtVk+Gqufwj1ktUxEyjzEhBl0+PiNJqYC31C+n1n6A==} + '@mui/icons-material@5.15.18': + resolution: {integrity: sha512-jGhyw02TSLM0NgW+MDQRLLRUD/K4eN9rlK2pTBTL1OtzyZmQ8nB060zK1wA0b7cVrIiG+zyrRmNAvGWXwm2N9Q==} engines: {node: '>=12.0.0'} peerDependencies: '@mui/material': ^5.0.0 @@ -1092,8 +1092,8 @@ packages: '@types/react': optional: true - '@mui/material@5.15.17': - resolution: {integrity: sha512-ru/MLvTkCh0AZXmqwIpqGTOoVBS/sX48zArXq/DvktxXZx4fskiRA2PEc7Rk5ZlFiZhKh4moL4an+l8zZwq49Q==} + '@mui/material@5.15.18': + resolution: {integrity: sha512-n+/dsiqux74fFfcRUJjok+ieNQ7+BEk6/OwX9cLcLvriZrZb+/7Y8+Fd2HlUUbn5N0CDurgAHm0VH1DqyJ9HAw==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1456,8 +1456,8 @@ packages: '@taplo/lib@0.4.0-alpha.2': resolution: {integrity: sha512-DV/Re3DPVY+BhBtLZ3dmP4mP6YMLSsgq9qGLXwOV38lvNF/fBlgvQswzlXmzCEefL/3q2eMoefZpOI/+GLuCNA==} - '@tauri-apps/api@1.5.5': - resolution: {integrity: sha512-Jgwj8BK/9YXZNzcqVDk1Al7+u5V9sWrZ8MhV41A1AKgJaicHuqlkc/qdx06sNDXvc+qprTPpBAaqnt891qOUIQ==} + '@tauri-apps/api@1.5.6': + resolution: {integrity: sha512-LH5ToovAHnDVe5Qa9f/+jW28I6DeMhos8bNDtBOmmnaDpPmJmYLyHdeDblAWWWYc7KKRDg9/66vMuKyq0WIeFA==} engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} '@tauri-apps/cli-darwin-arm64@1.5.14': @@ -2626,8 +2626,8 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - framer-motion@11.2.0: - resolution: {integrity: sha512-LRfLVPEwtO9IXJCAsWvtj3XZxrdZDcTxNNkZEq30aQ8p7/wimfUkDy67TDWdtzPiyKDkqOHDhaQC6XVrQ4Fh7A==} + framer-motion@11.2.4: + resolution: {integrity: sha512-D+EXd0lspaZijv3BJhAcSsyGz+gnvoEdnf+QWkPZdhoFzbeX/2skrH9XSVFb0osgUnCajW8x1frjhLuKwa/Reg==} peerDependencies: '@emotion/is-prop-valid': '*' react: npm:react@beta @@ -2706,6 +2706,9 @@ packages: get-tsconfig@4.7.4: resolution: {integrity: sha512-ofbkKj+0pjXjhejr007J/fLf+sW+8H7K5GCm+msC8q3IpvgjobpyPqSRFemNyIMxklC0zeJpi7VDFna19FacvQ==} + get-tsconfig@4.7.5: + resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==} + git-raw-commits@4.0.0: resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} engines: {node: '>=16'} @@ -4156,8 +4159,8 @@ packages: resolution: {integrity: sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==} engines: {node: '>= 0.10'} - sass@1.77.1: - resolution: {integrity: sha512-OMEyfirt9XEfyvocduUIOlUSkWOXS/LAt6oblR/ISXCTukyavjex+zQNm51pPCOiFKY1QpWvEH1EeCkgyV3I6w==} + sass@1.77.2: + resolution: {integrity: sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==} engines: {node: '>=14.0.0'} hasBin: true @@ -4526,8 +4529,8 @@ packages: tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - tsx@4.10.2: - resolution: {integrity: sha512-gOfACgv1ElsIjvt7Fp0rMJKGnMGjox0JfGOfX3kmZCV/yZumaNqtHGKBXt1KgaYS9KjDOmqGeI8gHk/W7kWVZg==} + tsx@4.10.4: + resolution: {integrity: sha512-Gtg9qnZWNqC/OtcgiXfoAUdAKx3/cgKOYvEocAsv+m21MV/eKpV/WUjRXe6/sDCaGBl2/v8S6v29BpUnGMCX5A==} engines: {node: '>=18.0.0'} hasBin: true @@ -5461,13 +5464,13 @@ snapshots: '@floating-ui/utils@0.2.2': {} - '@generouted/react-router@1.19.4(react-router-dom@6.23.1(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0))': + '@generouted/react-router@1.19.4(react-router-dom@6.23.1(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0))': dependencies: fast-glob: 3.3.2 - generouted: 1.19.4(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0)) + generouted: 1.19.4(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0)) react: 19.0.0-beta-04b058868c-20240508 react-router-dom: 6.23.1(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508) - vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0) + vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0) '@humanwhocodes/config-array@0.11.14': dependencies: @@ -5525,21 +5528,21 @@ snapshots: optionalDependencies: '@types/react': types-react@19.0.0-beta.1 - '@mui/core-downloads-tracker@5.15.17': {} + '@mui/core-downloads-tracker@5.15.18': {} - '@mui/icons-material@5.15.17(@mui/material@5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1)': + '@mui/icons-material@5.15.18(@mui/material@5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1)': dependencies: '@babel/runtime': 7.24.5 - '@mui/material': 5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + '@mui/material': 5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) react: 19.0.0-beta-04b058868c-20240508 optionalDependencies: '@types/react': types-react@19.0.0-beta.1 - '@mui/lab@5.0.0-alpha.170(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1)': + '@mui/lab@5.0.0-alpha.170(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1)': dependencies: '@babel/runtime': 7.24.5 '@mui/base': 5.0.0-beta.40(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) - '@mui/material': 5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + '@mui/material': 5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@mui/system': 5.15.15(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@mui/types': 7.2.14(types-react@19.0.0-beta.1) '@mui/utils': 5.15.14(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) @@ -5552,11 +5555,11 @@ snapshots: '@emotion/styled': 11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@types/react': types-react@19.0.0-beta.1 - '@mui/material@5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1)': + '@mui/material@5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1)': dependencies: '@babel/runtime': 7.24.5 '@mui/base': 5.0.0-beta.40(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) - '@mui/core-downloads-tracker': 5.15.17 + '@mui/core-downloads-tracker': 5.15.18 '@mui/system': 5.15.15(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@mui/types': 7.2.14(types-react@19.0.0-beta.1) '@mui/utils': 5.15.14(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) @@ -5623,10 +5626,10 @@ snapshots: optionalDependencies: '@types/react': types-react@19.0.0-beta.1 - '@mui/x-data-grid@7.4.0(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1)': + '@mui/x-data-grid@7.4.0(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1)': dependencies: '@babel/runtime': 7.24.5 - '@mui/material': 5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + '@mui/material': 5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@mui/system': 5.15.15(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@mui/utils': 5.15.14(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) clsx: 2.1.1 @@ -5887,7 +5890,7 @@ snapshots: dependencies: '@taplo/core': 0.1.1 - '@tauri-apps/api@1.5.5': {} + '@tauri-apps/api@1.5.6': {} '@tauri-apps/cli-darwin-arm64@1.5.14': optional: true @@ -6137,14 +6140,14 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react@4.2.1(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0))': + '@vitejs/plugin-react@4.2.1(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0))': dependencies: '@babel/core': 7.24.5 '@babel/plugin-transform-react-jsx-self': 7.24.5(@babel/core@7.24.5) '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.5) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0) + vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0) transitivePeerDependencies: - supports-color @@ -7278,7 +7281,7 @@ snapshots: fraction.js@4.3.7: {} - framer-motion@11.2.0(@emotion/is-prop-valid@1.2.2)(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508): + framer-motion@11.2.4(@emotion/is-prop-valid@1.2.2)(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508): dependencies: tslib: 2.6.2 optionalDependencies: @@ -7314,9 +7317,9 @@ snapshots: functions-have-names@1.2.3: {} - generouted@1.19.4(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0)): + generouted@1.19.4(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0)): dependencies: - vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0) + vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0) gensync@1.0.0-beta.2: {} @@ -7350,6 +7353,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.7.5: + dependencies: + resolve-pkg-maps: 1.0.0 + git-raw-commits@4.0.0: dependencies: dargs: 8.1.0 @@ -8273,12 +8280,12 @@ snapshots: ms@2.1.3: {} - mui-color-input@2.0.3(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1): + mui-color-input@2.0.3(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@mui/material@5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1): dependencies: '@ctrl/tinycolor': 4.1.0 '@emotion/react': 11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) '@emotion/styled': 11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) - '@mui/material': 5.15.17(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) + '@mui/material': 5.15.18(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1))(react-dom@19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508))(react@19.0.0-beta-04b058868c-20240508)(types-react@19.0.0-beta.1) react: 19.0.0-beta-04b058868c-20240508 react-dom: 19.0.0-beta-04b058868c-20240508(react@19.0.0-beta-04b058868c-20240508) optionalDependencies: @@ -8933,7 +8940,7 @@ snapshots: sandwich-stream@2.0.2: {} - sass@1.77.1: + sass@1.77.2: dependencies: chokidar: 3.6.0 immutable: 4.3.5 @@ -9384,10 +9391,10 @@ snapshots: tslib@2.6.2: {} - tsx@4.10.2: + tsx@4.10.4: dependencies: esbuild: 0.20.2 - get-tsconfig: 4.7.4 + get-tsconfig: 4.7.5 optionalDependencies: fsevents: 2.3.3 @@ -9456,7 +9463,7 @@ snapshots: postcss-modules-local-by-default: 4.0.5(postcss@8.4.38) postcss-modules-scope: 3.2.0(postcss@8.4.38) reserved-words: 0.1.2 - sass: 1.77.1 + sass: 1.77.2 source-map-js: 1.2.0 stylus: 0.62.0 tsconfig-paths: 4.2.0 @@ -9588,37 +9595,37 @@ snapshots: esbuild: 0.19.12 monaco-editor: 0.48.0 - vite-plugin-sass-dts@1.3.22(postcss@8.4.38)(prettier@3.2.5)(sass@1.77.1)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0)): + vite-plugin-sass-dts@1.3.22(postcss@8.4.38)(prettier@3.2.5)(sass@1.77.2)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0)): dependencies: postcss: 8.4.38 postcss-js: 4.0.1(postcss@8.4.38) prettier: 3.2.5 - sass: 1.77.1 - vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0) + sass: 1.77.2 + vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0) - vite-plugin-svgr@4.2.0(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0)): + vite-plugin-svgr@4.2.0(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0)): dependencies: '@rollup/pluginutils': 5.1.0(rollup@4.17.2) '@svgr/core': 8.1.0(typescript@5.4.5) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.4.5)) - vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0) + vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0) transitivePeerDependencies: - rollup - supports-color - typescript - vite-tsconfig-paths@4.3.2(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0)): + vite-tsconfig-paths@4.3.2(typescript@5.4.5)(vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0)): dependencies: debug: 4.3.4 globrex: 0.1.2 tsconfck: 3.0.3(typescript@5.4.5) optionalDependencies: - vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0) + vite: 5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0) transitivePeerDependencies: - supports-color - typescript - vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.1)(stylus@0.62.0): + vite@5.2.11(@types/node@20.12.12)(less@4.2.0)(sass@1.77.2)(stylus@0.62.0): dependencies: esbuild: 0.20.2 postcss: 8.4.38 @@ -9627,7 +9634,7 @@ snapshots: '@types/node': 20.12.12 fsevents: 2.3.3 less: 4.2.0 - sass: 1.77.1 + sass: 1.77.2 stylus: 0.62.0 void-elements@3.1.0: {} diff --git a/clash-verge-rev/src/assets/image/component/match_case.svg b/clash-verge-rev/src/assets/image/component/match_case.svg new file mode 100644 index 0000000000..cb59388f4c --- /dev/null +++ b/clash-verge-rev/src/assets/image/component/match_case.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/clash-verge-rev/src/assets/image/component/match_whole_word.svg b/clash-verge-rev/src/assets/image/component/match_whole_word.svg new file mode 100644 index 0000000000..5701ad4802 --- /dev/null +++ b/clash-verge-rev/src/assets/image/component/match_whole_word.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/clash-verge-rev/src/assets/image/component/use_regular_expression.svg b/clash-verge-rev/src/assets/image/component/use_regular_expression.svg new file mode 100644 index 0000000000..31165959c3 --- /dev/null +++ b/clash-verge-rev/src/assets/image/component/use_regular_expression.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/clash-verge-rev/src/components/base/base-search-box.tsx b/clash-verge-rev/src/components/base/base-search-box.tsx new file mode 100644 index 0000000000..19ede1107e --- /dev/null +++ b/clash-verge-rev/src/components/base/base-search-box.tsx @@ -0,0 +1,143 @@ +import { Box, SvgIcon, TextField, Theme, styled } from "@mui/material"; +import Tooltip from "@mui/material/Tooltip"; +import { ChangeEvent, useState } from "react"; + +import { useTranslation } from "react-i18next"; +import matchCaseIcon from "@/assets/image/component/match_case.svg?react"; +import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react"; +import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react"; + +type SearchProps = { + placeholder?: string; + onSearch: ( + match: (content: string) => boolean, + state: { + text: string; + matchCase: boolean; + matchWholeWord: boolean; + useRegularExpression: boolean; + } + ) => void; +}; + +export const BaseSearchBox = styled((props: SearchProps) => { + const { t } = useTranslation(); + const [matchCase, setMatchCase] = useState(true); + const [matchWholeWord, setMatchWholeWord] = useState(false); + const [useRegularExpression, setUseRegularExpression] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + const iconStyle = { + style: { + height: "24px", + width: "24px", + cursor: "pointer", + } as React.CSSProperties, + inheritViewBox: true, + }; + const active = "var(--primary-main)"; + + const onChange = (e: ChangeEvent) => { + props.onSearch( + (content) => doSearch([content], e.target?.value ?? "").length > 0, + { + text: e.target?.value ?? "", + matchCase, + matchWholeWord, + useRegularExpression, + } + ); + }; + + const doSearch = (searchList: string[], searchItem: string) => { + setErrorMessage(""); + return searchList.filter((item) => { + try { + let searchItemCopy = searchItem; + if (!matchCase) { + item = item.toLowerCase(); + searchItemCopy = searchItemCopy.toLowerCase(); + } + if (matchWholeWord) { + const regex = new RegExp(`\\b${searchItemCopy}\\b`); + if (useRegularExpression) { + const regexWithOptions = new RegExp(searchItemCopy); + return regexWithOptions.test(item) && regex.test(item); + } else { + return regex.test(item); + } + } else if (useRegularExpression) { + const regex = new RegExp(searchItemCopy); + return regex.test(item); + } else { + return item.includes(searchItemCopy); + } + } catch (err) { + setErrorMessage(`${err}`); + } + }); + }; + + return ( + + + +
+ { + setMatchCase(!matchCase); + }} + /> +
+
+ +
+ { + setMatchWholeWord(!matchWholeWord); + }} + /> +
+
+ +
+ { + setUseRegularExpression(!useRegularExpression); + }} + />{" "} +
+
+ + ), + }} + /> +
+ ); +})(({ theme }) => ({ + "& .MuiInputBase-root": { + background: theme.palette.mode === "light" ? "#fff" : undefined, + }, +})); diff --git a/clash-verge-rev/src/components/proxy/proxy-item-mini.tsx b/clash-verge-rev/src/components/proxy/proxy-item-mini.tsx index d605c8df9a..ded452bb31 100644 --- a/clash-verge-rev/src/components/proxy/proxy-item-mini.tsx +++ b/clash-verge-rev/src/components/proxy/proxy-item-mini.tsx @@ -5,6 +5,7 @@ import { alpha, Box, ListItemButton, styled, Typography } from "@mui/material"; import { BaseLoading } from "@/components/base"; import delayManager from "@/services/delay"; import { useVerge } from "@/hooks/use-verge"; +import { useTranslation } from "react-i18next"; interface Props { group: IProxyGroupItem; @@ -18,6 +19,8 @@ interface Props { export const ProxyItemMini = (props: Props) => { const { group, proxy, selected, showType = true, onClick } = props; + const { t } = useTranslation(); + // -1/<=0 为 不显示 // -2 为 loading const [delay, setDelay] = useState(-1); @@ -209,10 +212,14 @@ export const ProxyItemMini = (props: Props) => { /> )} - {group.fixed && group.fixed === proxy.name && ( // 展示fixed状态 - + 📌 )} diff --git a/clash-verge-rev/src/components/setting/mods/config-viewer.tsx b/clash-verge-rev/src/components/setting/mods/config-viewer.tsx index d758a0febe..8d092d061d 100644 --- a/clash-verge-rev/src/components/setting/mods/config-viewer.tsx +++ b/clash-verge-rev/src/components/setting/mods/config-viewer.tsx @@ -78,7 +78,7 @@ export const ConfigViewer = forwardRef((props, ref) => {
diff --git a/clash-verge-rev/src/components/setting/setting-verge.tsx b/clash-verge-rev/src/components/setting/setting-verge.tsx index 1266d505b3..53c30144a8 100644 --- a/clash-verge-rev/src/components/setting/setting-verge.tsx +++ b/clash-verge-rev/src/components/setting/setting-verge.tsx @@ -93,7 +93,7 @@ const SettingVerge = ({ onError }: Props) => { onChange={(e) => onChangeData({ language: e })} onGuard={(e) => patchVerge({ language: e })} > - div": { py: "7.5px" } }}> 中文 English Русский diff --git a/clash-verge-rev/src/locales/en.json b/clash-verge-rev/src/locales/en.json index 209be7976a..17b5c4f524 100644 --- a/clash-verge-rev/src/locales/en.json +++ b/clash-verge-rev/src/locales/en.json @@ -60,6 +60,7 @@ "Sort by delay": "Sort by delay", "Sort by name": "Sort by name", "Delay check URL": "Delay check URL", + "Delay check to cancel fixed": "Delay check to cancel fixed", "Proxy basic": "Proxy basic", "Proxy detail": "Proxy detail", "Filter": "Filter", @@ -214,6 +215,8 @@ "System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode", "Information: Please make sure that the Clash Verge Service is installed and enabled": "Information: Please make sure that the Clash Verge Service is installed and enabled", + "Match Case": "Match Case", + "Match Whole Word": "Match Whole Word", "Use Regular Expression": "Use Regular Expression", "External Controller Address Modified": "External Controller Address Modified", diff --git a/clash-verge-rev/src/locales/ru.json b/clash-verge-rev/src/locales/ru.json index d9f9dcd368..fafd7c840e 100644 --- a/clash-verge-rev/src/locales/ru.json +++ b/clash-verge-rev/src/locales/ru.json @@ -60,6 +60,7 @@ "Sort by delay": "Сортировать по задержке", "Sort by name": "Сортировать по названию", "Delay check URL": "URL проверки задержки", + "Delay check to cancel fixed": "Проверка задержки для отмены фиксированного", "Proxy basic": "Резюме о прокси", "Proxy detail": "Подробности о прокси", "Filter": "Фильтр", @@ -214,7 +215,9 @@ "System and Mixed Can Only be Used in Service Mode": "Система и смешанные могут использоваться только в сервисном режиме", "Information: Please make sure that the Clash Verge Service is installed and enabled": "Информация: Пожалуйста, убедитесь, что сервис Clash Verge Service установлен и включен", - "Use Regular Expression": "Использование регулярных выражений", + "Match Case": "Учитывать регистр", + "Match Whole Word": "Полное совпадение слова", + "Use Regular Expression": "Использовать регулярные выражения", "External Controller Address Modified": "Изменен адрес внешнего контроллера", "Clash Port Modified": "Clash порт изменен", diff --git a/clash-verge-rev/src/locales/zh.json b/clash-verge-rev/src/locales/zh.json index 24feb0d969..19a7e7b046 100644 --- a/clash-verge-rev/src/locales/zh.json +++ b/clash-verge-rev/src/locales/zh.json @@ -60,6 +60,7 @@ "Sort by delay": "按延迟排序", "Sort by name": "按名称排序", "Delay check URL": "延迟测试链接", + "Delay check to cancel fixed": "进行延迟测试,以取消固定", "Proxy basic": "隐藏节点细节", "Proxy detail": "展示节点细节", "Filter": "过滤节点", @@ -214,6 +215,8 @@ "System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服务模式下使用", "Information: Please make sure that the Clash Verge Service is installed and enabled": "提示信息: 请确保 Clash Verge Service 已安装并启用", + "Match Case": "区分大小写", + "Match Whole Word": "全字匹配", "Use Regular Expression": "使用正则表达式", "External Controller Address Modified": "外部控制器监听地址已修改", diff --git a/clash-verge-rev/src/pages/connections.tsx b/clash-verge-rev/src/pages/connections.tsx index bffcffa995..9bdd64b624 100644 --- a/clash-verge-rev/src/pages/connections.tsx +++ b/clash-verge-rev/src/pages/connections.tsx @@ -18,7 +18,7 @@ import { } from "@/components/connection/connection-detail"; import parseTraffic from "@/utils/parse-traffic"; import { useCustomTheme } from "@/components/layout/use-custom-theme"; -import { BaseStyledTextField } from "@/components/base/base-styled-text-field"; +import { BaseSearchBox } from "@/components/base/base-search-box"; const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] }; @@ -29,7 +29,7 @@ const ConnectionsPage = () => { const { clashInfo } = useClashInfo(); const { theme } = useCustomTheme(); const isDark = theme.palette.mode === "dark"; - const [filterText, setFilterText] = useState(""); + const [match, setMatch] = useState(() => (_: string) => true); const [curOrderOpt, setOrderOpt] = useState("Default"); const [connData, setConnData] = useState(initConn); @@ -52,7 +52,7 @@ const ConnectionsPage = () => { const [filterConn, download, upload] = useMemo(() => { const orderFunc = orderOpts[curOrderOpt]; let connections = connData.connections.filter((conn) => - (conn.metadata.host || conn.metadata.destinationIP)?.includes(filterText) + match(conn.metadata.host || conn.metadata.destinationIP || "") ); if (orderFunc) connections = orderFunc(connections); @@ -63,7 +63,7 @@ const ConnectionsPage = () => { upload += x.upload; }); return [connections, download, upload]; - }, [connData, filterText, curOrderOpt]); + }, [connData, match, curOrderOpt]); const { connect, disconnect } = useWebsocket( (event) => { @@ -182,11 +182,7 @@ const ConnectionsPage = () => { ))} )} - - setFilterText(e.target.value)} - /> + setMatch(() => match)} /> ) => { return ( @@ -46,35 +46,15 @@ const LogPage = () => { const { theme } = useCustomTheme(); const isDark = theme.palette.mode === "dark"; const [logState, setLogState] = useState("all"); - const [filterText, setFilterText] = useState(""); - const [useRegexSearch, setUseRegexSearch] = useState(true); - const [hasInputError, setInputError] = useState(false); - const [inputHelperText, setInputHelperText] = useState(""); + const [match, setMatch] = useState(() => (_: string) => true); + const filterLogs = useMemo(() => { - setInputHelperText(""); - setInputError(false); - if (useRegexSearch) { - try { - const regex = new RegExp(filterText); - return logData.filter((data) => { - return ( - regex.test(data.payload) && - (logState === "all" ? true : data.type.includes(logState)) - ); - }); - } catch (err: any) { - setInputHelperText(err.message.substring(0, 60)); - setInputError(true); - return logData; - } - } - return logData.filter((data) => { - return ( - data.payload.includes(filterText) && - (logState === "all" ? true : data.type.includes(logState)) - ); - }); - }, [logData, logState, filterText]); + return logData + .filter((data) => + logState === "all" ? true : data.type.includes(logState) + ) + .filter((data) => match(data.payload)); + }, [logData, logState, match]); return ( { WARN ERROR - - setFilterText(e.target.value)} - helperText={inputHelperText} - placeholder={t("Filter conditions")} - InputProps={{ - sx: { pr: 1 }, - endAdornment: ( - setUseRegexSearch(!useRegexSearch)} - > - .* - - ), - }} - /> + setMatch(() => match)} /> { const { t } = useTranslation(); const { data = [] } = useSWR("getRules", getRules); const { theme } = useCustomTheme(); const isDark = theme.palette.mode === "dark"; - const [filterText, setFilterText] = useState(""); + const [match, setMatch] = useState(() => (_: string) => true); const rules = useMemo(() => { - return data.filter((each) => each.payload.includes(filterText)); - }, [data, filterText]); + return data.filter((item) => match(item.payload)); + }, [data, match]); return ( { alignItems: "center", }} > - setFilterText(e.target.value)} - /> + setMatch(() => match)} /> 0 && b.cmgr.CountConnection(cmgr.ConnectionTypeActive) >= b.cfg.MaxConnection { + b.l.Warnf("Relay %s active connection count exceed limit", remote.Label) + c.Close() + } + clonedRemote := remote.Clone() rc, err := handshakeF(clonedRemote) if err != nil { return err } + b.l.Infof("RelayTCPConn from %s to %s", c.LocalAddr(), remote.Address) relayConn := conn.NewRelayConn( b.cfg.Label, c, rc, conn.WithHandshakeDuration(clonedRemote.HandShakeDuration)) diff --git a/echo/pkg/node_metric/utils.go b/echo/pkg/node_metric/utils.go index 221efbf5d5..419a65c700 100644 --- a/echo/pkg/node_metric/utils.go +++ b/echo/pkg/node_metric/utils.go @@ -2,16 +2,14 @@ package node_metric import "regexp" -var ( - // parse disk name from device path,such as: - // e.g. /dev/disk1s1 -> disk1 - // e.g. /dev/disk1s2 -> disk1 - // e.g. ntfs://disk1s1 -> disk1 - // e.g. ntfs://disk1s2 -> disk1 - // e.g. /dev/sda1 -> sda - // e.g. /dev/sda2 -> sda - diskNameRegex = regexp.MustCompile(`/dev/disk(\d+)|ntfs://disk(\d+)|/dev/sd[a-zA-Z]`) -) +// parse disk name from device path,such as: +// e.g. /dev/disk1s1 -> disk1 +// e.g. /dev/disk1s2 -> disk1 +// e.g. ntfs://disk1s1 -> disk1 +// e.g. ntfs://disk1s2 -> disk1 +// e.g. /dev/sda1 -> sda +// e.g. /dev/sda2 -> sda +var diskNameRegex = regexp.MustCompile(`/dev/disk(\d+)|ntfs://disk(\d+)|/dev/sd[a-zA-Z]`) func getDiskName(devicePath string) string { matches := diskNameRegex.FindStringSubmatch(devicePath) diff --git a/echo/test/echo/echo.go b/echo/test/echo/echo.go index e0f8224db9..c7c0eacbe8 100644 --- a/echo/test/echo/echo.go +++ b/echo/test/echo/echo.go @@ -8,25 +8,28 @@ import ( "os" "strconv" "time" + + "go.uber.org/zap" ) func echo(conn net.Conn) { + logger := zap.S().Named(("echo-test-server")) defer conn.Close() defer fmt.Println("conn closed", conn.RemoteAddr().String()) buf := make([]byte, 10) for { i, err := conn.Read(buf) if err == io.EOF { - fmt.Println("read eof") + logger.Info("conn closed,read eof ", conn.RemoteAddr().String()) return } if err != nil { - fmt.Println(err.Error()) + logger.Error(err.Error()) return } _, err = conn.Write(buf[:i]) if err != nil { - fmt.Println(err.Error()) + logger.Error(err.Error()) return } } @@ -114,6 +117,33 @@ func SendTcpMsg(msg []byte, address string) []byte { return buf[:n] } +func EchoTcpMsgLong(msg []byte, sleepTime time.Duration, address string) error { + logger := zap.S() + buf := make([]byte, len(msg)) + conn, err := net.Dial("tcp", address) + if err != nil { + return err + } + defer conn.Close() + logger.Infof("conn start %s %s", conn.RemoteAddr().String(), conn.LocalAddr().String()) + for i := 0; i < 10; i++ { + if _, err := conn.Write(msg); err != nil { + return err + } + n, err := conn.Read(buf) + if err != nil { + return err + } + if string(buf[:n]) != string(msg) { + return fmt.Errorf("msg not equal") + } + // to fake a long connection + time.Sleep(sleepTime) + } + logger.Infof("conn closed %s %s", conn.RemoteAddr().String(), conn.LocalAddr().String()) + return nil +} + func SendUdpMsg(msg []byte, address string) []byte { conn, err := net.Dial("udp", address) if err != nil { diff --git a/echo/test/relay_test.go b/echo/test/relay_test.go index d271f0678f..dc0521c8e7 100644 --- a/echo/test/relay_test.go +++ b/echo/test/relay_test.go @@ -23,7 +23,8 @@ const ( ECHO_PORT = 9002 ECHO_SERVER = "0.0.0.0:9002" - RAW_LISTEN = "0.0.0.0:1234" + RAW_LISTEN = "0.0.0.0:1234" + RAW_LISTEN_WITH_MAX_CONNECTION = "0.0.0.0:2234" WS_LISTEN = "0.0.0.0:1235" WS_REMOTE = "ws://0.0.0.0:2000" @@ -61,6 +62,15 @@ func init() { UDPRemotes: []string{ECHO_SERVER}, TransportType: constant.RelayTypeRaw, }, + // raw cfg with max connection + { + Listen: RAW_LISTEN_WITH_MAX_CONNECTION, + ListenType: constant.RelayTypeRaw, + TCPRemotes: []string{ECHO_SERVER}, + UDPRemotes: []string{ECHO_SERVER}, + TransportType: constant.RelayTypeRaw, + MaxConnection: 1, + }, // ws { @@ -155,6 +165,24 @@ func TestRelayOverRaw(t *testing.T) { // t.Log("test udp done!") } +func TestRelayWithMaxConnectionCount(t *testing.T) { + msg := []byte("hello") + + // first connection will be accepted + go func() { + err := echo.EchoTcpMsgLong(msg, time.Second, RAW_LISTEN_WITH_MAX_CONNECTION) + if err != nil { + t.Error(err) + } + }() + + // second connection will be rejected + time.Sleep(time.Second) // wait for first connection + if err := echo.EchoTcpMsgLong(msg, time.Second, RAW_LISTEN_WITH_MAX_CONNECTION); err == nil { + t.Fatal("need error here") + } +} + func TestRelayWithDeadline(t *testing.T) { logger, _ := zap.NewDevelopment() msg := []byte("hello") diff --git a/clash-meta/hub/updater/updater.go b/mihomo/component/updater/update_core.go similarity index 99% rename from clash-meta/hub/updater/updater.go rename to mihomo/component/updater/update_core.go index df5da3f4d1..0070fbb110 100644 --- a/clash-meta/hub/updater/updater.go +++ b/mihomo/component/updater/update_core.go @@ -67,7 +67,7 @@ func (e *updateError) Error() string { // Update performs the auto-updater. It returns an error if the updater failed. // If firstRun is true, it assumes the configuration file doesn't exist. -func Update(execPath string) (err error) { +func UpdateCore(execPath string) (err error) { mu.Lock() defer mu.Unlock() diff --git a/mihomo/config/update_geo.go b/mihomo/component/updater/update_geo.go similarity index 52% rename from mihomo/config/update_geo.go rename to mihomo/component/updater/update_geo.go index 43cac25c8d..a98d94dc7a 100644 --- a/mihomo/config/update_geo.go +++ b/mihomo/component/updater/update_geo.go @@ -1,18 +1,29 @@ -package config +package updater import ( + "errors" "fmt" + "os" "runtime" + "sync" + "time" + "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/component/geodata" _ "github.com/metacubex/mihomo/component/geodata/standard" "github.com/metacubex/mihomo/component/mmdb" C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" "github.com/oschwald/maxminddb-golang" ) -func UpdateGeoDatabases() error { +var ( + updateGeoMux sync.Mutex + UpdatingGeo atomic.Bool +) + +func updateGeoDatabases() error { defer runtime.GC() geoLoader, err := geodata.GetGeoDataLoader("standard") if err != nil { @@ -88,3 +99,82 @@ func UpdateGeoDatabases() error { return nil } + +func UpdateGeoDatabases() error { + log.Infoln("[GEO] Start updating GEO database") + + updateGeoMux.Lock() + + if UpdatingGeo.Load() { + updateGeoMux.Unlock() + return errors.New("GEO database is updating, skip") + } + + UpdatingGeo.Store(true) + updateGeoMux.Unlock() + + defer func() { + UpdatingGeo.Store(false) + }() + + log.Infoln("[GEO] Updating GEO database") + + if err := updateGeoDatabases(); err != nil { + log.Errorln("[GEO] update GEO database error: %s", err.Error()) + return err + } + + return nil +} + +func getUpdateTime() (err error, time time.Time) { + var fileInfo os.FileInfo + if C.GeodataMode { + fileInfo, err = os.Stat(C.Path.GeoIP()) + if err != nil { + return err, time + } + } else { + fileInfo, err = os.Stat(C.Path.MMDB()) + if err != nil { + return err, time + } + } + + return nil, fileInfo.ModTime() +} + +func RegisterGeoUpdater() { + if C.GeoUpdateInterval <= 0 { + log.Errorln("[GEO] Invalid update interval: %d", C.GeoUpdateInterval) + return + } + + ticker := time.NewTicker(time.Duration(C.GeoUpdateInterval) * time.Hour) + defer ticker.Stop() + + log.Infoln("[GEO] update GEO database every %d hours", C.GeoUpdateInterval) + go func() { + err, lastUpdate := getUpdateTime() + if err != nil { + log.Errorln("[GEO] Get GEO database update time error: %s", err.Error()) + return + } + + log.Infoln("[GEO] last update time %s", lastUpdate) + if lastUpdate.Add(time.Duration(C.GeoUpdateInterval) * time.Hour).Before(time.Now()) { + log.Infoln("[GEO] Database has not been updated for %v, update now", time.Duration(C.GeoUpdateInterval)*time.Hour) + if err := UpdateGeoDatabases(); err != nil { + log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) + return + } + } + + for range ticker.C { + if err := UpdateGeoDatabases(); err != nil { + log.Errorln("[GEO] Failed to update GEO database: %s", err.Error()) + return + } + } + }() +} diff --git a/mihomo/config/update_ui.go b/mihomo/component/updater/update_ui.go similarity index 97% rename from mihomo/config/update_ui.go rename to mihomo/component/updater/update_ui.go index cff1d6d7bd..85452ba550 100644 --- a/mihomo/config/update_ui.go +++ b/mihomo/component/updater/update_ui.go @@ -1,4 +1,4 @@ -package config +package updater import ( "archive/zip" @@ -29,7 +29,7 @@ func UpdateUI() error { xdMutex.Lock() defer xdMutex.Unlock() - err := prepare() + err := prepare_ui() if err != nil { return err } @@ -64,7 +64,7 @@ func UpdateUI() error { return nil } -func prepare() error { +func prepare_ui() error { if ExternalUIPath == "" || ExternalUIURL == "" { return ErrIncompleteConf } diff --git a/clash-meta/hub/updater/limitedreader.go b/mihomo/component/updater/utils.go similarity index 70% rename from clash-meta/hub/updater/limitedreader.go rename to mihomo/component/updater/utils.go index c31db601d0..0eecfc6cdc 100644 --- a/clash-meta/hub/updater/limitedreader.go +++ b/mihomo/component/updater/utils.go @@ -1,12 +1,35 @@ package updater import ( + "context" "fmt" "io" + "net/http" + "os" + "time" + + mihomoHttp "github.com/metacubex/mihomo/component/http" + C "github.com/metacubex/mihomo/constant" "golang.org/x/exp/constraints" ) +func downloadForBytes(url string) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return io.ReadAll(resp.Body) +} + +func saveFile(bytes []byte, path string) error { + return os.WriteFile(path, bytes, 0o644) +} + // LimitReachedError records the limit and the operation that caused it. type LimitReachedError struct { Limit int64 diff --git a/mihomo/config/config.go b/mihomo/config/config.go index 311fd2e285..9bc0afc824 100644 --- a/mihomo/config/config.go +++ b/mihomo/config/config.go @@ -28,6 +28,7 @@ import ( SNIFF "github.com/metacubex/mihomo/component/sniffer" tlsC "github.com/metacubex/mihomo/component/tls" "github.com/metacubex/mihomo/component/trie" + "github.com/metacubex/mihomo/component/updater" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" providerTypes "github.com/metacubex/mihomo/constant/provider" @@ -640,28 +641,28 @@ func parseGeneral(cfg *RawConfig) (*General, error) { N.KeepAliveInterval = time.Duration(cfg.KeepAliveInterval) * time.Second } - ExternalUIPath = cfg.ExternalUI + updater.ExternalUIPath = cfg.ExternalUI // checkout externalUI exist - if ExternalUIPath != "" { - ExternalUIPath = C.Path.Resolve(ExternalUIPath) - if _, err := os.Stat(ExternalUIPath); os.IsNotExist(err) { + if updater.ExternalUIPath != "" { + updater.ExternalUIPath = C.Path.Resolve(updater.ExternalUIPath) + if _, err := os.Stat(updater.ExternalUIPath); os.IsNotExist(err) { defaultUIpath := path.Join(C.Path.HomeDir(), "ui") - log.Warnln("external-ui: %s does not exist, creating folder in %s", ExternalUIPath, defaultUIpath) + log.Warnln("external-ui: %s does not exist, creating folder in %s", updater.ExternalUIPath, defaultUIpath) if err := os.MkdirAll(defaultUIpath, os.ModePerm); err != nil { return nil, err } - ExternalUIPath = defaultUIpath + updater.ExternalUIPath = defaultUIpath cfg.ExternalUI = defaultUIpath } } // checkout UIpath/name exist if cfg.ExternalUIName != "" { - ExternalUIName = cfg.ExternalUIName + updater.ExternalUIName = cfg.ExternalUIName } else { - ExternalUIFolder = ExternalUIPath + updater.ExternalUIFolder = updater.ExternalUIPath } if cfg.ExternalUIURL != "" { - ExternalUIURL = cfg.ExternalUIURL + updater.ExternalUIURL = cfg.ExternalUIURL } cfg.Tun.RedirectToTun = cfg.EBpf.RedirectToTun diff --git a/mihomo/config/utils.go b/mihomo/config/utils.go index 66bf3441f2..f87fb34131 100644 --- a/mihomo/config/utils.go +++ b/mihomo/config/utils.go @@ -1,38 +1,15 @@ package config import ( - "context" "fmt" - "io" "net" - "net/http" "net/netip" - "os" "strings" - "time" "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/structure" - mihomoHttp "github.com/metacubex/mihomo/component/http" - C "github.com/metacubex/mihomo/constant" ) -func downloadForBytes(url string) ([]byte, error) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) - defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, url, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - return io.ReadAll(resp.Body) -} - -func saveFile(bytes []byte, path string) error { - return os.WriteFile(path, bytes, 0o644) -} - func trimArr(arr []string) (r []string) { for _, e := range arr { r = append(r, strings.Trim(e, " ")) diff --git a/mihomo/hub/route/configs.go b/mihomo/hub/route/configs.go index 653e43519b..47fd26e0a1 100644 --- a/mihomo/hub/route/configs.go +++ b/mihomo/hub/route/configs.go @@ -4,11 +4,11 @@ import ( "net/http" "net/netip" "path/filepath" - "sync" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/hub/executor" @@ -21,11 +21,6 @@ import ( "github.com/go-chi/render" ) -var ( - updateGeoMux sync.Mutex - updatingGeo = false -) - func configRouter() http.Handler { r := chi.NewRouter() r.Get("/", getConfigs) @@ -369,30 +364,20 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) { } func updateGeoDatabases(w http.ResponseWriter, r *http.Request) { - updateGeoMux.Lock() - - if updatingGeo { - updateGeoMux.Unlock() + if updater.UpdatingGeo.Load() { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("updating...")) return } - updatingGeo = true - updateGeoMux.Unlock() + err := updater.UpdateGeoDatabases() + if err != nil { + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError(err.Error())) + return + } go func() { - defer func() { - updatingGeo = false - }() - - log.Warnln("[REST-API] updating GEO databases...") - - if err := config.UpdateGeoDatabases(); err != nil { - log.Errorln("[REST-API] update GEO databases failed: %v", err) - return - } - cfg, err := executor.ParseWithPath(C.Path.Config()) if err != nil { log.Errorln("[REST-API] update GEO databases failed: %v", err) diff --git a/mihomo/hub/route/upgrade.go b/mihomo/hub/route/upgrade.go index ea371798f3..db00af5c8a 100644 --- a/mihomo/hub/route/upgrade.go +++ b/mihomo/hub/route/upgrade.go @@ -6,8 +6,7 @@ import ( "net/http" "os" - "github.com/metacubex/mihomo/config" - "github.com/metacubex/mihomo/hub/updater" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/log" "github.com/go-chi/chi/v5" @@ -18,6 +17,7 @@ func upgradeRouter() http.Handler { r := chi.NewRouter() r.Post("/", upgradeCore) r.Post("/ui", updateUI) + r.Post("/geo", updateGeoDatabases) return r } @@ -31,7 +31,7 @@ func upgradeCore(w http.ResponseWriter, r *http.Request) { return } - err = updater.Update(execPath) + err = updater.UpdateCore(execPath) if err != nil { log.Warnln("%s", err) render.Status(r, http.StatusInternalServerError) @@ -48,9 +48,9 @@ func upgradeCore(w http.ResponseWriter, r *http.Request) { } func updateUI(w http.ResponseWriter, r *http.Request) { - err := config.UpdateUI() + err := updater.UpdateUI() if err != nil { - if errors.Is(err, config.ErrIncompleteConf) { + if errors.Is(err, updater.ErrIncompleteConf) { log.Warnln("%s", err) render.Status(r, http.StatusNotImplemented) render.JSON(w, r, newError(fmt.Sprintf("%s", err))) diff --git a/mihomo/main.go b/mihomo/main.go index afe9cfd244..1d16f8bfc7 100644 --- a/mihomo/main.go +++ b/mihomo/main.go @@ -8,10 +8,9 @@ import ( "path/filepath" "runtime" "strings" - "sync" "syscall" - "time" + "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" @@ -32,8 +31,6 @@ var ( externalController string externalControllerUnix string secret string - updateGeoMux sync.Mutex - updatingGeo = false ) func init() { @@ -116,14 +113,7 @@ func main() { } if C.GeoAutoUpdate { - ticker := time.NewTicker(time.Duration(C.GeoUpdateInterval) * time.Hour) - - log.Infoln("[GEO] Start update GEO database every %d hours", C.GeoUpdateInterval) - go func() { - for range ticker.C { - updateGeoDatabases() - } - }() + updater.RegisterGeoUpdater() } defer executor.Shutdown() @@ -145,39 +135,3 @@ func main() { } } } - -func updateGeoDatabases() { - log.Infoln("[GEO] Start updating GEO database") - updateGeoMux.Lock() - - if updatingGeo { - updateGeoMux.Unlock() - log.Infoln("[GEO] GEO database is updating, skip") - return - } - - updatingGeo = true - updateGeoMux.Unlock() - - go func() { - defer func() { - updatingGeo = false - }() - - log.Infoln("[GEO] Updating GEO database") - - if err := config.UpdateGeoDatabases(); err != nil { - log.Errorln("[GEO] update GEO database error: %s", err.Error()) - return - } - - cfg, err := executor.ParseWithPath(C.Path.Config()) - if err != nil { - log.Errorln("[GEO] update GEO database failed: %s", err.Error()) - return - } - - log.Infoln("[GEO] Update GEO database success, apply new config") - executor.ApplyConfig(cfg, false) - }() -} diff --git a/openwrt-packages/luci-app-filebrowser/po/sv/filebrowser.po b/openwrt-packages/luci-app-filebrowser/po/sv/filebrowser.po new file mode 100644 index 0000000000..f9c53502d8 --- /dev/null +++ b/openwrt-packages/luci-app-filebrowser/po/sv/filebrowser.po @@ -0,0 +1,20 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2024-05-12 20:34+0000\n" +"Last-Translator: Daniel Nilsson \n" +"Language-Team: Swedish \n" +"Language: sv\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.5.4\n" + +#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16 +#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3 +msgid "File Browser" +msgstr "Filbläddrare" + +#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3 +msgid "Grant access to File Browser" +msgstr "Ge åtkomst till Filbläddrare" diff --git a/v2rayng/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/v2rayng/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt index 8d58cdcee6..3662b8bdf7 100644 --- a/v2rayng/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt +++ b/v2rayng/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/MainRecyclerAdapter.kt @@ -67,7 +67,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter + fun clearAllTestDelayResults(keys: List?) { + keys?.forEach { key -> decodeServerAffiliationInfo(key)?.let { aff -> aff.testDelayMillis = 0 serverAffStorage?.encode(key, Gson().toJson(aff)) @@ -172,7 +172,7 @@ object MmkvManager { fun removeInvalidServer() { serverAffStorage?.allKeys()?.forEach { key -> decodeServerAffiliationInfo(key)?.let { aff -> - if (aff.testDelayMillis <= 0L) { + if (aff.testDelayMillis < 0L) { removeServer(key) } } diff --git a/v2rayng/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt b/v2rayng/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt index a57f60d610..8af4a1bf77 100644 --- a/v2rayng/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt +++ b/v2rayng/V2rayNG/app/src/main/kotlin/com/v2ray/ang/viewmodel/MainViewModel.kt @@ -1,7 +1,11 @@ package com.v2ray.ang.viewmodel import android.app.Application -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.IntentFilter import android.os.Build import android.util.Log import android.view.LayoutInflater @@ -17,12 +21,23 @@ import com.v2ray.ang.AppConfig import com.v2ray.ang.AppConfig.ANG_PACKAGE import com.v2ray.ang.R import com.v2ray.ang.databinding.DialogConfigFilterBinding -import com.v2ray.ang.dto.* +import com.v2ray.ang.dto.EConfigType +import com.v2ray.ang.dto.ServerConfig +import com.v2ray.ang.dto.ServersCache +import com.v2ray.ang.dto.V2rayConfig import com.v2ray.ang.extension.toast -import com.v2ray.ang.util.* +import com.v2ray.ang.util.MessageUtil +import com.v2ray.ang.util.MmkvManager import com.v2ray.ang.util.MmkvManager.KEY_ANG_CONFIGS -import kotlinx.coroutines.* -import java.util.* +import com.v2ray.ang.util.SpeedtestUtil +import com.v2ray.ang.util.Utils +import com.v2ray.ang.util.V2rayConfigUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch +import java.util.Collections class MainViewModel(application: Application) : AndroidViewModel(application) { private val mainStorage by lazy { @@ -130,7 +145,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { fun testAllTcping() { tcpingTestScope.coroutineContext[Job]?.cancelChildren() SpeedtestUtil.closeAllTcpSockets() - MmkvManager.clearAllTestDelayResults() + MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) updateListAction.value = -1 // update all getApplication().toast(R.string.connection_test_testing) @@ -153,7 +168,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { fun testAllRealPing() { MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG_CANCEL, "") - MmkvManager.clearAllTestDelayResults() + MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) updateListAction.value = -1 // update all val serversCopy = serversCache.toList() // Create a copy of the list diff --git a/yass/src/cli/cli_connection.cpp b/yass/src/cli/cli_connection.cpp index aae9f7e276..9ceeef5462 100644 --- a/yass/src/cli/cli_connection.cpp +++ b/yass/src/cli/cli_connection.cpp @@ -114,24 +114,7 @@ bool DataFrameSource::Send(absl::string_view frame_header, size_t payload_length } const int64_t result = connection_->OnReadyToSend(concatenated); - // Write encountered error. - if (result < 0) { - connection_->OnConnectionError(http2::adapter::Http2VisitorInterface::ConnectionError::kSendError); - return false; - } - - // Write blocked. - if (result == 0) { - connection_->blocked_stream_ = stream_id_; - return false; - } - - if (static_cast(result) < concatenated.size()) { - // Probably need to handle this better within this test class. - QUICHE_LOG(DFATAL) << "DATA frame not fully flushed. Connection will be corrupt!"; - connection_->OnConnectionError(http2::adapter::Http2VisitorInterface::ConnectionError::kSendError); - return false; - } + DCHECK_EQ(static_cast(result), concatenated.size()); if (!payload_length) { return true; @@ -189,12 +172,6 @@ void CliConnection::start() { upstream_writable_ = false; downstream_readable_ = true; - int ret = resolver_.Init(); - if (ret < 0) { - LOG(WARNING) << "resolver initialize failure"; - close(); - return; - } ReadMethodSelect(); } @@ -206,27 +183,12 @@ void CliConnection::close() { << " disconnected with client at stage: " << CliConnection::state_to_str(CurrentState()); asio::error_code ec; closed_ = true; - resolver_.Cancel(); + resolver_.Reset(); downlink_->close(ec); if (ec) { VLOG(1) << "close() error: " << ec; } if (channel_) { -#ifdef HAVE_QUICHE - if (adapter_) { - if (data_frame_) { - data_frame_->set_last_frame(true); - adapter_->ResumeStream(stream_id_); - SendIfNotProcessing(); - data_frame_ = nullptr; - stream_id_ = 0; - } - adapter_->SubmitGoAway(0, http2::adapter::Http2ErrorCode::HTTP2_NO_ERROR, ""sv); - DCHECK(adapter_->want_write()); - SendIfNotProcessing(); - WriteUpstreamInPipe(); - } -#endif channel_->close(); } on_disconnect(); @@ -234,6 +196,7 @@ void CliConnection::close() { #ifdef HAVE_QUICHE void CliConnection::SendIfNotProcessing() { + DCHECK(!http2_in_recv_callback_); if (!processing_responses_) { processing_responses_ = true; adapter_->Send(); @@ -291,8 +254,6 @@ bool CliConnection::OnEndStream(StreamId stream_id) { stream_id_ = 0; adapter_->SubmitGoAway(0, http2::adapter::Http2ErrorCode::HTTP2_NO_ERROR, ""sv); DCHECK(adapter_->want_write()); - SendIfNotProcessing(); - WriteUpstreamInPipe(); } return true; } @@ -310,8 +271,12 @@ bool CliConnection::OnCloseStream(StreamId stream_id, http2::adapter::Http2Error return true; } -void CliConnection::OnConnectionError(ConnectionError /*error*/) { - disconnected(asio::error::connection_aborted); +void CliConnection::OnConnectionError(ConnectionError error) { + LOG(INFO) << "Connection (client) " << connection_id() << " http2 connection error: " << (int)error; + data_frame_ = nullptr; + stream_id_ = 0; + adapter_->SubmitGoAway(0, http2::adapter::Http2ErrorCode::HTTP2_NO_ERROR, ""sv); + DCHECK(adapter_->want_write()); } bool CliConnection::OnFrameHeader(StreamId stream_id, size_t /*length*/, uint8_t /*type*/, uint8_t /*flags*/) { @@ -728,6 +693,12 @@ asio::error_code CliConnection::OnReadHttpRequest(std::shared_ptr buf) { http_keep_alive_remaining_bytes_ += parser.content_length() + header.size() - buf->length(); VLOG(3) << "Connection (client) " << connection_id() << " Host: " << http_host_ << " PORT: " << http_port_ << " KEEPALIVE: " << std::boolalpha << http_is_keep_alive_; + if (parser.transfer_encoding_is_chunked()) { + // See #957 + LOG(WARNING) << "Connection (client) " << connection_id() + << " detected chunked transfer encoding, disabling keep alive handling"; + http_is_keep_alive_ = false; + } } else { VLOG(3) << "Connection (client) " << connection_id() << " CONNECT: " << http_host_ << " PORT: " << http_port_; } @@ -1041,15 +1012,22 @@ try_again: #ifdef HAVE_QUICHE if (adapter_) { absl::string_view remaining_buffer(reinterpret_cast(buf->data()), buf->length()); - while (!remaining_buffer.empty()) { - int result = adapter_->ProcessBytes(remaining_buffer); + while (!remaining_buffer.empty() && adapter_->want_read()) { + http2_in_recv_callback_ = true; + int64_t result = adapter_->ProcessBytes(remaining_buffer); + http2_in_recv_callback_ = false; if (result < 0) { - ec = asio::error::connection_refused; - disconnected(ec); - return nullptr; + /* handled in OnConnectionError inside ProcessBytes call */ + goto out; } remaining_buffer = remaining_buffer.substr(result); } + // don't want read anymore (after goaway sent) + if (UNLIKELY(!remaining_buffer.empty())) { + ec = asio::error::connection_refused; + disconnected(ec); + return nullptr; + } // not enough buffer for recv window if (downstream_.byte_length() < H2_STREAM_WINDOW_SIZE) { goto try_again; @@ -1867,26 +1845,40 @@ void CliConnection::OnCmdConnect(const std::string& domain_name, uint16_t port) DCHECK_LE(domain_name.size(), (unsigned int)TLSEXT_MAXLEN_host_name); if (CIPHER_METHOD_IS_SOCKS_NON_DOMAIN_NAME(method())) { + VLOG(1) << "Connection (client) " << connection_id() << " resolving domain name " << domain_name << " locally"; scoped_refptr self(this); - resolver_.AsyncResolve(domain_name, port, - [this, self](const asio::error_code& ec, asio::ip::tcp::resolver::results_type results) { - // Cancelled, safe to ignore - if (UNLIKELY(ec == asio::error::operation_aborted)) { - return; - } - if (closed_) { - return; - } - if (ec) { - disconnected(ec); - return; - } - for (auto iter = std::begin(results); iter != std::end(results); ++iter) { - ss_request_ = std::make_unique(*iter); - OnConnect(); - break; - } - }); + int ret = resolver_.Init(); + if (ret < 0) { + LOG(WARNING) << "resolver initialize failure"; + OnDisconnect(asio::error::host_not_found); + return; + } + resolver_.AsyncResolve( + domain_name, port, + [this, self, domain_name](const asio::error_code& ec, asio::ip::tcp::resolver::results_type results) { + resolver_.Reset(); + // Cancelled, safe to ignore + if (UNLIKELY(ec == asio::error::operation_aborted)) { + return; + } + if (closed_) { + return; + } + if (ec) { + OnDisconnect(ec); + return; + } + asio::ip::tcp::endpoint endpoint; + for (auto iter = std::begin(results); iter != std::end(results); ++iter) { + endpoint = iter->endpoint(); + break; + } + DCHECK(!endpoint.address().is_unspecified()); + VLOG(1) << "Connection (client) " << connection_id() << " resolved domain name " << domain_name << " to " + << endpoint.address(); + ss_request_ = std::make_unique(endpoint); + OnConnect(); + }); return; } ss_request_ = std::make_unique(domain_name, port); @@ -2229,16 +2221,6 @@ void CliConnection::disconnected(asio::error_code ec) { scoped_refptr self(this); VLOG(1) << "Connection (client) " << connection_id() << " upstream: lost connection with: " << remote_domain() << " due to " << ec; -#ifdef HAVE_QUICHE - if (data_frame_) { - data_frame_->set_last_frame(true); - adapter_->ResumeStream(stream_id_); - SendIfNotProcessing(); - data_frame_ = nullptr; - stream_id_ = 0; - WriteUpstreamInPipe(); - } -#endif upstream_readable_ = false; upstream_writable_ = false; channel_->close(); diff --git a/yass/src/cli/cli_connection.hpp b/yass/src/cli/cli_connection.hpp index 75a58c538c..0b0ab2dec5 100644 --- a/yass/src/cli/cli_connection.hpp +++ b/yass/src/cli/cli_connection.hpp @@ -171,6 +171,7 @@ class CliConnection : public RefCountedThreadSafe, #ifdef HAVE_QUICHE private: + bool http2_in_recv_callback_ = false; void SendIfNotProcessing(); bool processing_responses_ = false; StreamId stream_id_ = 0; diff --git a/yass/src/net/c-ares.cpp b/yass/src/net/c-ares.cpp index 58e3f7b619..20ce4151df 100644 --- a/yass/src/net/c-ares.cpp +++ b/yass/src/net/c-ares.cpp @@ -330,7 +330,7 @@ void CAresResolver::OnAsyncResolve(AsyncResolveCallback cb, if (status != ARES_SUCCESS) { asio::error_code ec = AresToAsioError(status); VLOG(1) << "C-Ares: Host " << host << ":" << service << " Resolved error: " << ec; - cb(ec, {}); + asio::post(io_context_, [cb, ec]() { cb(ec, {}); }); return; } @@ -349,7 +349,7 @@ void CAresResolver::OnAsyncResolve(AsyncResolveCallback cb, ss << endpoint << " "; } VLOG(1) << "C-Ares: Resolved " << host << ":" << service << " to: [ " << ss.str() << " ]"; - cb(asio::error_code(), std::move(results)); + asio::post(io_context_, [cb, results]() { cb(asio::error_code(), std::move(results)); }); } void CAresResolver::WaitTimer() { diff --git a/yass/src/net/http_parser.cpp b/yass/src/net/http_parser.cpp index ce8e1adb9e..1aeca16562 100644 --- a/yass/src/net/http_parser.cpp +++ b/yass/src/net/http_parser.cpp @@ -25,6 +25,7 @@ constexpr const std::string_view kHttpVersionPrefix = "HTTP/"; // reforge HTTP Request Header and pretend it to buf // including removal of Proxy-Connection header static void ReforgeHttpRequestImpl(std::string* header, + const std::string& version, const char* method_str, const absl::flat_hash_map* additional_headers, const std::string& uri, @@ -44,7 +45,7 @@ static void ReforgeHttpRequestImpl(std::string* header, } ss << method_str << " " // NOLINT(google-*) - << canon_uri << " HTTP/1.1\r\n"; + << canon_uri << " " << version << "\r\n"; for (auto [key, value] : headers) { if (key == "Proxy-Connection") { continue; @@ -221,7 +222,7 @@ int HttpRequestParser::Parse(std::shared_ptr buf, bool* ok) { void HttpRequestParser::ReforgeHttpRequest(std::string* header, const absl::flat_hash_map* additional_headers) { - ReforgeHttpRequestImpl(header, method_.c_str(), additional_headers, http_url_, http_headers_); + ReforgeHttpRequestImpl(header, version_input_, method_.c_str(), additional_headers, http_url_, http_headers_); } void HttpRequestParser::OnRawBodyInput(std::string_view /*input*/) {} @@ -259,24 +260,15 @@ void HttpRequestParser::ProcessHeaders(const quiche::BalsaHeaders& headers) { http_host_ = hostname; http_port_ = portnum; } - if (key == "Content-Length") { - std::string length = std::string(value); - - std::optional lengthnum_opt = StringToIntegerU64(length); - if (!lengthnum_opt.has_value()) { - VLOG(1) << "parser failed: bad http field: content-length: " << length; - status_ = ParserStatus::Error; - break; - } - const uint64_t lengthnum = lengthnum_opt.value(); - content_length_ = lengthnum; - } if (key == "Content-Type") { content_type_ = std::string(value); } if (key == "Connection") { connection_ = std::string(value); } + if (key == "Proxy-Connection") { + connection_ = std::string(value); + } } } @@ -303,6 +295,7 @@ void HttpRequestParser::OnRequestFirstLineInput(std::string_view /*line_input*/, return; } http_url_ = std::string(request_uri); + version_input_ = std::string(version_input); if (is_connect) { std::string authority = http_url_; std::string hostname; @@ -458,8 +451,9 @@ int HttpRequestParser::Parse(std::shared_ptr buf, bool* ok) { void HttpRequestParser::ReforgeHttpRequest(std::string* header, const absl::flat_hash_map* additional_headers) { - ReforgeHttpRequestImpl(header, http_method_str((http_method)parser_->method), additional_headers, http_url_, - http_headers_); + auto version_input = absl::StrCat("HTTP/", parser_->http_major, ".", parser_->http_minor); + ReforgeHttpRequestImpl(header, version_input, http_method_str((http_method)parser_->method), additional_headers, + http_url_, http_headers_); } const char* HttpRequestParser::ErrorMessage() const { @@ -470,6 +464,19 @@ int HttpRequestParser::status_code() const { return parser_->status_code; } +uint64_t HttpRequestParser::content_length() const { + return parser_->content_length; +} + +std::string_view HttpRequestParser::connection() const { + using std::string_view_literals::operator""sv; + return http_should_keep_alive(parser_) ? "Keep-Alive"sv : "Close"sv; +} + +bool HttpRequestParser::transfer_encoding_is_chunked() const { + return parser_->flags & F_CHUNKED; +} + static int OnHttpRequestParseUrl(const char* buf, size_t len, std::string* host, uint16_t* port, int is_connect) { struct http_parser_url url; @@ -499,11 +506,6 @@ int HttpRequestParser::OnReadHttpRequestURL(http_parser* p, const char* buf, siz self->http_is_connect_ = true; } - if (p->http_major == 1 && p->http_minor == 1) { - self->connection_ = "Keep-Alive"; - } else { - self->connection_ = "Close"; - } return 0; } @@ -535,23 +537,9 @@ int HttpRequestParser::OnReadHttpRequestHeaderValue(http_parser* parser, const c self->http_port_ = portnum; } - if (self->http_field_ == "Content-Length") { - std::string length = std::string(buf, len); - - std::optional lengthnum_opt = StringToIntegerU64(length); - if (!lengthnum_opt.has_value()) { - VLOG(1) << "parser failed: bad http field: content-length: " << length; - return -1; - } - const uint64_t lengthnum = lengthnum_opt.value(); - self->content_length_ = lengthnum; - } if (self->http_field_ == "Content-Type") { self->content_type_ = std::string(buf, len); } - if (self->http_field_ == "Connection") { - self->connection_ = std::string(buf, len); - } return 0; } diff --git a/yass/src/net/http_parser.hpp b/yass/src/net/http_parser.hpp index 8146cbacde..47bdb11655 100644 --- a/yass/src/net/http_parser.hpp +++ b/yass/src/net/http_parser.hpp @@ -47,9 +47,10 @@ class HttpRequestParser : public quiche::BalsaVisitorInterface { const std::string& host() const { return http_host_; } uint16_t port() const { return http_port_; } bool is_connect() const { return http_is_connect_; } - uint64_t content_length() const { return content_length_; } + uint64_t content_length() const { return headers_.content_length(); } const std::string& content_type() const { return content_type_; } const std::string& connection() const { return connection_; } + bool transfer_encoding_is_chunked() const { return headers_.transfer_encoding_is_chunked(); } void ReforgeHttpRequest(std::string* header, const absl::flat_hash_map* additional_headers = nullptr); @@ -91,6 +92,8 @@ class HttpRequestParser : public quiche::BalsaVisitorInterface { std::string method_; /// copy of url std::string http_url_; + /// copy of version input + std::string version_input_; /// copy of parsed connect host or host field std::string http_host_; /// copy of parsed connect host or host field @@ -99,8 +102,6 @@ class HttpRequestParser : public quiche::BalsaVisitorInterface { absl::flat_hash_map http_headers_; /// copy of connect method bool http_is_connect_ = false; - /// copy of content length - uint64_t content_length_ = 0; /// copy of content type std::string content_type_; /// copy of connection @@ -134,9 +135,10 @@ class HttpRequestParser { const std::string& host() const { return http_host_; } uint16_t port() const { return http_port_; } bool is_connect() const { return http_is_connect_; } - uint64_t content_length() const { return content_length_; } + uint64_t content_length() const; const std::string& content_type() const { return content_type_; } - const std::string& connection() const { return connection_; } + std::string_view connection() const; + bool transfer_encoding_is_chunked() const; int status_code() const; @@ -166,12 +168,8 @@ class HttpRequestParser { absl::flat_hash_map http_headers_; /// copy of connect method bool http_is_connect_ = false; - /// copy of content length - uint64_t content_length_ = 0; /// copy of content type std::string content_type_; - /// copy of connection - std::string connection_; }; class HttpResponseParser : public HttpRequestParser { diff --git a/yass/src/server/server_connection.cpp b/yass/src/server/server_connection.cpp index 48f85b51c2..7163112842 100644 --- a/yass/src/server/server_connection.cpp +++ b/yass/src/server/server_connection.cpp @@ -78,24 +78,8 @@ bool DataFrameSource::Send(absl::string_view frame_header, size_t payload_length concatenated = std::string{frame_header}; } const int64_t result = connection_->OnReadyToSend(concatenated); - // Write encountered error. - if (result < 0) { - connection_->OnConnectionError(http2::adapter::Http2VisitorInterface::ConnectionError::kSendError); - return false; - } - // Write blocked. - if (result == 0) { - connection_->blocked_stream_ = stream_id_; - return false; - } - - if (static_cast(result) < concatenated.size()) { - // Probably need to handle this better within this test class. - QUICHE_LOG(DFATAL) << "DATA frame not fully flushed. Connection will be corrupt!"; - connection_->OnConnectionError(http2::adapter::Http2VisitorInterface::ConnectionError::kSendError); - return false; - } + DCHECK_EQ(static_cast(result), concatenated.size()); if (!payload_length) { return true; @@ -176,22 +160,6 @@ void ServerConnection::close() { << " disconnected with client at stage: " << ServerConnection::state_to_str(CurrentState()); asio::error_code ec; closing_ = true; - -#ifdef HAVE_QUICHE - if (adapter_) { - if (data_frame_) { - data_frame_->set_last_frame(true); - adapter_->ResumeStream(stream_id_); - SendIfNotProcessing(); - data_frame_ = nullptr; - stream_id_ = 0; - } - adapter_->SubmitGoAway(0, http2::adapter::Http2ErrorCode::HTTP2_NO_ERROR, ""sv); - DCHECK(adapter_->want_write()); - SendIfNotProcessing(); - WriteStreamInPipe(); - } -#endif closed_ = true; if (enable_tls_ && !shutdown_) { shutdown_ = true; @@ -257,6 +225,7 @@ void ServerConnection::Start() { #ifdef HAVE_QUICHE void ServerConnection::SendIfNotProcessing() { + DCHECK(!http2_in_recv_callback_); if (!processing_responses_) { processing_responses_ = true; adapter_->Send(); @@ -375,8 +344,6 @@ bool ServerConnection::OnEndStream(StreamId stream_id) { stream_id_ = 0; adapter_->SubmitGoAway(0, http2::adapter::Http2ErrorCode::HTTP2_NO_ERROR, ""sv); DCHECK(adapter_->want_write()); - SendIfNotProcessing(); - WriteStreamInPipe(); } return true; } @@ -394,8 +361,12 @@ bool ServerConnection::OnCloseStream(StreamId stream_id, http2::adapter::Http2Er return true; } -void ServerConnection::OnConnectionError(ConnectionError /*error*/) { - OnDisconnect(asio::error::connection_aborted); +void ServerConnection::OnConnectionError(ConnectionError error) { + LOG(INFO) << "Connection (server) " << connection_id() << " http2 connection error: " << (int)error; + data_frame_ = nullptr; + stream_id_ = 0; + adapter_->SubmitGoAway(0, http2::adapter::Http2ErrorCode::HTTP2_NO_ERROR, ""sv); + DCHECK(adapter_->want_write()); } bool ServerConnection::OnFrameHeader(StreamId stream_id, size_t /*length*/, uint8_t /*type*/, uint8_t /*flags*/) { @@ -1414,15 +1385,22 @@ try_again: #ifdef HAVE_QUICHE if (adapter_) { absl::string_view remaining_buffer(reinterpret_cast(buf->data()), buf->length()); - while (!remaining_buffer.empty()) { - int result = adapter_->ProcessBytes(remaining_buffer); + while (!remaining_buffer.empty() && adapter_->want_read()) { + http2_in_recv_callback_ = true; + int64_t result = adapter_->ProcessBytes(remaining_buffer); + http2_in_recv_callback_ = false; if (result < 0) { - ec = asio::error::connection_refused; - OnDisconnect(asio::error::connection_refused); - return nullptr; + /* handled in OnConnectionError inside ProcessBytes call */ + goto out; } remaining_buffer = remaining_buffer.substr(result); } + // don't want read anymore (after goaway sent) + if (UNLIKELY(!remaining_buffer.empty())) { + ec = asio::error::connection_refused; + OnDisconnect(ec); + return nullptr; + } // not enough buffer for recv window if (upstream_.byte_length() < H2_STREAM_WINDOW_SIZE) { goto try_again; @@ -1582,7 +1560,6 @@ void ServerConnection::OnConnect() { } int submit_result = adapter_->SubmitResponse(stream_id_, GenerateHeaders(headers, 200), std::move(data_frame), false); - SendIfNotProcessing(); if (submit_result != 0) { OnDisconnect(asio::error::connection_aborted); } diff --git a/yass/src/server/server_connection.hpp b/yass/src/server/server_connection.hpp index eb40b15429..4c73035718 100644 --- a/yass/src/server/server_connection.hpp +++ b/yass/src/server/server_connection.hpp @@ -164,6 +164,7 @@ class ServerConnection : public RefCountedThreadSafe, #ifdef HAVE_QUICHE private: + bool http2_in_recv_callback_ = false; void SendIfNotProcessing(); bool processing_responses_ = false; StreamId stream_id_ = 0; diff --git a/yass/third_party/libc++/gcc-mac.patch b/yass/third_party/libc++/gcc-mac.patch deleted file mode 100644 index 9d777576a4..0000000000 --- a/yass/third_party/libc++/gcc-mac.patch +++ /dev/null @@ -1,28 +0,0 @@ -From 55a08eed4ab3b6ca6b784c5b0b3cecec8a6d9bb4 Mon Sep 17 00:00:00 2001 -From: Chilledheart -Date: Wed, 9 Mar 2022 15:35:19 +0800 -Subject: [PATCH] fix gcc compiling under mac - ---- - include/__config | 5 +++++ - 1 file changed, 5 insertions(+) - -diff --git a/include/__config b/include/__config -index 30a4f4a95..78948680d 100644 ---- a/include/__config -+++ b/include/__config -@@ -1454,6 +1454,11 @@ extern "C" _LIBCPP_FUNC_VIS void __sanitizer_annotate_contiguous_container( - # define _LIBCPP_INIT_PRIORITY_MAX - #endif - -+#if defined(__GNUC__) && !defined(__clang__) && defined(__APPLE__) -+# undef _LIBCPP_INIT_PRIORITY_MAX -+# define _LIBCPP_INIT_PRIORITY_MAX -+#endif -+ - # if __has_attribute(__format__) - // The attribute uses 1-based indices for ordinary and static member functions. - // The attribute uses 2-based indices for non-static member functions. --- -2.35.1 - diff --git a/yass/third_party/libc++/gcc-using-if-exist.patch b/yass/third_party/libc++/gcc-using-if-exist.patch deleted file mode 100644 index 42e58a9853..0000000000 --- a/yass/third_party/libc++/gcc-using-if-exist.patch +++ /dev/null @@ -1,98 +0,0 @@ -From f1464fdb51d4e1bc8c4011f899b2a1fb5f58e5f2 Mon Sep 17 00:00:00 2001 -From: Chilledheart -Date: Sat, 10 Dec 2022 22:36:19 +0800 -Subject: [PATCH] gcc: fix using-if-exist - ---- - include/cstdlib | 2 ++ - include/ctime | 4 ++++ - include/system_error | 4 ---- - 3 files changed, 6 insertions(+), 4 deletions(-) - -diff --git a/include/cstdlib b/include/cstdlib -index 25c9de516..8f0e8ba6f 100644 ---- a/include/cstdlib -+++ b/include/cstdlib -@@ -140,6 +140,7 @@ using ::mbtowc _LIBCPP_USING_IF_EXISTS; - using ::wctomb _LIBCPP_USING_IF_EXISTS; - using ::mbstowcs _LIBCPP_USING_IF_EXISTS; - using ::wcstombs _LIBCPP_USING_IF_EXISTS; -+#if defined(__clang__) - #if !defined(_LIBCPP_CXX03_LANG) - using ::at_quick_exit _LIBCPP_USING_IF_EXISTS; - using ::quick_exit _LIBCPP_USING_IF_EXISTS; -@@ -147,6 +148,7 @@ using ::quick_exit _LIBCPP_USING_IF_EXISTS; - #if _LIBCPP_STD_VER > 14 - using ::aligned_alloc _LIBCPP_USING_IF_EXISTS; - #endif -+#endif - - _LIBCPP_END_NAMESPACE_STD - -diff --git a/include/ctime b/include/ctime -index 0c6e4dfd6..a07f299b7 100644 ---- a/include/ctime -+++ b/include/ctime -@@ -59,9 +59,11 @@ using ::clock_t _LIBCPP_USING_IF_EXISTS; - using ::size_t _LIBCPP_USING_IF_EXISTS; - using ::time_t _LIBCPP_USING_IF_EXISTS; - using ::tm _LIBCPP_USING_IF_EXISTS; -+#if defined(__clang__) - #if _LIBCPP_STD_VER > 14 - using ::timespec _LIBCPP_USING_IF_EXISTS; - #endif -+#endif - using ::clock _LIBCPP_USING_IF_EXISTS; - using ::difftime _LIBCPP_USING_IF_EXISTS; - using ::mktime _LIBCPP_USING_IF_EXISTS; -@@ -71,9 +73,11 @@ using ::ctime _LIBCPP_USING_IF_EXISTS; - using ::gmtime _LIBCPP_USING_IF_EXISTS; - using ::localtime _LIBCPP_USING_IF_EXISTS; - using ::strftime _LIBCPP_USING_IF_EXISTS; -+#if defined(__clang__) - #if _LIBCPP_STD_VER > 14 - using ::timespec_get _LIBCPP_USING_IF_EXISTS; - #endif -+#endif - - _LIBCPP_END_NAMESPACE_STD - -diff --git a/include/system_error b/include/system_error -index 98919927b..990fb1473 100644 ---- a/include/system_error -+++ b/include/system_error -@@ -281,7 +281,6 @@ public: - typename enable_if::value>::type* = nullptr - ) _NOEXCEPT - { -- using __adl_only::make_error_condition; - *this = make_error_condition(__e); - } - -@@ -301,7 +300,6 @@ public: - >::type - operator=(_Ep __e) _NOEXCEPT - { -- using __adl_only::make_error_condition; - *this = make_error_condition(__e); - return *this; - } -@@ -351,7 +349,6 @@ public: - typename enable_if::value>::type* = nullptr - ) _NOEXCEPT - { -- using __adl_only::make_error_code; - *this = make_error_code(__e); - } - -@@ -371,7 +368,6 @@ public: - >::type - operator=(_Ep __e) _NOEXCEPT - { -- using __adl_only::make_error_code; - *this = make_error_code(__e); - return *this; - } --- -2.37.1 (Apple Git-137.1) - diff --git a/yass/third_party/nghttp2/lib/nghttp2_session.c b/yass/third_party/nghttp2/lib/nghttp2_session.c index a560b407ee..54746fb37b 100644 --- a/yass/third_party/nghttp2/lib/nghttp2_session.c +++ b/yass/third_party/nghttp2/lib/nghttp2_session.c @@ -5477,15 +5477,6 @@ int nghttp2_session_on_data_received(nghttp2_session *session, if (nghttp2_is_fatal(rv)) { return rv; } - /* it might send goaway and call session_close_stream_on_goaway in previous callback - * and stream might be gone after nghttp2_map_remove */ - stream = nghttp2_session_get_stream(session, frame->hd.stream_id); - if (!stream || stream->state == NGHTTP2_STREAM_CLOSING) { - /* This should be treated as stream error, but it results in lots - of RST_STREAM. So just ignore frame against nonexistent stream - for now. */ - return 0; - } if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { nghttp2_stream_shutdown(stream, NGHTTP2_SHUT_RD); diff --git a/yt-dlp/README.md b/yt-dlp/README.md index e3257682b5..cdd57b024c 100644 --- a/yt-dlp/README.md +++ b/yt-dlp/README.md @@ -666,7 +666,7 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git The name of the browser to load cookies from. Currently supported browsers are: brave, chrome, chromium, edge, firefox, - opera, safari, vivaldi. Optionally, the + opera, safari, vivaldi, whale. Optionally, the KEYRING used for decrypting Chromium cookies on Linux, the name/path of the PROFILE to load cookies from, and the CONTAINER name @@ -1760,7 +1760,7 @@ The following extractors use this feature: #### youtube * `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube.py](https://github.com/yt-dlp/yt-dlp/blob/c26f9b991a0681fd3ea548d535919cec1fbbd430/yt_dlp/extractor/youtube.py#L381-L390) for list of supported content language codes * `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively -* `player_client`: Clients to extract video data from. The main clients are `web`, `android` and `ios` with variants `_music`, `_embedded`, `_embedscreen`, `_creator` (e.g. `web_embedded`); and `mweb`, `mweb_embedscreen`, `mediaconnect` and `tv_embedded` (agegate bypass) with no variants. By default, `ios,android,web` is used, but `tv_embedded` and `creator` variants are added as required for age-gated videos. Similarly, the music variants are added for `music.youtube.com` urls. You can use `all` to use all the clients, and `default` for the default clients. +* `player_client`: Clients to extract video data from. The main clients are `web`, `ios` and `android`, with variants `_music`, `_embedded`, `_embedscreen`, `_creator` (e.g. `web_embedded`); and `mweb`, `mweb_embedscreen` and `tv_embedded` (agegate bypass) with no variants. By default, `ios,web` is used, but `tv_embedded` and `creator` variants are added as required for age-gated videos. Similarly, the music variants are added for `music.youtube.com` urls. The `android` clients will always be given lowest priority since their formats are broken. You can use `all` to use all the clients, and `default` for the default clients. * `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause some issues. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) for more details * `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp. * `comment_sort`: `top` or `new` (default) - choose comment sorting mode (on YouTube's side) @@ -1813,8 +1813,8 @@ The following extractors use this feature: * `app_name`: Default app name to use with mobile API calls, e.g. `trill` * `app_version`: Default app version to use with mobile API calls - should be set along with `manifest_app_version`, e.g. `34.1.2` * `manifest_app_version`: Default numeric app version to use with mobile API calls, e.g. `2023401020` -* `aid`: Default app ID to use with API calls, e.g. `1180` -* `app_info`: One or more app info strings in the format of `/[app_name]/[app_version]/[manifest_app_version]/[aid]`, where `iid` is the unique app install ID. `iid` is the only required value; all other values and their `/` separators can be omitted, e.g. `tiktok:app_info=1234567890123456789` or `tiktok:app_info=123,456/trill///1180,789//34.0.1/340001` +* `aid`: Default app ID to use with mobile API calls, e.g. `1180` +* `app_info`: Enable mobile API extraction with one or more app info strings in the format of `/[app_name]/[app_version]/[manifest_app_version]/[aid]`, where `iid` is the unique app install ID. `iid` is the only required value; all other values and their `/` separators can be omitted, e.g. `tiktok:app_info=1234567890123456789` or `tiktok:app_info=123,456/trill///1180,789//34.0.1/340001` #### rokfinchannel * `tab`: Which tab to download - one of `new`, `top`, `videos`, `podcasts`, `streams`, `stacks` diff --git a/yt-dlp/yt_dlp/cookies.py b/yt-dlp/yt_dlp/cookies.py index 0de0672e12..815897d5a5 100644 --- a/yt-dlp/yt_dlp/cookies.py +++ b/yt-dlp/yt_dlp/cookies.py @@ -46,7 +46,7 @@ from .utils import ( from .utils._utils import _YDLLogger from .utils.networking import normalize_url -CHROMIUM_BASED_BROWSERS = {'brave', 'chrome', 'chromium', 'edge', 'opera', 'vivaldi'} +CHROMIUM_BASED_BROWSERS = {'brave', 'chrome', 'chromium', 'edge', 'opera', 'vivaldi', 'whale'} SUPPORTED_BROWSERS = CHROMIUM_BASED_BROWSERS | {'firefox', 'safari'} @@ -219,6 +219,7 @@ def _get_chromium_based_browser_settings(browser_name): 'edge': os.path.join(appdata_local, R'Microsoft\Edge\User Data'), 'opera': os.path.join(appdata_roaming, R'Opera Software\Opera Stable'), 'vivaldi': os.path.join(appdata_local, R'Vivaldi\User Data'), + 'whale': os.path.join(appdata_local, R'Naver\Naver Whale\User Data'), }[browser_name] elif sys.platform == 'darwin': @@ -230,6 +231,7 @@ def _get_chromium_based_browser_settings(browser_name): 'edge': os.path.join(appdata, 'Microsoft Edge'), 'opera': os.path.join(appdata, 'com.operasoftware.Opera'), 'vivaldi': os.path.join(appdata, 'Vivaldi'), + 'whale': os.path.join(appdata, 'Naver/Whale'), }[browser_name] else: @@ -241,6 +243,7 @@ def _get_chromium_based_browser_settings(browser_name): 'edge': os.path.join(config, 'microsoft-edge'), 'opera': os.path.join(config, 'opera'), 'vivaldi': os.path.join(config, 'vivaldi'), + 'whale': os.path.join(config, 'naver-whale'), }[browser_name] # Linux keyring names can be determined by snooping on dbus while opening the browser in KDE: @@ -252,6 +255,7 @@ def _get_chromium_based_browser_settings(browser_name): 'edge': 'Microsoft Edge' if sys.platform == 'darwin' else 'Chromium', 'opera': 'Opera' if sys.platform == 'darwin' else 'Chromium', 'vivaldi': 'Vivaldi' if sys.platform == 'darwin' else 'Chrome', + 'whale': 'Whale', }[browser_name] browsers_without_profiles = {'opera'} diff --git a/yt-dlp/yt_dlp/extractor/bbc.py b/yt-dlp/yt_dlp/extractor/bbc.py index 015af9e1d6..f6b58b361f 100644 --- a/yt-dlp/yt_dlp/extractor/bbc.py +++ b/yt-dlp/yt_dlp/extractor/bbc.py @@ -602,7 +602,7 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'url': 'http://www.bbc.com/news/world-europe-32668511', 'info_dict': { 'id': 'world-europe-32668511', - 'title': 'Russia stages massive WW2 parade', + 'title': 'Russia stages massive WW2 parade despite Western boycott', 'description': 'md5:00ff61976f6081841f759a08bf78cc9c', }, 'playlist_count': 2, @@ -623,6 +623,7 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'info_dict': { 'id': '3662a707-0af9-3149-963f-47bea720b460', 'title': 'BUGGER', + 'description': r're:BUGGER The recent revelations by the whistleblower Edward Snowden were fascinating. .{211}\.{3}$', }, 'playlist_count': 18, }, { @@ -631,14 +632,14 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'info_dict': { 'id': 'p02mprgb', 'ext': 'mp4', - 'title': 'Aerial footage showed the site of the crash in the Alps - courtesy BFM TV', - 'description': 'md5:2868290467291b37feda7863f7a83f54', + 'title': 'Germanwings crash site aerial video', + 'description': r're:(?s)Aerial video showed the site where the Germanwings flight 4U 9525, .{156} BFM TV\.$', 'duration': 47, 'timestamp': 1427219242, 'upload_date': '20150324', + 'thumbnail': 'https://ichef.bbci.co.uk/news/1024/media/images/81879000/jpg/_81879090_81879089.jpg', }, 'params': { - # rtmp download 'skip_download': True, } }, { @@ -656,21 +657,24 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE }, 'params': { 'skip_download': True, - } + }, + 'skip': 'now SIMORGH_DATA with no video', }, { # single video embedded with data-playable containing XML playlists (regional section) 'url': 'http://www.bbc.com/mundo/video_fotos/2015/06/150619_video_honduras_militares_hospitales_corrupcion_aw', 'info_dict': { - 'id': '150619_video_honduras_militares_hospitales_corrupcion_aw', + 'id': '39275083', + 'display_id': '150619_video_honduras_militares_hospitales_corrupcion_aw', 'ext': 'mp4', 'title': 'Honduras militariza sus hospitales por nuevo escándalo de corrupción', - 'description': 'md5:1525f17448c4ee262b64b8f0c9ce66c8', + 'description': 'Honduras militariza sus hospitales por nuevo escándalo de corrupción', 'timestamp': 1434713142, 'upload_date': '20150619', + 'thumbnail': 'https://a.files.bbci.co.uk/worldservice/live/assets/images/2015/06/19/150619132146_honduras_hsopitales_militares_640x360_aptn_nocredit.jpg', }, 'params': { 'skip_download': True, - } + }, }, { # single video from video playlist embedded with vxp-playlist-data JSON 'url': 'http://www.bbc.com/news/video_and_audio/must_see/33376376', @@ -683,22 +687,21 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE }, 'params': { 'skip_download': True, - } + }, + 'skip': '404 Not Found', }, { - # single video story with digitalData + # single video story with __PWA_PRELOADED_STATE__ 'url': 'http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret', 'info_dict': { 'id': 'p02q6gc4', - 'ext': 'flv', - 'title': 'Sri Lanka’s spicy secret', - 'description': 'As a new train line to Jaffna opens up the country’s north, travellers can experience a truly distinct slice of Tamil culture.', - 'timestamp': 1437674293, - 'upload_date': '20150723', + 'ext': 'mp4', + 'title': 'Tasting the spice of life in Jaffna', + 'description': r're:(?s)BBC Travel Show’s Henry Golding explores the city of Jaffna .{151} aftertaste\.$', + 'timestamp': 1646058397, + 'upload_date': '20220228', + 'duration': 255, + 'thumbnail': 'https://ichef.bbci.co.uk/images/ic/1920xn/p02vxvkn.jpg', }, - 'params': { - # rtmp download - 'skip_download': True, - } }, { # single video story without digitalData 'url': 'http://www.bbc.com/autos/story/20130513-hyundais-rock-star', @@ -710,12 +713,10 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'timestamp': 1415867444, 'upload_date': '20141113', }, - 'params': { - # rtmp download - 'skip_download': True, - } + 'skip': 'redirects to TopGear home page', }, { # single video embedded with Morph + # TODO: replacement test page 'url': 'http://www.bbc.co.uk/sport/live/olympics/36895975', 'info_dict': { 'id': 'p041vhd0', @@ -726,27 +727,22 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'uploader': 'BBC Sport', 'uploader_id': 'bbc_sport', }, - 'params': { - # m3u8 download - 'skip_download': True, - }, - 'skip': 'Georestricted to UK', + 'skip': 'Video no longer in page', }, { - # single video with playlist.sxml URL in playlist param + # single video in __INITIAL_DATA__ 'url': 'http://www.bbc.com/sport/0/football/33653409', 'info_dict': { 'id': 'p02xycnp', 'ext': 'mp4', - 'title': 'Transfers: Cristiano Ronaldo to Man Utd, Arsenal to spend?', - 'description': 'BBC Sport\'s David Ornstein has the latest transfer gossip, including rumours of a Manchester United return for Cristiano Ronaldo.', + 'title': 'Ronaldo to Man Utd, Arsenal to spend?', + 'description': r're:(?s)BBC Sport\'s David Ornstein rounds up the latest transfer reports, .{359} here\.$', + 'timestamp': 1437750175, + 'upload_date': '20150724', + 'thumbnail': r're:https?://.+/.+media/images/69320000/png/_69320754_mmgossipcolumnextraaugust18.png', 'duration': 140, }, - 'params': { - # rtmp download - 'skip_download': True, - } }, { - # article with multiple videos embedded with playlist.sxml in playlist param + # article with multiple videos embedded with Morph.setPayload 'url': 'http://www.bbc.com/sport/0/football/34475836', 'info_dict': { 'id': '34475836', @@ -754,6 +750,21 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'description': 'Fast-paced football, wit, wisdom and a ready smile - why Liverpool fans should come to love new boss Jurgen Klopp.', }, 'playlist_count': 3, + }, { + # Testing noplaylist + 'url': 'http://www.bbc.com/sport/0/football/34475836', + 'info_dict': { + 'id': 'p034ppnv', + 'ext': 'mp4', + 'title': 'All you need to know about Jurgen Klopp', + 'timestamp': 1444335081, + 'upload_date': '20151008', + 'duration': 122.0, + 'thumbnail': 'https://ichef.bbci.co.uk/onesport/cps/976/cpsprodpb/7542/production/_85981003_klopp.jpg', + }, + 'params': { + 'noplaylist': True, + }, }, { # school report article with single video 'url': 'http://www.bbc.co.uk/schoolreport/35744779', @@ -762,6 +773,7 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'title': 'School which breaks down barriers in Jerusalem', }, 'playlist_count': 1, + 'skip': 'redirects to Young Reporter home page https://www.bbc.co.uk/news/topics/cg41ylwv43pt', }, { # single video with playlist URL from weather section 'url': 'http://www.bbc.com/weather/features/33601775', @@ -778,18 +790,33 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'thumbnail': r're:https?://.+/.+\.jpg', 'timestamp': 1437785037, 'upload_date': '20150725', + 'duration': 105, }, }, { # video with window.__INITIAL_DATA__ and value as JSON string 'url': 'https://www.bbc.com/news/av/world-europe-59468682', 'info_dict': { - 'id': 'p0b71qth', + 'id': 'p0b779gc', 'ext': 'mp4', 'title': 'Why France is making this woman a national hero', - 'description': 'md5:7affdfab80e9c3a1f976230a1ff4d5e4', + 'description': r're:(?s)France is honouring the US-born 20th Century singer and activist Josephine .{208} Second World War.', 'thumbnail': r're:https?://.+/.+\.jpg', - 'timestamp': 1638230731, - 'upload_date': '20211130', + 'timestamp': 1638215626, + 'upload_date': '20211129', + 'duration': 125, + }, + }, { + # video with script id __NEXT_DATA__ and value as JSON string + 'url': 'https://www.bbc.com/news/uk-68546268', + 'info_dict': { + 'id': 'p0hj0lq7', + 'ext': 'mp4', + 'title': 'Nasser Hospital doctor describes his treatment by IDF', + 'description': r're:(?s)Doctor Abu Sabha said he was detained by Israeli forces after .{276} hostages\."$', + 'thumbnail': r're:https?://.+/.+\.jpg', + 'timestamp': 1710188248, + 'upload_date': '20240311', + 'duration': 104, }, }, { # single video article embedded with data-media-vpid @@ -817,6 +844,7 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'uploader': 'Radio 3', 'uploader_id': 'bbc_radio_three', }, + 'skip': '404 Not Found', }, { 'url': 'http://www.bbc.co.uk/learningenglish/chinese/features/lingohack/ep-181227', 'info_dict': { @@ -824,6 +852,7 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'ext': 'mp4', 'title': 'md5:2fabf12a726603193a2879a055f72514', 'description': 'Learn English words and phrases from this story', + 'thumbnail': 'https://ichef.bbci.co.uk/images/ic/1200x675/p06pq9gk.jpg', }, 'add_ie': [BBCCoUkIE.ie_key()], }, { @@ -832,28 +861,30 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'info_dict': { 'id': 'p07c6sb9', 'ext': 'mp4', - 'title': 'How positive thinking is harming your happiness', - 'alt_title': 'The downsides of positive thinking', - 'description': 'md5:fad74b31da60d83b8265954ee42d85b4', + 'title': 'The downsides of positive thinking', + 'description': 'The downsides of positive thinking', 'duration': 235, - 'thumbnail': r're:https?://.+/p07c9dsr.jpg', - 'upload_date': '20190604', - 'categories': ['Psychology'], + 'thumbnail': r're:https?://.+/p07c9dsr\.(?:jpg|webp|png)', + 'upload_date': '20220223', + 'timestamp': 1645632746, }, }, { # BBC Sounds - 'url': 'https://www.bbc.co.uk/sounds/play/m001q78b', + 'url': 'https://www.bbc.co.uk/sounds/play/w3ct5rgx', 'info_dict': { - 'id': 'm001q789', + 'id': 'p0hrw4nr', 'ext': 'mp4', - 'title': 'The Night Tracks Mix - Music for the darkling hour', - 'thumbnail': 'https://ichef.bbci.co.uk/images/ic/raw/p0c00hym.jpg', - 'chapters': 'count:8', - 'description': 'md5:815fb51cbdaa270040aab8145b3f1d67', - 'uploader': 'Radio 3', - 'duration': 1800, - 'uploader_id': 'bbc_radio_three', - }, + 'title': 'Are our coastlines being washed away?', + 'description': r're:(?s)Around the world, coastlines are constantly changing .{2000,} Images\)$', + 'timestamp': 1713556800, + 'upload_date': '20240419', + 'duration': 1588, + 'thumbnail': 'https://ichef.bbci.co.uk/images/ic/raw/p0hrnxbl.jpg', + 'uploader': 'World Service', + 'uploader_id': 'bbc_world_service', + 'series': 'CrowdScience', + 'chapters': [], + } }, { # onion routes 'url': 'https://www.bbcnewsd73hkzno2ini43t4gblxvycyac5aw4gnv7t2rccijh7745uqd.onion/news/av/world-europe-63208576', 'only_matching': True, @@ -1008,8 +1039,7 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE webpage, 'group id', default=None) if group_id: return self.url_result( - 'https://www.bbc.co.uk/programmes/%s' % group_id, - ie=BBCCoUkIE.ie_key()) + f'https://www.bbc.co.uk/programmes/{group_id}', BBCCoUkIE) # single video story (e.g. http://www.bbc.com/travel/story/20150625-sri-lankas-spicy-secret) programme_id = self._search_regex( @@ -1069,83 +1099,133 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE } # Morph based embed (e.g. http://www.bbc.co.uk/sport/live/olympics/36895975) - # There are several setPayload calls may be present but the video - # seems to be always related to the first one - morph_payload = self._parse_json( - self._search_regex( - r'Morph\.setPayload\([^,]+,\s*({.+?})\);', - webpage, 'morph payload', default='{}'), - playlist_id, fatal=False) + # Several setPayload calls may be present but the video(s) + # should be in one that mentions leadMedia or videoData + morph_payload = self._search_json( + r'\bMorph\s*\.\s*setPayload\s*\([^,]+,', webpage, 'morph payload', playlist_id, + contains_pattern=r'{(?s:(?:(?!).)+(?:"leadMedia"|\\"videoData\\")\s*:.+)}', + default={}) if morph_payload: - components = try_get(morph_payload, lambda x: x['body']['components'], list) or [] - for component in components: - if not isinstance(component, dict): - continue - lead_media = try_get(component, lambda x: x['props']['leadMedia'], dict) - if not lead_media: - continue - identifiers = lead_media.get('identifiers') - if not identifiers or not isinstance(identifiers, dict): - continue - programme_id = identifiers.get('vpid') or identifiers.get('playablePid') + for lead_media in traverse_obj(morph_payload, ( + 'body', 'components', ..., 'props', 'leadMedia', {dict})): + programme_id = traverse_obj(lead_media, ('identifiers', ('vpid', 'playablePid'), {str}, any)) if not programme_id: continue - title = lead_media.get('title') or self._og_search_title(webpage) formats, subtitles = self._download_media_selector(programme_id) - description = lead_media.get('summary') - uploader = lead_media.get('masterBrand') - uploader_id = lead_media.get('mid') - duration = None - duration_d = lead_media.get('duration') - if isinstance(duration_d, dict): - duration = parse_duration(dict_get( - duration_d, ('rawDuration', 'formattedDuration', 'spokenDuration'))) return { 'id': programme_id, - 'title': title, - 'description': description, - 'duration': duration, - 'uploader': uploader, - 'uploader_id': uploader_id, + 'title': lead_media.get('title') or self._og_search_title(webpage), + **traverse_obj(lead_media, { + 'description': ('summary', {str}), + 'duration': ('duration', ('rawDuration', 'formattedDuration', 'spokenDuration'), {parse_duration}), + 'uploader': ('masterBrand', {str}), + 'uploader_id': ('mid', {str}), + }), 'formats': formats, 'subtitles': subtitles, } + body = self._parse_json(traverse_obj(morph_payload, ( + 'body', 'content', 'article', 'body')), playlist_id, fatal=False) + for video_data in traverse_obj(body, (lambda _, v: v['videoData']['pid'], 'videoData')): + if video_data.get('vpid'): + video_id = video_data['vpid'] + formats, subtitles = self._download_media_selector(video_id) + entry = { + 'id': video_id, + 'formats': formats, + 'subtitles': subtitles, + } + else: + video_id = video_data['pid'] + entry = self.url_result( + f'https://www.bbc.co.uk/programmes/{video_id}', BBCCoUkIE, + video_id, url_transparent=True) + entry.update({ + 'timestamp': traverse_obj(morph_payload, ( + 'body', 'content', 'article', 'dateTimeInfo', 'dateTime', {parse_iso8601}) + ), + **traverse_obj(video_data, { + 'thumbnail': (('iChefImage', 'image'), {url_or_none}, any), + 'title': (('title', 'caption'), {str}, any), + 'duration': ('duration', {parse_duration}), + }), + }) + if video_data.get('isLead') and not self._yes_playlist(playlist_id, video_id): + return entry + entries.append(entry) + if entries: + playlist_title = traverse_obj(morph_payload, ( + 'body', 'content', 'article', 'headline', {str})) or playlist_title + return self.playlist_result( + entries, playlist_id, playlist_title, playlist_description) - preload_state = self._parse_json(self._search_regex( - r'window\.__PRELOADED_STATE__\s*=\s*({.+?});', webpage, - 'preload state', default='{}'), playlist_id, fatal=False) - if preload_state: - current_programme = preload_state.get('programmes', {}).get('current') or {} - programme_id = current_programme.get('id') - if current_programme and programme_id and current_programme.get('type') == 'playable_item': - title = current_programme.get('titles', {}).get('tertiary') or playlist_title - formats, subtitles = self._download_media_selector(programme_id) - synopses = current_programme.get('synopses') or {} - network = current_programme.get('network') or {} - duration = int_or_none( - current_programme.get('duration', {}).get('value')) - thumbnail = None - image_url = current_programme.get('image_url') - if image_url: - thumbnail = image_url.replace('{recipe}', 'raw') + # various PRELOADED_STATE JSON + preload_state = self._search_json( + r'window\.__(?:PWA_)?PRELOADED_STATE__\s*=', webpage, + 'preload state', playlist_id, transform_source=js_to_json, default={}) + # PRELOADED_STATE with current programmme + current_programme = traverse_obj(preload_state, ('programmes', 'current', {dict})) + programme_id = traverse_obj(current_programme, ('id', {str})) + if programme_id and current_programme.get('type') == 'playable_item': + title = traverse_obj(current_programme, ('titles', ('tertiary', 'secondary'), {str}, any)) or playlist_title + formats, subtitles = self._download_media_selector(programme_id) + return { + 'id': programme_id, + 'title': title, + 'formats': formats, + **traverse_obj(current_programme, { + 'description': ('synopses', ('long', 'medium', 'short'), {str}, any), + 'thumbnail': ('image_url', {lambda u: url_or_none(u.replace('{recipe}', 'raw'))}), + 'duration': ('duration', 'value', {int_or_none}), + 'uploader': ('network', 'short_title', {str}), + 'uploader_id': ('network', 'id', {str}), + 'timestamp': ((('availability', 'from'), ('release', 'date')), {parse_iso8601}, any), + 'series': ('titles', 'primary', {str}), + }), + 'subtitles': subtitles, + 'chapters': traverse_obj(preload_state, ( + 'tracklist', 'tracks', lambda _, v: float(v['offset']['start']), { + 'title': ('titles', {lambda x: join_nonempty( + 'primary', 'secondary', 'tertiary', delim=' - ', from_dict=x)}), + 'start_time': ('offset', 'start', {float_or_none}), + 'end_time': ('offset', 'end', {float_or_none}), + }) + ), + } + + # PWA_PRELOADED_STATE with article video asset + asset_id = traverse_obj(preload_state, ( + 'entities', 'articles', lambda k, _: k.rsplit('/', 1)[-1] == playlist_id, + 'assetVideo', 0, {str}, any)) + if asset_id: + video_id = traverse_obj(preload_state, ('entities', 'videos', asset_id, 'vpid', {str})) + if video_id: + article = traverse_obj(preload_state, ( + 'entities', 'articles', lambda _, v: v['assetVideo'][0] == asset_id, any)) + + def image_url(image_id): + return traverse_obj(preload_state, ( + 'entities', 'images', image_id, 'url', + {lambda u: url_or_none(u.replace('$recipe', 'raw'))})) + + formats, subtitles = self._download_media_selector(video_id) return { - 'id': programme_id, - 'title': title, - 'description': dict_get(synopses, ('long', 'medium', 'short')), - 'thumbnail': thumbnail, - 'duration': duration, - 'uploader': network.get('short_title'), - 'uploader_id': network.get('id'), + 'id': video_id, + **traverse_obj(preload_state, ('entities', 'videos', asset_id, { + 'title': ('title', {str}), + 'description': (('synopsisLong', 'synopsisMedium', 'synopsisShort'), {str}, any), + 'thumbnail': (0, {image_url}), + 'duration': ('duration', {int_or_none}), + })), 'formats': formats, 'subtitles': subtitles, - 'chapters': traverse_obj(preload_state, ( - 'tracklist', 'tracks', lambda _, v: float_or_none(v['offset']['start']), { - 'title': ('titles', {lambda x: join_nonempty( - 'primary', 'secondary', 'tertiary', delim=' - ', from_dict=x)}), - 'start_time': ('offset', 'start', {float_or_none}), - 'end_time': ('offset', 'end', {float_or_none}), - })) or None, + 'timestamp': traverse_obj(article, ('displayDate', {parse_iso8601})), } + else: + return self.url_result( + f'https://www.bbc.co.uk/programmes/{asset_id}', BBCCoUkIE, + asset_id, playlist_title, display_id=playlist_id, + description=playlist_description) bbc3_config = self._parse_json( self._search_regex( @@ -1191,6 +1271,28 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE return self.playlist_result( entries, playlist_id, playlist_title, playlist_description) + def parse_model(model): + """Extract single video from model structure""" + item_id = traverse_obj(model, ('versions', 0, 'versionId', {str})) + if not item_id: + return + formats, subtitles = self._download_media_selector(item_id) + return { + 'id': item_id, + 'formats': formats, + 'subtitles': subtitles, + **traverse_obj(model, { + 'title': ('title', {str}), + 'thumbnail': ('imageUrl', {lambda u: urljoin(url, u.replace('$recipe', 'raw'))}), + 'description': ('synopses', ('long', 'medium', 'short'), {str}, {lambda x: x or None}, any), + 'duration': ('versions', 0, 'duration', {int}), + 'timestamp': ('versions', 0, 'availableFrom', {functools.partial(int_or_none, scale=1000)}), + }) + } + + def is_type(*types): + return lambda _, v: v['type'] in types + initial_data = self._search_regex( r'window\.__INITIAL_DATA__\s*=\s*("{.+?}")\s*;', webpage, 'quoted preload state', default=None) @@ -1202,6 +1304,19 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE initial_data = self._parse_json(initial_data or '"{}"', playlist_id, fatal=False) initial_data = self._parse_json(initial_data, playlist_id, fatal=False) if initial_data: + for video_data in traverse_obj(initial_data, ( + 'stores', 'article', 'articleBodyContent', is_type('video'))): + model = traverse_obj(video_data, ( + 'model', 'blocks', is_type('aresMedia'), + 'model', 'blocks', is_type('aresMediaMetadata'), + 'model', {dict}, any)) + entry = parse_model(model) + if entry: + entries.append(entry) + if entries: + return self.playlist_result( + entries, playlist_id, playlist_title, playlist_description) + def parse_media(media): if not media: return @@ -1234,27 +1349,90 @@ class BBCIE(BBCCoUkIE): # XXX: Do not subclass from concrete IE 'subtitles': subtitles, 'timestamp': item_time, 'description': strip_or_none(item_desc), + 'duration': int_or_none(item.get('duration')), }) - for resp in (initial_data.get('data') or {}).values(): - name = resp.get('name') + + for resp in traverse_obj(initial_data, ('data', lambda _, v: v['name'])): + name = resp['name'] if name == 'media-experience': parse_media(try_get(resp, lambda x: x['data']['initialItem']['mediaItem'], dict)) elif name == 'article': - for block in (try_get(resp, - (lambda x: x['data']['blocks'], - lambda x: x['data']['content']['model']['blocks'],), - list) or []): - if block.get('type') not in ['media', 'video']: - continue - parse_media(block.get('model')) + for block in traverse_obj(resp, ( + 'data', (None, ('content', 'model')), 'blocks', + is_type('media', 'video'), 'model', {dict})): + parse_media(block) return self.playlist_result( entries, playlist_id, playlist_title, playlist_description) + # extract from SIMORGH_DATA hydration JSON + simorgh_data = self._search_json( + r'window\s*\.\s*SIMORGH_DATA\s*=', webpage, + 'simorgh data', playlist_id, default={}) + if simorgh_data: + done = False + for video_data in traverse_obj(simorgh_data, ( + 'pageData', 'content', 'model', 'blocks', is_type('video', 'legacyMedia'))): + model = traverse_obj(video_data, ( + 'model', 'blocks', is_type('aresMedia'), + 'model', 'blocks', is_type('aresMediaMetadata'), + 'model', {dict}, any)) + if video_data['type'] == 'video': + entry = parse_model(model) + else: # legacyMedia: no duration, subtitles + block_id, entry = traverse_obj(model, ('blockId', {str})), None + media_data = traverse_obj(simorgh_data, ( + 'pageData', 'promo', 'media', + {lambda x: x if x['id'] == block_id else None})) + formats = traverse_obj(media_data, ('playlist', lambda _, v: url_or_none(v['url']), { + 'url': ('url', {url_or_none}), + 'ext': ('format', {str}), + 'tbr': ('bitrate', {functools.partial(int_or_none, scale=1000)}), + })) + if formats: + entry = { + 'id': block_id, + 'display_id': playlist_id, + 'formats': formats, + 'description': traverse_obj(simorgh_data, ('pageData', 'promo', 'summary', {str})), + **traverse_obj(model, { + 'title': ('title', {str}), + 'thumbnail': ('imageUrl', {lambda u: urljoin(url, u.replace('$recipe', 'raw'))}), + 'description': ('synopses', ('long', 'medium', 'short'), {str}, any), + 'timestamp': ('firstPublished', {functools.partial(int_or_none, scale=1000)}), + }), + } + done = True + if entry: + entries.append(entry) + if done: + break + if entries: + return self.playlist_result( + entries, playlist_id, playlist_title, playlist_description) + def extract_all(pattern): return list(filter(None, map( lambda s: self._parse_json(s, playlist_id, fatal=False), re.findall(pattern, webpage)))) + # US accessed article with single embedded video (e.g. + # https://www.bbc.com/news/uk-68546268) + next_data = traverse_obj(self._search_nextjs_data(webpage, playlist_id, default={}), + ('props', 'pageProps', 'page')) + model = traverse_obj(next_data, ( + ..., 'contents', is_type('video'), + 'model', 'blocks', is_type('media'), + 'model', 'blocks', is_type('mediaMetadata'), + 'model', {dict}, any)) + if model and (entry := parse_model(model)): + if not entry.get('timestamp'): + entry['timestamp'] = traverse_obj(next_data, ( + ..., 'contents', is_type('timestamp'), 'model', + 'timestamp', {functools.partial(int_or_none, scale=1000)}, any)) + entries.append(entry) + return self.playlist_result( + entries, playlist_id, playlist_title, playlist_description) + # Multiple video article (e.g. # http://www.bbc.co.uk/blogs/adamcurtis/entries/3662a707-0af9-3149-963f-47bea720b460) EMBED_URL = r'https?://(?:www\.)?bbc\.co\.uk/(?:[^/]+/)+%s(?:\b[^"]+)?' % self._ID_REGEX diff --git a/yt-dlp/yt_dlp/extractor/cda.py b/yt-dlp/yt_dlp/extractor/cda.py index 90b4d082e2..0a5a524c16 100644 --- a/yt-dlp/yt_dlp/extractor/cda.py +++ b/yt-dlp/yt_dlp/extractor/cda.py @@ -16,7 +16,6 @@ from ..utils import ( merge_dicts, multipart_encode, parse_duration, - random_birthday, traverse_obj, try_call, try_get, @@ -63,38 +62,57 @@ class CDAIE(InfoExtractor): 'description': 'md5:60d76b71186dcce4e0ba6d4bbdb13e1a', 'thumbnail': r're:^https?://.*\.jpg$', 'uploader': 'crash404', - 'view_count': int, 'average_rating': float, 'duration': 137, 'age_limit': 0, + 'upload_date': '20160220', + 'timestamp': 1455968218, } }, { - # Age-restricted - 'url': 'http://www.cda.pl/video/1273454c4', + # Age-restricted with vfilm redirection + 'url': 'https://www.cda.pl/video/8753244c4', + 'md5': 'd8eeb83d63611289507010d3df3bb8b3', 'info_dict': { - 'id': '1273454c4', + 'id': '8753244c4', 'ext': 'mp4', - 'title': 'Bronson (2008) napisy HD 1080p', - 'description': 'md5:1b6cb18508daf2dc4e0fa4db77fec24c', + 'title': '[18+] Bez Filtra: Rezerwowe Psy czyli... najwulgarniejsza polska gra?', + 'description': 'md5:ae80bac31bd6a9f077a6cce03c7c077e', 'height': 1080, - 'uploader': 'boniek61', + 'uploader': 'arhn eu', 'thumbnail': r're:^https?://.*\.jpg$', - 'duration': 5554, + 'duration': 991, 'age_limit': 18, - 'view_count': int, 'average_rating': float, - }, + 'timestamp': 1633888264, + 'upload_date': '20211010', + } + }, { + # Age-restricted without vfilm redirection + 'url': 'https://www.cda.pl/video/17028157b8', + 'md5': 'c1fe5ff4582bace95d4f0ce0fbd0f992', + 'info_dict': { + 'id': '17028157b8', + 'ext': 'mp4', + 'title': 'STENDUPY MICHAŁ OGIŃSKI', + 'description': 'md5:5851f3272bfc31f762d616040a1d609a', + 'height': 480, + 'uploader': 'oginski', + 'thumbnail': r're:^https?://.*\.jpg$', + 'duration': 18855, + 'age_limit': 18, + 'average_rating': float, + 'timestamp': 1699705901, + 'upload_date': '20231111', + } }, { 'url': 'http://ebd.cda.pl/0x0/5749950c', 'only_matching': True, }] def _download_age_confirm_page(self, url, video_id, *args, **kwargs): - form_data = random_birthday('rok', 'miesiac', 'dzien') - form_data.update({'return': url, 'module': 'video', 'module_id': video_id}) - data, content_type = multipart_encode(form_data) + data, content_type = multipart_encode({'age_confirm': ''}) return self._download_webpage( - urljoin(url, '/a/validatebirth'), video_id, *args, + url, video_id, *args, data=data, headers={ 'Referer': url, 'Content-Type': content_type, @@ -164,7 +182,7 @@ class CDAIE(InfoExtractor): if 'Authorization' in self._API_HEADERS: return self._api_extract(video_id) else: - return self._web_extract(video_id, url) + return self._web_extract(video_id) def _api_extract(self, video_id): meta = self._download_json( @@ -197,9 +215,9 @@ class CDAIE(InfoExtractor): 'view_count': meta.get('views'), } - def _web_extract(self, video_id, url): + def _web_extract(self, video_id): self._set_cookie('cda.pl', 'cda.player', 'html5') - webpage = self._download_webpage( + webpage, urlh = self._download_webpage_handle( f'{self._BASE_URL}/video/{video_id}/vfilm', video_id) if 'Ten film jest dostępny dla użytkowników premium' in webpage: @@ -209,10 +227,10 @@ class CDAIE(InfoExtractor): self.raise_geo_restricted() need_confirm_age = False - if self._html_search_regex(r'(]+action="[^"]*/a/validatebirth[^"]*")', + if self._html_search_regex(r'(]+name="[^"]*age_confirm[^"]*")', webpage, 'birthday validate form', default=None): webpage = self._download_age_confirm_page( - url, video_id, note='Confirming age') + urlh.url, video_id, note='Confirming age') need_confirm_age = True formats = [] @@ -222,9 +240,6 @@ class CDAIE(InfoExtractor): (?:<\1[^>]*>[^<]*|(?!)(?:.|\n))*? <(span|meta)[^>]+itemprop=(["\'])name\4[^>]*>(?P[^<]+) ''', webpage, 'uploader', default=None, group='uploader') - view_count = self._search_regex( - r'Odsłony:(?:\s| )*([0-9]+)', webpage, - 'view_count', default=None) average_rating = self._search_regex( (r'<(?:span|meta)[^>]+itemprop=(["\'])ratingValue\1[^>]*>(?P[0-9.]+)', r']+\bclass=["\']rating["\'][^>]*>(?P[0-9.]+)'), webpage, 'rating', fatal=False, @@ -235,7 +250,6 @@ class CDAIE(InfoExtractor): 'title': self._og_search_title(webpage), 'description': self._og_search_description(webpage), 'uploader': uploader, - 'view_count': int_or_none(view_count), 'average_rating': float_or_none(average_rating), 'thumbnail': self._og_search_thumbnail(webpage), 'formats': formats, diff --git a/yt-dlp/yt_dlp/extractor/common.py b/yt-dlp/yt_dlp/extractor/common.py index bebbc6b43f..e232aa883a 100644 --- a/yt-dlp/yt_dlp/extractor/common.py +++ b/yt-dlp/yt_dlp/extractor/common.py @@ -957,7 +957,8 @@ class InfoExtractor: if urlh is False: assert not fatal return False - content = self._webpage_read_content(urlh, url_or_request, video_id, note, errnote, fatal, encoding=encoding) + content = self._webpage_read_content(urlh, url_or_request, video_id, note, errnote, fatal, + encoding=encoding, data=data) return (content, urlh) @staticmethod @@ -1005,8 +1006,10 @@ class InfoExtractor: 'Visit http://blocklist.rkn.gov.ru/ for a block reason.', expected=True) - def _request_dump_filename(self, url, video_id): - basen = f'{video_id}_{url}' + def _request_dump_filename(self, url, video_id, data=None): + if data is not None: + data = hashlib.md5(data).hexdigest() + basen = join_nonempty(video_id, data, url, delim='_') trim_length = self.get_param('trim_file_name') or 240 if len(basen) > trim_length: h = '___' + hashlib.md5(basen.encode('utf-8')).hexdigest() @@ -1028,16 +1031,18 @@ class InfoExtractor: except LookupError: return webpage_bytes.decode('utf-8', 'replace') - def _webpage_read_content(self, urlh, url_or_request, video_id, note=None, errnote=None, fatal=True, prefix=None, encoding=None): + def _webpage_read_content(self, urlh, url_or_request, video_id, note=None, errnote=None, fatal=True, + prefix=None, encoding=None, data=None): webpage_bytes = urlh.read() if prefix is not None: webpage_bytes = prefix + webpage_bytes + url_or_request = self._create_request(url_or_request, data) if self.get_param('dump_intermediate_pages', False): self.to_screen('Dumping request to ' + urlh.url) dump = base64.b64encode(webpage_bytes).decode('ascii') self._downloader.to_screen(dump) if self.get_param('write_pages'): - filename = self._request_dump_filename(urlh.url, video_id) + filename = self._request_dump_filename(urlh.url, video_id, url_or_request.data) self.to_screen(f'Saving request to {filename}') with open(filename, 'wb') as outf: outf.write(webpage_bytes) @@ -1098,7 +1103,7 @@ class InfoExtractor: impersonate=None, require_impersonation=False): if self.get_param('load_pages'): url_or_request = self._create_request(url_or_request, data, headers, query) - filename = self._request_dump_filename(url_or_request.url, video_id) + filename = self._request_dump_filename(url_or_request.url, video_id, url_or_request.data) self.to_screen(f'Loading request from {filename}') try: with open(filename, 'rb') as dumpf: diff --git a/yt-dlp/yt_dlp/extractor/tiktok.py b/yt-dlp/yt_dlp/extractor/tiktok.py index 3d965dd452..2fb41ba794 100644 --- a/yt-dlp/yt_dlp/extractor/tiktok.py +++ b/yt-dlp/yt_dlp/extractor/tiktok.py @@ -45,19 +45,18 @@ class TikTokBaseIE(InfoExtractor): # "app id": aweme = 1128, trill = 1180, musical_ly = 1233, universal = 0 'aid': '0', } - _KNOWN_APP_INFO = [ - '7351144126450059040', - '7351149742343391009', - '7351153174894626592', - ] _APP_INFO_POOL = None _APP_INFO = None _APP_USER_AGENT = None + @property + def _KNOWN_APP_INFO(self): + return self._configuration_arg('app_info', ie_key=TikTokIE) + @property def _API_HOSTNAME(self): return self._configuration_arg( - 'api_hostname', ['api22-normal-c-useast2a.tiktokv.com'], ie_key=TikTokIE)[0] + 'api_hostname', ['api16-normal-c-useast1a.tiktokv.com'], ie_key=TikTokIE)[0] def _get_next_app_info(self): if self._APP_INFO_POOL is None: @@ -66,13 +65,10 @@ class TikTokBaseIE(InfoExtractor): for key, default in self._APP_INFO_DEFAULTS.items() if key != 'iid' } - app_info_list = ( - self._configuration_arg('app_info', ie_key=TikTokIE) - or random.sample(self._KNOWN_APP_INFO, len(self._KNOWN_APP_INFO))) self._APP_INFO_POOL = [ {**defaults, **dict( (k, v) for k, v in zip(self._APP_INFO_DEFAULTS, app_info.split('/')) if v - )} for app_info in app_info_list + )} for app_info in self._KNOWN_APP_INFO ] if not self._APP_INFO_POOL: @@ -757,11 +753,13 @@ class TikTokIE(TikTokBaseIE): def _real_extract(self, url): video_id, user_id = self._match_valid_url(url).group('id', 'user_id') - try: - return self._extract_aweme_app(video_id) - except ExtractorError as e: - e.expected = True - self.report_warning(f'{e}; trying with webpage') + + if self._KNOWN_APP_INFO: + try: + return self._extract_aweme_app(video_id) + except ExtractorError as e: + e.expected = True + self.report_warning(f'{e}; trying with webpage') url = self._create_url(user_id, video_id) webpage = self._download_webpage(url, video_id, headers={'User-Agent': 'Mozilla/5.0'}) diff --git a/yt-dlp/yt_dlp/extractor/twitter.py b/yt-dlp/yt_dlp/extractor/twitter.py index ecc865655d..df7f816bd3 100644 --- a/yt-dlp/yt_dlp/extractor/twitter.py +++ b/yt-dlp/yt_dlp/extractor/twitter.py @@ -36,7 +36,7 @@ class TwitterBaseIE(InfoExtractor): _NETRC_MACHINE = 'twitter' _API_BASE = 'https://api.twitter.com/1.1/' _GRAPHQL_API_BASE = 'https://twitter.com/i/api/graphql/' - _BASE_REGEX = r'https?://(?:(?:www|m(?:obile)?)\.)?(?:twitter\.com|twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid\.onion)/' + _BASE_REGEX = r'https?://(?:(?:www|m(?:obile)?)\.)?(?:(?:twitter|x)\.com|twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid\.onion)/' _AUTH = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA' _LEGACY_AUTH = 'AAAAAAAAAAAAAAAAAAAAAIK1zgAAAAAA2tUWuhGZ2JceoId5GwYWU5GspY4%3DUq7gzFoCZs1QfwGoVdvSac3IniczZEYXIcDyumCauIXpcAPorE' _flow_token = None @@ -1191,6 +1191,31 @@ class TwitterIE(TwitterBaseIE): 'age_limit': 0, '_old_archive_ids': ['twitter 1724884212803834154'], }, + }, { + # x.com + 'url': 'https://x.com/historyinmemes/status/1790637656616943991', + 'md5': 'daca3952ba0defe2cfafb1276d4c1ea5', + 'info_dict': { + 'id': '1790637589910654976', + 'ext': 'mp4', + 'title': 'Historic Vids - One of the most intense moments in history', + 'description': 'One of the most intense moments in history https://t.co/Zgzhvix8ES', + 'display_id': '1790637656616943991', + 'uploader': 'Historic Vids', + 'uploader_id': 'historyinmemes', + 'uploader_url': 'https://twitter.com/historyinmemes', + 'channel_id': '855481986290524160', + 'upload_date': '20240515', + 'timestamp': 1715756260.0, + 'duration': 15.488, + 'tags': [], + 'comment_count': int, + 'repost_count': int, + 'like_count': int, + 'thumbnail': r're:https://pbs\.twimg\.com/amplify_video_thumb/.+', + 'age_limit': 0, + '_old_archive_ids': ['twitter 1790637656616943991'], + } }, { # onion route 'url': 'https://twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion/TwitterBlue/status/1484226494708662273', diff --git a/yt-dlp/yt_dlp/extractor/youtube.py b/yt-dlp/yt_dlp/extractor/youtube.py index a5fe179c29..e676c5cde2 100644 --- a/yt-dlp/yt_dlp/extractor/youtube.py +++ b/yt-dlp/yt_dlp/extractor/youtube.py @@ -2353,6 +2353,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'format': '17', # 3gp format available on android 'extractor_args': {'youtube': {'player_client': ['android']}}, }, + 'skip': 'android client broken', }, { # Skip download of additional client configs (remix client config in this case) @@ -2730,7 +2731,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'heatmap': 'count:100', }, 'params': { - 'extractor_args': {'youtube': {'player_client': ['android'], 'player_skip': ['webpage']}}, + 'extractor_args': {'youtube': {'player_client': ['ios'], 'player_skip': ['webpage']}}, }, }, ] @@ -3317,7 +3318,36 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'value': ('intensityScoreNormalized', {float_or_none}), })) or None - def _extract_comment(self, comment_renderer, parent=None): + def _extract_comment(self, entities, parent=None): + comment_entity_payload = get_first(entities, ('payload', 'commentEntityPayload', {dict})) + if not (comment_id := traverse_obj(comment_entity_payload, ('properties', 'commentId', {str}))): + return + + toolbar_entity_payload = get_first(entities, ('payload', 'engagementToolbarStateEntityPayload', {dict})) + time_text = traverse_obj(comment_entity_payload, ('properties', 'publishedTime', {str})) or '' + + return { + 'id': comment_id, + 'parent': parent or 'root', + **traverse_obj(comment_entity_payload, { + 'text': ('properties', 'content', 'content', {str}), + 'like_count': ('toolbar', 'likeCountA11y', {parse_count}), + 'author_id': ('author', 'channelId', {self.ucid_or_none}), + 'author': ('author', 'displayName', {str}), + 'author_thumbnail': ('author', 'avatarThumbnailUrl', {url_or_none}), + 'author_is_uploader': ('author', 'isCreator', {bool}), + 'author_is_verified': ('author', 'isVerified', {bool}), + 'author_url': ('author', 'channelCommand', 'innertubeCommand', ( + ('browseEndpoint', 'canonicalBaseUrl'), ('commandMetadata', 'webCommandMetadata', 'url') + ), {lambda x: urljoin('https://www.youtube.com', x)}), + }, get_all=False), + 'is_favorited': (None if toolbar_entity_payload is None else + toolbar_entity_payload.get('heartState') == 'TOOLBAR_HEART_STATE_HEARTED'), + '_time_text': time_text, # FIXME: non-standard, but we need a way of showing that it is an estimate. + 'timestamp': self._parse_time_text(time_text), + } + + def _extract_comment_old(self, comment_renderer, parent=None): comment_id = comment_renderer.get('commentId') if not comment_id: return @@ -3398,21 +3428,39 @@ class YoutubeIE(YoutubeBaseInfoExtractor): break return _continuation - def extract_thread(contents): + def extract_thread(contents, entity_payloads): if not parent: tracker['current_page_thread'] = 0 for content in contents: if not parent and tracker['total_parent_comments'] >= max_parents: yield comment_thread_renderer = try_get(content, lambda x: x['commentThreadRenderer']) - comment_renderer = get_first( - (comment_thread_renderer, content), [['commentRenderer', ('comment', 'commentRenderer')]], - expected_type=dict, default={}) - comment = self._extract_comment(comment_renderer, parent) + # old comment format + if not entity_payloads: + comment_renderer = get_first( + (comment_thread_renderer, content), [['commentRenderer', ('comment', 'commentRenderer')]], + expected_type=dict, default={}) + + comment = self._extract_comment_old(comment_renderer, parent) + + # new comment format + else: + view_model = ( + traverse_obj(comment_thread_renderer, ('commentViewModel', 'commentViewModel', {dict})) + or traverse_obj(content, ('commentViewModel', {dict}))) + comment_keys = traverse_obj(view_model, (('commentKey', 'toolbarStateKey'), {str})) + if not comment_keys: + continue + entities = traverse_obj(entity_payloads, lambda _, v: v['entityKey'] in comment_keys) + comment = self._extract_comment(entities, parent) + if comment: + comment['is_pinned'] = traverse_obj(view_model, ('pinnedText', {str})) is not None + if not comment: continue comment_id = comment['id'] + if comment.get('is_pinned'): tracker['pinned_comment_ids'].add(comment_id) # Sometimes YouTube may break and give us infinite looping comments. @@ -3505,7 +3553,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): check_get_keys = None if not is_forced_continuation and not (tracker['est_total'] == 0 and tracker['running_total'] == 0): check_get_keys = [[*continuation_items_path, ..., ( - 'commentsHeaderRenderer' if is_first_continuation else ('commentThreadRenderer', 'commentRenderer'))]] + 'commentsHeaderRenderer' if is_first_continuation else ('commentThreadRenderer', 'commentViewModel', 'commentRenderer'))]] try: response = self._extract_response( item_id=None, query=continuation, @@ -3529,6 +3577,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): raise is_forced_continuation = False continuation = None + mutations = traverse_obj(response, ('frameworkUpdates', 'entityBatchUpdate', 'mutations', ..., {dict})) for continuation_items in traverse_obj(response, continuation_items_path, expected_type=list, default=[]): if is_first_continuation: continuation = extract_header(continuation_items) @@ -3537,7 +3586,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): break continue - for entry in extract_thread(continuation_items): + for entry in extract_thread(continuation_items, mutations): if not entry: return yield entry @@ -3614,8 +3663,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor): yt_query = { 'videoId': video_id, } - if _split_innertube_client(client)[0] in ('android', 'android_embedscreen'): - yt_query['params'] = 'CgIIAQ==' pp_arg = self._configuration_arg('player_params', [None], casesense=True)[0] if pp_arg: @@ -3631,19 +3678,24 @@ class YoutubeIE(YoutubeBaseInfoExtractor): def _get_requested_clients(self, url, smuggled_data): requested_clients = [] - default = ['ios', 'android', 'web'] + android_clients = [] + default = ['ios', 'web'] allowed_clients = sorted( (client for client in INNERTUBE_CLIENTS.keys() if client[:1] != '_'), key=lambda client: INNERTUBE_CLIENTS[client]['priority'], reverse=True) for client in self._configuration_arg('player_client'): - if client in allowed_clients: - requested_clients.append(client) - elif client == 'default': + if client == 'default': requested_clients.extend(default) elif client == 'all': requested_clients.extend(allowed_clients) - else: + elif client not in allowed_clients: self.report_warning(f'Skipping unsupported client {client}') + elif client.startswith('android'): + android_clients.append(client) + else: + requested_clients.append(client) + # Force deprioritization of broken Android clients for format de-duplication + requested_clients.extend(android_clients) if not requested_clients: requested_clients = default @@ -3862,6 +3914,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor): f'{video_id}: Some formats are possibly damaged. They will be deprioritized', only_once=True) client_name = fmt.get(STREAMING_DATA_CLIENT_NAME) + # Android client formats are broken due to integrity check enforcement + # Ref: https://github.com/yt-dlp/yt-dlp/issues/9554 + is_broken = client_name and client_name.startswith(short_client_name('android')) + if is_broken: + self.report_warning( + f'{video_id}: Android client formats are broken and may yield HTTP Error 403. ' + 'They will be deprioritized', only_once=True) + name = fmt.get('qualityLabel') or quality.replace('audio_quality_', '') or '' fps = int_or_none(fmt.get('fps')) or 0 dct = { @@ -3874,7 +3934,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): name, fmt.get('isDrc') and 'DRC', try_get(fmt, lambda x: x['projectionType'].replace('RECTANGULAR', '').lower()), try_get(fmt, lambda x: x['spatialAudioType'].replace('SPATIAL_AUDIO_TYPE_', '').lower()), - throttled and 'THROTTLED', is_damaged and 'DAMAGED', + throttled and 'THROTTLED', is_damaged and 'DAMAGED', is_broken and 'BROKEN', (self.get_param('verbose') or all_formats) and client_name, delim=', '), # Format 22 is likely to be damaged. See https://github.com/yt-dlp/yt-dlp/issues/3372 @@ -3892,8 +3952,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'language': join_nonempty(audio_track.get('id', '').split('.')[0], 'desc' if language_preference < -1 else '') or None, 'language_preference': language_preference, - # Strictly de-prioritize damaged and 3gp formats - 'preference': -10 if is_damaged else -2 if itag == '17' else None, + # Strictly de-prioritize broken, damaged and 3gp formats + 'preference': -20 if is_broken else -10 if is_damaged else -2 if itag == '17' else None, } mime_mobj = re.match( r'((?:[^/]+)/(?:[^;]+))(?:;\s*codecs="([^"]+)")?', fmt.get('mimeType') or '')