Update On Fri Sep 12 20:36:09 CEST 2025

This commit is contained in:
github-action[bot]
2025-09-12 20:36:09 +02:00
parent 3d4eb548d6
commit f93a1b54e8
99 changed files with 2766 additions and 4094 deletions

1
.github/update.log vendored
View File

@@ -1118,3 +1118,4 @@ Update On Mon Sep 8 20:41:03 CEST 2025
Update On Tue Sep 9 20:33:51 CEST 2025 Update On Tue Sep 9 20:33:51 CEST 2025
Update On Wed Sep 10 20:42:57 CEST 2025 Update On Wed Sep 10 20:42:57 CEST 2025
Update On Thu Sep 11 20:34:24 CEST 2025 Update On Thu Sep 11 20:34:24 CEST 2025
Update On Fri Sep 12 20:36:01 CEST 2025

View File

@@ -11,6 +11,7 @@ import (
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/ntp"
gost "github.com/metacubex/mihomo/transport/gost-plugin" gost "github.com/metacubex/mihomo/transport/gost-plugin"
"github.com/metacubex/mihomo/transport/restls" "github.com/metacubex/mihomo/transport/restls"
obfs "github.com/metacubex/mihomo/transport/simple-obfs" obfs "github.com/metacubex/mihomo/transport/simple-obfs"
@@ -251,8 +252,9 @@ func (ss *ShadowSocks) SupportUOT() bool {
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
method, err := shadowsocks.CreateMethod(context.Background(), option.Cipher, shadowsocks.MethodOptions{ method, err := shadowsocks.CreateMethod(option.Cipher, shadowsocks.MethodOptions{
Password: option.Password, Password: option.Password,
TimeFunc: ntp.Now,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err) return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err)

View File

@@ -331,13 +331,20 @@ func (cp *CompatibleProvider) Close() error {
} }
func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) { func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) {
var excludeTypeArray []string
if excludeType != "" {
excludeTypeArray = strings.Split(excludeType, "|")
}
var excludeFilterRegs []*regexp2.Regexp
if excludeFilter != "" {
for _, excludeFilter := range strings.Split(excludeFilter, "`") {
excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None) excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid excludeFilter regex: %w", err) return nil, fmt.Errorf("invalid excludeFilter regex: %w", err)
} }
var excludeTypeArray []string excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg)
if excludeType != "" { }
excludeTypeArray = strings.Split(excludeType, "|")
} }
var filterRegs []*regexp2.Regexp var filterRegs []*regexp2.Regexp
@@ -367,8 +374,9 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
proxies := []C.Proxy{} proxies := []C.Proxy{}
proxiesSet := map[string]struct{}{} proxiesSet := map[string]struct{}{}
for _, filterReg := range filterRegs { for _, filterReg := range filterRegs {
LOOP1:
for idx, mapping := range schema.Proxies { for idx, mapping := range schema.Proxies {
if nil != excludeTypeArray && len(excludeTypeArray) > 0 { if len(excludeTypeArray) > 0 {
mType, ok := mapping["type"] mType, ok := mapping["type"]
if !ok { if !ok {
continue continue
@@ -377,18 +385,11 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
if !ok { if !ok {
continue continue
} }
flag := false for _, excludeType := range excludeTypeArray {
for i := range excludeTypeArray { if strings.EqualFold(pType, excludeType) {
if strings.EqualFold(pType, excludeTypeArray[i]) { continue LOOP1
flag = true
break
} }
} }
if flag {
continue
}
} }
mName, ok := mapping["name"] mName, ok := mapping["name"]
if !ok { if !ok {
@@ -398,9 +399,11 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
if !ok { if !ok {
continue continue
} }
if len(excludeFilter) > 0 { if len(excludeFilterRegs) > 0 {
for _, excludeFilterReg := range excludeFilterRegs {
if mat, _ := excludeFilterReg.MatchString(name); mat { if mat, _ := excludeFilterReg.MatchString(name); mat {
continue continue LOOP1
}
} }
} }
if len(filter) > 0 { if len(filter) > 0 {

View File

@@ -24,13 +24,13 @@ require (
github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295 github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295
github.com/metacubex/randv2 v0.2.0 github.com/metacubex/randv2 v0.2.0
github.com/metacubex/restls-client-go v0.1.7 github.com/metacubex/restls-client-go v0.1.7
github.com/metacubex/sing v0.5.6-0.20250904143031-f1a62fab1489 github.com/metacubex/sing v0.5.6-0.20250912172506-82b42a287539
github.com/metacubex/sing-mux v0.3.3 github.com/metacubex/sing-mux v0.3.3
github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231 github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231
github.com/metacubex/sing-shadowsocks v0.2.12 github.com/metacubex/sing-shadowsocks v0.2.12
github.com/metacubex/sing-shadowsocks2 v0.2.6 github.com/metacubex/sing-shadowsocks2 v0.2.7
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
github.com/metacubex/sing-tun v0.4.8-0.20250910070000-df2c1a4be299 github.com/metacubex/sing-tun v0.4.8-0.20250912172659-89eba941fb22
github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115 github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee github.com/metacubex/smux v0.0.0-20250503055512-501391591dee

View File

@@ -117,20 +117,20 @@ github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFq
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k= github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
github.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g= github.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g=
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.6-0.20250904143031-f1a62fab1489 h1:jKOFzhHTbxqhCluh5ONxjDe6CJMNHvgniXAf1RWuzlE= github.com/metacubex/sing v0.5.6-0.20250912172506-82b42a287539 h1:ArXEdw7JvbL3dLc3D7kBGTDmuBBI/sNIyR3O4MlfPH8=
github.com/metacubex/sing v0.5.6-0.20250904143031-f1a62fab1489/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing v0.5.6-0.20250912172506-82b42a287539/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.3 h1:oqCbUAJgTLsa71vfo8otW8xIhrDfbc/Y2rmtW34sQjg= github.com/metacubex/sing-mux v0.3.3 h1:oqCbUAJgTLsa71vfo8otW8xIhrDfbc/Y2rmtW34sQjg=
github.com/metacubex/sing-mux v0.3.3/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw= github.com/metacubex/sing-mux v0.3.3/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231 h1:dGvo7UahC/gYBQNBoictr14baJzBjAKUAorP63QFFtg= github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231 h1:dGvo7UahC/gYBQNBoictr14baJzBjAKUAorP63QFFtg=
github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA= github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA=
github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE= github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE=
github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU= github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU=
github.com/metacubex/sing-shadowsocks2 v0.2.6 h1:ZR1kYT0f0Vi64iQSS09OdhFfppiNkh7kjgRdMm0SB98= github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=
github.com/metacubex/sing-shadowsocks2 v0.2.6/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE= github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.8-0.20250910070000-df2c1a4be299 h1:ytXxmMPndWV0w+yHMwVXjx6CO9AzFdZ1VE0VIjoGjZU= github.com/metacubex/sing-tun v0.4.8-0.20250912172659-89eba941fb22 h1:A/FVt2fbZ1a6elVOP/e3X/1ww7/vvzN5wdS1DJd6Ti8=
github.com/metacubex/sing-tun v0.4.8-0.20250910070000-df2c1a4be299/go.mod h1:e4AyoGUrhiKQjRio3npn87E4TmIk7X5LmeiRwZettUA= github.com/metacubex/sing-tun v0.4.8-0.20250912172659-89eba941fb22/go.mod h1:e4AyoGUrhiKQjRio3npn87E4TmIk7X5LmeiRwZettUA=
github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115 h1:Idk4GoB44BNN1cbjmV5aFHDXjRoV2taSgQypjCEemGM= github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115 h1:Idk4GoB44BNN1cbjmV5aFHDXjRoV2taSgQypjCEemGM=
github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM= github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU= github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=

View File

@@ -231,6 +231,8 @@ func updateNTP(c *config.NTP) {
c.DialerProxy, c.DialerProxy,
c.WriteToSystem, c.WriteToSystem,
) )
} else {
ntp.ReCreateNTPService("", 0, "", false)
} }
} }

View File

@@ -3,6 +3,7 @@ package ntp
import ( import (
"context" "context"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
@@ -13,8 +14,8 @@ import (
"github.com/metacubex/sing/common/ntp" "github.com/metacubex/sing/common/ntp"
) )
var offset time.Duration var globalSrv atomic.Pointer[Service]
var service *Service var globalMu sync.Mutex
type Service struct { type Service struct {
server M.Socksaddr server M.Socksaddr
@@ -22,15 +23,22 @@ type Service struct {
ticker *time.Ticker ticker *time.Ticker
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
mu sync.Mutex mu sync.RWMutex
offset time.Duration
syncSystemTime bool syncSystemTime bool
running bool running bool
} }
func ReCreateNTPService(server string, interval time.Duration, dialerProxy string, syncSystemTime bool) { func ReCreateNTPService(server string, interval time.Duration, dialerProxy string, syncSystemTime bool) {
globalMu.Lock()
defer globalMu.Unlock()
service := globalSrv.Swap(nil)
if service != nil { if service != nil {
service.Stop() service.Stop()
} }
if server == "" {
return
}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
service = &Service{ service = &Service{
server: M.ParseSocksaddr(server), server: M.ParseSocksaddr(server),
@@ -41,6 +49,7 @@ func ReCreateNTPService(server string, interval time.Duration, dialerProxy strin
syncSystemTime: syncSystemTime, syncSystemTime: syncSystemTime,
} }
service.Start() service.Start()
globalSrv.Store(service)
} }
func (srv *Service) Start() { func (srv *Service) Start() {
@@ -52,46 +61,49 @@ func (srv *Service) Start() {
log.Errorln("Initialize NTP time failed: %s", err) log.Errorln("Initialize NTP time failed: %s", err)
return return
} }
service.running = true srv.running = true
go srv.loopUpdate() go srv.loopUpdate()
} }
func (srv *Service) Stop() { func (srv *Service) Stop() {
srv.mu.Lock() srv.mu.Lock()
defer srv.mu.Unlock() defer srv.mu.Unlock()
if service.running { if srv.running {
srv.ticker.Stop() srv.ticker.Stop()
srv.cancel() srv.cancel()
service.running = false srv.running = false
} }
} }
func (srv *Service) Running() bool { func (srv *Service) Offset() time.Duration {
if srv == nil { if srv == nil {
return false return 0
} }
srv.mu.Lock() srv.mu.RLock()
defer srv.mu.Unlock() defer srv.mu.RUnlock()
return srv.running if srv.running {
return srv.offset
}
return 0
} }
func (srv *Service) update() error { func (srv *Service) update() error {
var response *ntp.Response var response *ntp.Response
var err error var err error
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
if response, err = ntp.Exchange(context.Background(), srv.dialer, srv.server); err == nil { response, err = ntp.Exchange(srv.ctx, srv.dialer, srv.server)
break if err != nil {
continue
} }
if i == 2 { offset := response.ClockOffset
return err
}
}
offset = response.ClockOffset
if offset > time.Duration(0) { if offset > time.Duration(0) {
log.Infoln("System clock is ahead of NTP time by %s", offset) log.Infoln("System clock is ahead of NTP time by %s", offset)
} else if offset < time.Duration(0) { } else if offset < time.Duration(0) {
log.Infoln("System clock is behind NTP time by %s", -offset) log.Infoln("System clock is behind NTP time by %s", -offset)
} }
srv.mu.Lock()
srv.offset = offset
srv.mu.Unlock()
if srv.syncSystemTime { if srv.syncSystemTime {
timeNow := response.Time timeNow := response.Time
syncErr := setSystemTime(timeNow) syncErr := setSystemTime(timeNow)
@@ -103,6 +115,8 @@ func (srv *Service) update() error {
} }
} }
return nil return nil
}
return err
} }
func (srv *Service) loopUpdate() { func (srv *Service) loopUpdate() {
@@ -121,7 +135,7 @@ func (srv *Service) loopUpdate() {
func Now() time.Time { func Now() time.Time {
now := time.Now() now := time.Now()
if service.Running() && offset.Abs() > 0 { if offset := globalSrv.Load().Offset(); offset.Abs() > 0 {
now = now.Add(offset) now = now.Add(offset)
} }
return now return now

View File

@@ -2,7 +2,7 @@
"manifest_version": 1, "manifest_version": 1,
"latest": { "latest": {
"mihomo": "v1.19.13", "mihomo": "v1.19.13",
"mihomo_alpha": "alpha-7061c5a", "mihomo_alpha": "alpha-909729c",
"clash_rs": "v0.9.0", "clash_rs": "v0.9.0",
"clash_premium": "2023-09-05-gdcc8d87", "clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.0-alpha+sha.50f295d" "clash_rs_alpha": "0.9.0-alpha+sha.50f295d"
@@ -69,5 +69,5 @@
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf" "linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
} }
}, },
"updated_at": "2025-09-10T22:20:55.939Z" "updated_at": "2025-09-11T22:20:53.894Z"
} }

View File

@@ -13,5 +13,5 @@ func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err
if e != 0 { if e != 0 {
err = e err = e
} }
return return err
} }

View File

@@ -19,5 +19,5 @@ func getsockopt(s, level, name uintptr, val unsafe.Pointer, vallen *uint32) (err
if e != 0 { if e != 0 {
err = e err = e
} }
return return err
} }

View File

@@ -42,15 +42,15 @@ func (e *UnsupportedError) Error() string {
func (o *SocketOptions) ListenUDP() (uconn net.PacketConn, err error) { func (o *SocketOptions) ListenUDP() (uconn net.PacketConn, err error) {
uconn, err = net.ListenUDP("udp", nil) uconn, err = net.ListenUDP("udp", nil)
if err != nil { if err != nil {
return return uconn, err
} }
err = o.applyToUDPConn(uconn.(*net.UDPConn)) err = o.applyToUDPConn(uconn.(*net.UDPConn))
if err != nil { if err != nil {
uconn.Close() uconn.Close()
uconn = nil uconn = nil
return return uconn, err
} }
return return uconn, err
} }
func (o *SocketOptions) applyToUDPConn(c *net.UDPConn) error { func (o *SocketOptions) applyToUDPConn(c *net.UDPConn) error {

View File

@@ -24,19 +24,19 @@ func init() {
func controlUDPConn(c *net.UDPConn, cb func(fd int) error) (err error) { func controlUDPConn(c *net.UDPConn, cb func(fd int) error) (err error) {
rconn, err := c.SyscallConn() rconn, err := c.SyscallConn()
if err != nil { if err != nil {
return return err
} }
cerr := rconn.Control(func(fd uintptr) { cerr := rconn.Control(func(fd uintptr) {
err = cb(int(fd)) err = cb(int(fd))
}) })
if err != nil { if err != nil {
return return err
} }
if cerr != nil { if cerr != nil {
err = fmt.Errorf("failed to control fd: %w", cerr) err = fmt.Errorf("failed to control fd: %w", cerr)
return return err
} }
return return err
} }
func bindInterfaceImpl(c *net.UDPConn, device string) error { func bindInterfaceImpl(c *net.UDPConn, device string) error {

View File

@@ -42,12 +42,12 @@ func Test_fdControlUnixSocketImpl(t *testing.T) {
err = controlUDPConn(conn.(*net.UDPConn), func(fd int) (err error) { err = controlUDPConn(conn.(*net.UDPConn), func(fd int) (err error) {
rcvbuf, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF) rcvbuf, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF)
if err != nil { if err != nil {
return return err
} }
// The test server called setsockopt(fd, SOL_SOCKET, SO_RCVBUF, 2500), // The test server called setsockopt(fd, SOL_SOCKET, SO_RCVBUF, 2500),
// and kernel will double this value for getsockopt(). // and kernel will double this value for getsockopt().
assert.Equal(t, 5000, rcvbuf) assert.Equal(t, 5000, rcvbuf)
return return err
}) })
assert.NoError(t, err) assert.NoError(t, err)
} }

View File

@@ -1173,7 +1173,7 @@ func splitHostPort(hostPort string) (host, port string) {
host = host[1 : len(host)-1] host = host[1 : len(host)-1]
} }
return return host, port
} }
// Marshaling interface implementations. // Marshaling interface implementations.
@@ -1263,8 +1263,8 @@ func stringContainsCTLByte(s string) bool {
func JoinPath(base string, elem ...string) (result string, err error) { func JoinPath(base string, elem ...string) (result string, err error) {
url, err := Parse(base) url, err := Parse(base)
if err != nil { if err != nil {
return return result, err
} }
result = url.JoinPath(elem...).String() result = url.JoinPath(elem...).String()
return return result, err
} }

View File

@@ -68,17 +68,17 @@ func (l *LocalCertificateLoader) checkModTime() (certModTime, keyModTime time.Ti
fi, err := os.Stat(l.CertFile) fi, err := os.Stat(l.CertFile)
if err != nil { if err != nil {
err = fmt.Errorf("failed to stat certificate file: %w", err) err = fmt.Errorf("failed to stat certificate file: %w", err)
return return certModTime, keyModTime, err
} }
certModTime = fi.ModTime() certModTime = fi.ModTime()
fi, err = os.Stat(l.KeyFile) fi, err = os.Stat(l.KeyFile)
if err != nil { if err != nil {
err = fmt.Errorf("failed to stat key file: %w", err) err = fmt.Errorf("failed to stat key file: %w", err)
return return certModTime, keyModTime, err
} }
keyModTime = fi.ModTime() keyModTime = fi.ModTime()
return return certModTime, keyModTime, err
} }
func (l *LocalCertificateLoader) makeCache() (cache *localCertificateCache, err error) { func (l *LocalCertificateLoader) makeCache() (cache *localCertificateCache, err error) {
@@ -86,24 +86,24 @@ func (l *LocalCertificateLoader) makeCache() (cache *localCertificateCache, err
c.certModTime, c.keyModTime, err = l.checkModTime() c.certModTime, c.keyModTime, err = l.checkModTime()
if err != nil { if err != nil {
return return cache, err
} }
cert, err := tls.LoadX509KeyPair(l.CertFile, l.KeyFile) cert, err := tls.LoadX509KeyPair(l.CertFile, l.KeyFile)
if err != nil { if err != nil {
return return cache, err
} }
c.certificate = &cert c.certificate = &cert
if c.certificate.Leaf == nil { if c.certificate.Leaf == nil {
// certificate.Leaf was left nil by tls.LoadX509KeyPair before Go 1.23 // certificate.Leaf was left nil by tls.LoadX509KeyPair before Go 1.23
c.certificate.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) c.certificate.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil { if err != nil {
return return cache, err
} }
} }
cache = c cache = c
return return cache, err
} }
func (l *LocalCertificateLoader) getCertificateWithCache() (*tls.Certificate, error) { func (l *LocalCertificateLoader) getCertificateWithCache() (*tls.Certificate, error) {

View File

@@ -60,7 +60,7 @@ func newUDPSessionEntry(
ExitFunc: exitFunc, ExitFunc: exitFunc,
} }
return return e
} }
// CloseWithErr closes the session and calls ExitFunc with the given error. // CloseWithErr closes the session and calls ExitFunc with the given error.
@@ -259,10 +259,9 @@ func (m *udpSessionManager) idleCleanupLoop(stopCh <-chan struct{}) {
} }
func (m *udpSessionManager) cleanup(idleOnly bool) { func (m *udpSessionManager) cleanup(idleOnly bool) {
timeoutEntry := make([]*udpSessionEntry, 0, len(m.m))
// We use RLock here as we are only scanning the map, not deleting from it. // We use RLock here as we are only scanning the map, not deleting from it.
m.mutex.RLock() m.mutex.RLock()
timeoutEntry := make([]*udpSessionEntry, 0, len(m.m))
now := time.Now() now := time.Now()
for _, entry := range m.m { for _, entry := range m.m {
if !idleOnly || now.Sub(entry.Last.Get()) > m.idleTimeout { if !idleOnly || now.Sub(entry.Last.Get()) > m.idleTimeout {
@@ -289,14 +288,14 @@ func (m *udpSessionManager) feed(msg *protocol.UDPMessage) {
// Call the hook // Call the hook
err = m.io.Hook(firstMsgData, &addr) err = m.io.Hook(firstMsgData, &addr)
if err != nil { if err != nil {
return return conn, actualAddr, err
} }
actualAddr = addr actualAddr = addr
// Log the event // Log the event
m.eventLogger.New(msg.SessionID, addr) m.eventLogger.New(msg.SessionID, addr)
// Dial target // Dial target
conn, err = m.io.UDP(addr) conn, err = m.io.UDP(addr)
return return conn, actualAddr, err
} }
exitFunc := func(err error) { exitFunc := func(err error) {
// Log the event // Log the event

View File

@@ -64,12 +64,12 @@ func (c *obfsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, addr, err = c.Conn.ReadFrom(c.readBuf) n, addr, err = c.Conn.ReadFrom(c.readBuf)
if n <= 0 { if n <= 0 {
c.readMutex.Unlock() c.readMutex.Unlock()
return return n, addr, err
} }
n = c.Obfs.Deobfuscate(c.readBuf[:n], p) n = c.Obfs.Deobfuscate(c.readBuf[:n], p)
c.readMutex.Unlock() c.readMutex.Unlock()
if n > 0 || err != nil { if n > 0 || err != nil {
return return n, addr, err
} }
// Invalid packet, try again // Invalid packet, try again
} }
@@ -83,7 +83,7 @@ func (c *obfsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
if err == nil { if err == nil {
n = len(p) n = len(p)
} }
return return n, err
} }
func (c *obfsPacketConn) Close() error { func (c *obfsPacketConn) Close() error {

View File

@@ -258,7 +258,7 @@ func addrExToSOCKS5Addr(addr *AddrEx) (atyp byte, dstAddr, dstPort []byte) {
// Port // Port
dstPort = make([]byte, 2) dstPort = make([]byte, 2)
binary.BigEndian.PutUint16(dstPort, addr.Port) binary.BigEndian.PutUint16(dstPort, addr.Port)
return return atyp, dstAddr, dstPort
} }
func socks5AddrToAddrEx(atyp byte, dstAddr, dstPort []byte) *AddrEx { func socks5AddrToAddrEx(atyp byte, dstAddr, dstPort []byte) *AddrEx {

View File

@@ -20,7 +20,7 @@ func splitIPv4IPv6(ips []net.IP) (ipv4, ipv6 net.IP) {
break break
} }
} }
return return ipv4, ipv6
} }
// tryParseIP tries to parse the host string in the AddrEx as an IP address. // tryParseIP tries to parse the host string in the AddrEx as an IP address.

View File

@@ -1,12 +1,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=r8125 PKG_NAME:=r8125
PKG_VERSION:=9.016.00 PKG_VERSION:=9.016.01
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.bz2 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.bz2
PKG_SOURCE_URL:=https://github.com/openwrt/rtl8125/releases/download/$(PKG_VERSION) PKG_SOURCE_URL:=https://github.com/openwrt/rtl8125/releases/download/$(PKG_VERSION)
PKG_HASH:=cd1955dd07d2f5a6faaa210ffc4e8af992421295a32ab6ddcfa759bed9eba922 PKG_HASH:=5434b26500538a62541c55cd09eea099177f59bd9cc48d16969089a9bcdbbd41
PKG_BUILD_PARALLEL:=1 PKG_BUILD_PARALLEL:=1
PKG_LICENSE:=GPLv2 PKG_LICENSE:=GPLv2

View File

@@ -37,7 +37,7 @@ Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
#include <linux/netdevice.h> #include <linux/netdevice.h>
#include <linux/etherdevice.h> #include <linux/etherdevice.h>
#include <linux/delay.h> #include <linux/delay.h>
@@ -5045,6 +5046,38 @@ rtl8125_link_down_patch(struct net_devic @@ -5051,6 +5052,38 @@ rtl8125_link_down_patch(struct net_devic
#endif #endif
} }
@@ -76,7 +76,7 @@ Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
static void static void
_rtl8125_check_link_status(struct net_device *dev, unsigned int link_state) _rtl8125_check_link_status(struct net_device *dev, unsigned int link_state)
{ {
@@ -5057,11 +5090,18 @@ _rtl8125_check_link_status(struct net_de @@ -5063,11 +5096,18 @@ _rtl8125_check_link_status(struct net_de
if (link_state == R8125_LINK_STATE_ON) { if (link_state == R8125_LINK_STATE_ON) {
rtl8125_link_on_patch(dev); rtl8125_link_on_patch(dev);

View File

@@ -8,7 +8,7 @@
#include <linux/if_vlan.h> #include <linux/if_vlan.h>
#include <linux/crc32.h> #include <linux/crc32.h>
#include <linux/interrupt.h> #include <linux/interrupt.h>
@@ -14808,6 +14809,22 @@ rtl8125_restore_phy_fuse_dout(struct rtl @@ -14801,6 +14802,22 @@ rtl8125_restore_phy_fuse_dout(struct rtl
} }
static void static void
@@ -31,7 +31,7 @@
rtl8125_init_software_variable(struct net_device *dev) rtl8125_init_software_variable(struct net_device *dev)
{ {
struct rtl8125_private *tp = netdev_priv(dev); struct rtl8125_private *tp = netdev_priv(dev);
@@ -15309,6 +15326,7 @@ rtl8125_init_software_variable(struct ne @@ -15316,6 +15333,7 @@ rtl8125_init_software_variable(struct ne
else if (tp->InitRxDescType == RX_DESC_RING_TYPE_4) else if (tp->InitRxDescType == RX_DESC_RING_TYPE_4)
tp->rtl8125_rx_config &= ~EnableRxDescV4_1; tp->rtl8125_rx_config &= ~EnableRxDescV4_1;

View File

@@ -1,12 +1,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=r8126 PKG_NAME:=r8126
PKG_VERSION:=10.015.00 PKG_VERSION:=10.016.00
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.bz2 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.bz2
PKG_SOURCE_URL:=https://github.com/openwrt/rtl8126/releases/download/$(PKG_VERSION) PKG_SOURCE_URL:=https://github.com/openwrt/rtl8126/releases/download/$(PKG_VERSION)
PKG_HASH:=fac513aa925264a95b053e7532fcda56022d29db288f6625fafee2759a8a6124 PKG_HASH:=50c8d3d49592d2e8f372bd7ece8e7df9b50a71b055c077d42eacc42302914440
PKG_BUILD_PARALLEL:=1 PKG_BUILD_PARALLEL:=1
PKG_LICENSE:=GPLv2 PKG_LICENSE:=GPLv2

View File

@@ -1,116 +0,0 @@
--- a/src/r8126_n.c
+++ b/src/r8126_n.c
@@ -6929,7 +6929,11 @@ rtl8126_device_lpi_t_to_ethtool_lpi_t(st
}
static int
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,9,0)
+rtl_ethtool_get_eee(struct net_device *net, struct ethtool_keee *edata)
+#else
rtl_ethtool_get_eee(struct net_device *net, struct ethtool_eee *edata)
+#endif
{
struct rtl8126_private *tp = netdev_priv(net);
struct ethtool_eee *eee = &tp->eee;
@@ -6962,9 +6966,15 @@ rtl_ethtool_get_eee(struct net_device *n
edata->eee_enabled = !!val;
edata->eee_active = !!(supported & adv & lp);
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,9,0)
+ ethtool_convert_legacy_u32_to_link_mode(edata->supported, supported);
+ ethtool_convert_legacy_u32_to_link_mode(edata->advertised, adv);
+ ethtool_convert_legacy_u32_to_link_mode(edata->lp_advertised, lp);
+#else
edata->supported = supported;
edata->advertised = adv;
edata->lp_advertised = lp;
+#endif
edata->tx_lpi_enabled = edata->eee_enabled;
edata->tx_lpi_timer = tx_lpi_timer;
@@ -6972,11 +6982,19 @@ rtl_ethtool_get_eee(struct net_device *n
}
static int
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,9,0)
+rtl_ethtool_set_eee(struct net_device *net, struct ethtool_keee *edata)
+#else
rtl_ethtool_set_eee(struct net_device *net, struct ethtool_eee *edata)
+#endif
{
struct rtl8126_private *tp = netdev_priv(net);
struct ethtool_eee *eee = &tp->eee;
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,9,0)
+ u32 advertising, adv;
+#else
u32 advertising;
+#endif
int rc = 0;
if (!HW_HAS_WRITE_PHY_MCU_RAM_CODE(tp) ||
@@ -7008,6 +7026,18 @@ rtl_ethtool_set_eee(struct net_device *n
*/
advertising = tp->advertising;
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,9,0)
+ ethtool_convert_link_mode_to_legacy_u32(&adv, edata->advertised);
+ if (linkmode_empty(edata->advertised)) {
+ adv = advertising & eee->supported;
+ ethtool_convert_legacy_u32_to_link_mode(edata->advertised, adv);
+ } else if (!linkmode_empty(edata->advertised) & ~advertising) {
+ dev_printk(KERN_WARNING, tp_to_dev(tp), "EEE advertised %x must be a subset of autoneg advertised speeds %x\n",
+ adv, advertising);
+ rc = -EINVAL;
+ goto out;
+ }
+#else
if (!edata->advertised) {
edata->advertised = advertising & eee->supported;
} else if (edata->advertised & ~advertising) {
@@ -7016,13 +7046,23 @@ rtl_ethtool_set_eee(struct net_device *n
rc = -EINVAL;
goto out;
}
+#endif
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,9,0)
+ if (!linkmode_empty(edata->advertised) & ~eee->supported) {
+ dev_printk(KERN_WARNING, tp_to_dev(tp), "EEE advertised %x must be a subset of support %x\n",
+ adv, eee->supported);
+ rc = -EINVAL;
+ goto out;
+ }
+#else
if (edata->advertised & ~eee->supported) {
dev_printk(KERN_WARNING, tp_to_dev(tp), "EEE advertised %x must be a subset of support %x\n",
edata->advertised, eee->supported);
rc = -EINVAL;
goto out;
}
+#endif
//tp->eee.eee_enabled = edata->eee_enabled;
//tp->eee_adv_t = ethtool_adv_to_mmd_eee_adv_t(edata->advertised);
@@ -7030,7 +7070,11 @@ rtl_ethtool_set_eee(struct net_device *n
dev_printk(KERN_WARNING, tp_to_dev(tp), "EEE tx_lpi_timer %x must be a subset of support %x\n",
edata->tx_lpi_timer, eee->tx_lpi_timer);
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(6,9,0)
+ ethtool_convert_link_mode_to_legacy_u32(&eee->advertised, edata->advertised);
+#else
eee->advertised = edata->advertised;
+#endif
//eee->tx_lpi_enabled = edata->tx_lpi_enabled;
//eee->tx_lpi_timer = edata->tx_lpi_timer;
eee->eee_enabled = edata->eee_enabled;
@@ -7106,8 +7150,10 @@ static const struct ethtool_ops rtl8126_
.set_rxnfc = rtl8126_set_rxnfc,
.get_rxfh_indir_size = rtl8126_rss_indir_size,
.get_rxfh_key_size = rtl8126_get_rxfh_key_size,
+#if LINUX_VERSION_CODE < KERNEL_VERSION(6,9,0)
.get_rxfh = rtl8126_get_rxfh,
.set_rxfh = rtl8126_set_rxfh,
+#endif
#endif //ENABLE_RSS_SUPPORT
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3,5,0)
#ifdef ENABLE_PTP_SUPPORT

View File

@@ -17,7 +17,7 @@ Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
--- a/src/r8126.h --- a/src/r8126.h
+++ b/src/r8126.h +++ b/src/r8126.h
@@ -1756,6 +1756,10 @@ enum RTL8126_register_content { @@ -1752,6 +1752,10 @@ enum RTL8126_register_content {
LinkStatus = 0x02, LinkStatus = 0x02,
FullDup = 0x01, FullDup = 0x01,
@@ -38,8 +38,8 @@ Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
#include <linux/netdevice.h> #include <linux/netdevice.h>
#include <linux/etherdevice.h> #include <linux/etherdevice.h>
#include <linux/delay.h> #include <linux/delay.h>
@@ -4661,6 +4662,40 @@ rtl8126_link_down_patch(struct net_devic @@ -4410,6 +4411,40 @@ rtl8126_link_down_patch(struct net_devic
#endif //rtl8126_set_speed(dev, tp->autoneg, tp->speed, tp->duplex, tp->advertising);
} }
+static unsigned int rtl8126_phy_duplex(u32 status) +static unsigned int rtl8126_phy_duplex(u32 status)
@@ -79,7 +79,7 @@ Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
static void static void
_rtl8126_check_link_status(struct net_device *dev, unsigned int link_state) _rtl8126_check_link_status(struct net_device *dev, unsigned int link_state)
{ {
@@ -4673,11 +4708,18 @@ _rtl8126_check_link_status(struct net_de @@ -4422,11 +4457,18 @@ _rtl8126_check_link_status(struct net_de
if (link_state == R8126_LINK_STATE_ON) { if (link_state == R8126_LINK_STATE_ON) {
rtl8126_link_on_patch(dev); rtl8126_link_on_patch(dev);

View File

@@ -0,0 +1,141 @@
// Copyright (C) 2025 mieru authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package common
import (
"bytes"
"fmt"
"io"
"net"
"strings"
"sync"
"time"
"github.com/enfein/mieru/v3/apis/constant"
"github.com/enfein/mieru/v3/apis/model"
)
// EarlyConn implements net.Conn interface.
// When the Write() method on the net.Conn is called for the first time,
// it performs the initial handshake and writes the request to the server.
type EarlyConn struct {
net.Conn
handshakeOnce sync.Once
handshakeErr error
handshaked chan struct{}
netAddrSpec model.NetAddrSpec
}
// NewEarlyConn creates a new EarlyConn.
func NewEarlyConn(conn net.Conn, netAddrSpec model.NetAddrSpec) *EarlyConn {
return &EarlyConn{
Conn: conn,
handshaked: make(chan struct{}),
netAddrSpec: netAddrSpec,
}
}
// Read will block until a message is received or an error occurs.
// It waits for the handshake to complete.
func (c *EarlyConn) Read(b []byte) (n int, err error) {
<-c.handshaked
if c.handshakeErr != nil {
return 0, c.handshakeErr
}
return c.Conn.Read(b)
}
// Write will block until the message is sent or an error occurs.
// It triggers the initial handshake if it has not been performed yet,
// and sends the data in the same packet as the handshake request.
func (c *EarlyConn) Write(b []byte) (n int, err error) {
var writtenDuringHandshake bool
c.handshakeOnce.Do(func() {
c.handshakeErr = c.doHandshakeAndWrite(b)
close(c.handshaked)
writtenDuringHandshake = true
})
if c.handshakeErr != nil {
return 0, c.handshakeErr
}
if writtenDuringHandshake {
return len(b), nil
}
return c.Conn.Write(b)
}
func (c *EarlyConn) Close() error {
c.handshakeOnce.Do(func() {
close(c.handshaked) // unblock Read() method
})
return c.Conn.Close()
}
// NeedHandshake returns true if the handshake has not been performed yet.
func (c *EarlyConn) NeedHandshake() bool {
select {
case <-c.handshaked:
return false
default:
return true
}
}
func (c *EarlyConn) doHandshakeAndWrite(b []byte) error {
var req bytes.Buffer
isTCP := strings.HasPrefix(c.netAddrSpec.Network(), "tcp")
isUDP := strings.HasPrefix(c.netAddrSpec.Network(), "udp")
if isTCP {
req.Write([]byte{constant.Socks5Version, constant.Socks5ConnectCmd, 0})
} else if isUDP {
req.Write([]byte{constant.Socks5Version, constant.Socks5UDPAssociateCmd, 0})
} else {
return fmt.Errorf("unsupported network type %s", c.netAddrSpec.Network())
}
if err := c.netAddrSpec.WriteToSocks5(&req); err != nil {
return err
}
if len(b) > 0 {
req.Write(b)
}
if _, err := c.Conn.Write(req.Bytes()); err != nil {
return fmt.Errorf("failed to write socks5 connection request to the server: %w", err)
}
c.Conn.SetReadDeadline(time.Now().Add(10 * time.Second))
defer func() {
c.Conn.SetReadDeadline(time.Time{})
}()
resp := make([]byte, 3)
if _, err := io.ReadFull(c.Conn, resp); err != nil {
return fmt.Errorf("failed to read socks5 connection response from the server: %w", err)
}
var respAddr model.NetAddrSpec
if err := respAddr.ReadFromSocks5(c.Conn); err != nil {
return fmt.Errorf("failed to read socks5 connection address response from the server: %w", err)
}
if resp[1] != 0 {
return fmt.Errorf("server returned socks5 error code %d", resp[1])
}
return nil
}

View File

@@ -0,0 +1,106 @@
// Copyright (C) 2025 mieru authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package common_test
import (
"io"
"sync"
"testing"
"github.com/enfein/mieru/v3/apis/common"
"github.com/enfein/mieru/v3/apis/constant"
"github.com/enfein/mieru/v3/apis/model"
"github.com/enfein/mieru/v3/pkg/testtool"
)
func TestEarlyConn(t *testing.T) {
clientConn, serverConn := testtool.BufPipe()
var wg sync.WaitGroup
wg.Add(1)
// Run a fake socks5 server.
go func() {
defer wg.Done()
defer serverConn.Close()
// Read and discard socks5 request header.
reqHeader := make([]byte, 3)
if _, err := io.ReadFull(serverConn, reqHeader); err != nil {
t.Errorf("server: failed to read request header: %v", err)
return
}
// Read destination address.
var addr model.NetAddrSpec
if err := addr.ReadFromSocks5(serverConn); err != nil {
t.Errorf("server: failed to read destination address: %v", err)
return
}
// Send reply using a dummy IPv4 address 0.0.0.0:0.
reply := []byte{constant.Socks5Version, 0, 0, constant.Socks5IPv4Address, 0, 0, 0, 0, 0, 0}
if _, err := serverConn.Write(reply); err != nil {
t.Errorf("server: failed to write reply: %v", err)
return
}
// Read client data ("ping").
ping := make([]byte, 4)
if _, err := io.ReadFull(serverConn, ping); err != nil {
t.Errorf("server: failed to read data: %v", err)
return
}
if string(ping) != "ping" {
t.Errorf("server: expected client to send 'ping', got '%s'", string(ping))
return
}
// Send server data ("pong").
if _, err := serverConn.Write([]byte("pong")); err != nil {
t.Errorf("server: failed to write data: %v", err)
return
}
}()
// Create client early connection.
target := model.NetAddrSpec{
AddrSpec: model.AddrSpec{
FQDN: "example.com",
Port: 80,
},
Net: "tcp",
}
conn := common.NewEarlyConn(clientConn, target)
defer conn.Close()
// The first write triggers the handshake.
if _, err := conn.Write([]byte("ping")); err != nil {
t.Fatalf("client: failed to write data: %v", err)
}
// The server should respond with "pong" after the handshake is complete.
pong := make([]byte, 4)
if _, err := io.ReadFull(conn, pong); err != nil {
t.Fatalf("client: failed to read data: %v", err)
}
if string(pong) != "pong" {
t.Fatalf("client: expected server to send 'pong', got '%s'", string(pong))
}
wg.Wait()
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/ntp"
gost "github.com/metacubex/mihomo/transport/gost-plugin" gost "github.com/metacubex/mihomo/transport/gost-plugin"
"github.com/metacubex/mihomo/transport/restls" "github.com/metacubex/mihomo/transport/restls"
obfs "github.com/metacubex/mihomo/transport/simple-obfs" obfs "github.com/metacubex/mihomo/transport/simple-obfs"
@@ -251,8 +252,9 @@ func (ss *ShadowSocks) SupportUOT() bool {
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
method, err := shadowsocks.CreateMethod(context.Background(), option.Cipher, shadowsocks.MethodOptions{ method, err := shadowsocks.CreateMethod(option.Cipher, shadowsocks.MethodOptions{
Password: option.Password, Password: option.Password,
TimeFunc: ntp.Now,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err) return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err)

View File

@@ -331,13 +331,20 @@ func (cp *CompatibleProvider) Close() error {
} }
func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) { func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) {
var excludeTypeArray []string
if excludeType != "" {
excludeTypeArray = strings.Split(excludeType, "|")
}
var excludeFilterRegs []*regexp2.Regexp
if excludeFilter != "" {
for _, excludeFilter := range strings.Split(excludeFilter, "`") {
excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None) excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid excludeFilter regex: %w", err) return nil, fmt.Errorf("invalid excludeFilter regex: %w", err)
} }
var excludeTypeArray []string excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg)
if excludeType != "" { }
excludeTypeArray = strings.Split(excludeType, "|")
} }
var filterRegs []*regexp2.Regexp var filterRegs []*regexp2.Regexp
@@ -367,8 +374,9 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
proxies := []C.Proxy{} proxies := []C.Proxy{}
proxiesSet := map[string]struct{}{} proxiesSet := map[string]struct{}{}
for _, filterReg := range filterRegs { for _, filterReg := range filterRegs {
LOOP1:
for idx, mapping := range schema.Proxies { for idx, mapping := range schema.Proxies {
if nil != excludeTypeArray && len(excludeTypeArray) > 0 { if len(excludeTypeArray) > 0 {
mType, ok := mapping["type"] mType, ok := mapping["type"]
if !ok { if !ok {
continue continue
@@ -377,18 +385,11 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
if !ok { if !ok {
continue continue
} }
flag := false for _, excludeType := range excludeTypeArray {
for i := range excludeTypeArray { if strings.EqualFold(pType, excludeType) {
if strings.EqualFold(pType, excludeTypeArray[i]) { continue LOOP1
flag = true
break
} }
} }
if flag {
continue
}
} }
mName, ok := mapping["name"] mName, ok := mapping["name"]
if !ok { if !ok {
@@ -398,9 +399,11 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
if !ok { if !ok {
continue continue
} }
if len(excludeFilter) > 0 { if len(excludeFilterRegs) > 0 {
for _, excludeFilterReg := range excludeFilterRegs {
if mat, _ := excludeFilterReg.MatchString(name); mat { if mat, _ := excludeFilterReg.MatchString(name); mat {
continue continue LOOP1
}
} }
} }
if len(filter) > 0 { if len(filter) > 0 {

View File

@@ -24,13 +24,13 @@ require (
github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295 github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295
github.com/metacubex/randv2 v0.2.0 github.com/metacubex/randv2 v0.2.0
github.com/metacubex/restls-client-go v0.1.7 github.com/metacubex/restls-client-go v0.1.7
github.com/metacubex/sing v0.5.6-0.20250904143031-f1a62fab1489 github.com/metacubex/sing v0.5.6-0.20250912172506-82b42a287539
github.com/metacubex/sing-mux v0.3.3 github.com/metacubex/sing-mux v0.3.3
github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231 github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231
github.com/metacubex/sing-shadowsocks v0.2.12 github.com/metacubex/sing-shadowsocks v0.2.12
github.com/metacubex/sing-shadowsocks2 v0.2.6 github.com/metacubex/sing-shadowsocks2 v0.2.7
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
github.com/metacubex/sing-tun v0.4.8-0.20250910070000-df2c1a4be299 github.com/metacubex/sing-tun v0.4.8-0.20250912172659-89eba941fb22
github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115 github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee github.com/metacubex/smux v0.0.0-20250503055512-501391591dee

View File

@@ -117,20 +117,20 @@ github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFq
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k= github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
github.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g= github.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g=
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.6-0.20250904143031-f1a62fab1489 h1:jKOFzhHTbxqhCluh5ONxjDe6CJMNHvgniXAf1RWuzlE= github.com/metacubex/sing v0.5.6-0.20250912172506-82b42a287539 h1:ArXEdw7JvbL3dLc3D7kBGTDmuBBI/sNIyR3O4MlfPH8=
github.com/metacubex/sing v0.5.6-0.20250904143031-f1a62fab1489/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing v0.5.6-0.20250912172506-82b42a287539/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.3 h1:oqCbUAJgTLsa71vfo8otW8xIhrDfbc/Y2rmtW34sQjg= github.com/metacubex/sing-mux v0.3.3 h1:oqCbUAJgTLsa71vfo8otW8xIhrDfbc/Y2rmtW34sQjg=
github.com/metacubex/sing-mux v0.3.3/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw= github.com/metacubex/sing-mux v0.3.3/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231 h1:dGvo7UahC/gYBQNBoictr14baJzBjAKUAorP63QFFtg= github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231 h1:dGvo7UahC/gYBQNBoictr14baJzBjAKUAorP63QFFtg=
github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA= github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA=
github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE= github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE=
github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU= github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU=
github.com/metacubex/sing-shadowsocks2 v0.2.6 h1:ZR1kYT0f0Vi64iQSS09OdhFfppiNkh7kjgRdMm0SB98= github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=
github.com/metacubex/sing-shadowsocks2 v0.2.6/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE= github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.8-0.20250910070000-df2c1a4be299 h1:ytXxmMPndWV0w+yHMwVXjx6CO9AzFdZ1VE0VIjoGjZU= github.com/metacubex/sing-tun v0.4.8-0.20250912172659-89eba941fb22 h1:A/FVt2fbZ1a6elVOP/e3X/1ww7/vvzN5wdS1DJd6Ti8=
github.com/metacubex/sing-tun v0.4.8-0.20250910070000-df2c1a4be299/go.mod h1:e4AyoGUrhiKQjRio3npn87E4TmIk7X5LmeiRwZettUA= github.com/metacubex/sing-tun v0.4.8-0.20250912172659-89eba941fb22/go.mod h1:e4AyoGUrhiKQjRio3npn87E4TmIk7X5LmeiRwZettUA=
github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115 h1:Idk4GoB44BNN1cbjmV5aFHDXjRoV2taSgQypjCEemGM= github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115 h1:Idk4GoB44BNN1cbjmV5aFHDXjRoV2taSgQypjCEemGM=
github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM= github.com/metacubex/sing-vmess v0.2.4-0.20250908094854-bc8e2a88b115/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU= github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=

View File

@@ -231,6 +231,8 @@ func updateNTP(c *config.NTP) {
c.DialerProxy, c.DialerProxy,
c.WriteToSystem, c.WriteToSystem,
) )
} else {
ntp.ReCreateNTPService("", 0, "", false)
} }
} }

View File

@@ -3,6 +3,7 @@ package ntp
import ( import (
"context" "context"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
@@ -13,8 +14,8 @@ import (
"github.com/metacubex/sing/common/ntp" "github.com/metacubex/sing/common/ntp"
) )
var offset time.Duration var globalSrv atomic.Pointer[Service]
var service *Service var globalMu sync.Mutex
type Service struct { type Service struct {
server M.Socksaddr server M.Socksaddr
@@ -22,15 +23,22 @@ type Service struct {
ticker *time.Ticker ticker *time.Ticker
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
mu sync.Mutex mu sync.RWMutex
offset time.Duration
syncSystemTime bool syncSystemTime bool
running bool running bool
} }
func ReCreateNTPService(server string, interval time.Duration, dialerProxy string, syncSystemTime bool) { func ReCreateNTPService(server string, interval time.Duration, dialerProxy string, syncSystemTime bool) {
globalMu.Lock()
defer globalMu.Unlock()
service := globalSrv.Swap(nil)
if service != nil { if service != nil {
service.Stop() service.Stop()
} }
if server == "" {
return
}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
service = &Service{ service = &Service{
server: M.ParseSocksaddr(server), server: M.ParseSocksaddr(server),
@@ -41,6 +49,7 @@ func ReCreateNTPService(server string, interval time.Duration, dialerProxy strin
syncSystemTime: syncSystemTime, syncSystemTime: syncSystemTime,
} }
service.Start() service.Start()
globalSrv.Store(service)
} }
func (srv *Service) Start() { func (srv *Service) Start() {
@@ -52,46 +61,49 @@ func (srv *Service) Start() {
log.Errorln("Initialize NTP time failed: %s", err) log.Errorln("Initialize NTP time failed: %s", err)
return return
} }
service.running = true srv.running = true
go srv.loopUpdate() go srv.loopUpdate()
} }
func (srv *Service) Stop() { func (srv *Service) Stop() {
srv.mu.Lock() srv.mu.Lock()
defer srv.mu.Unlock() defer srv.mu.Unlock()
if service.running { if srv.running {
srv.ticker.Stop() srv.ticker.Stop()
srv.cancel() srv.cancel()
service.running = false srv.running = false
} }
} }
func (srv *Service) Running() bool { func (srv *Service) Offset() time.Duration {
if srv == nil { if srv == nil {
return false return 0
} }
srv.mu.Lock() srv.mu.RLock()
defer srv.mu.Unlock() defer srv.mu.RUnlock()
return srv.running if srv.running {
return srv.offset
}
return 0
} }
func (srv *Service) update() error { func (srv *Service) update() error {
var response *ntp.Response var response *ntp.Response
var err error var err error
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
if response, err = ntp.Exchange(context.Background(), srv.dialer, srv.server); err == nil { response, err = ntp.Exchange(srv.ctx, srv.dialer, srv.server)
break if err != nil {
continue
} }
if i == 2 { offset := response.ClockOffset
return err
}
}
offset = response.ClockOffset
if offset > time.Duration(0) { if offset > time.Duration(0) {
log.Infoln("System clock is ahead of NTP time by %s", offset) log.Infoln("System clock is ahead of NTP time by %s", offset)
} else if offset < time.Duration(0) { } else if offset < time.Duration(0) {
log.Infoln("System clock is behind NTP time by %s", -offset) log.Infoln("System clock is behind NTP time by %s", -offset)
} }
srv.mu.Lock()
srv.offset = offset
srv.mu.Unlock()
if srv.syncSystemTime { if srv.syncSystemTime {
timeNow := response.Time timeNow := response.Time
syncErr := setSystemTime(timeNow) syncErr := setSystemTime(timeNow)
@@ -103,6 +115,8 @@ func (srv *Service) update() error {
} }
} }
return nil return nil
}
return err
} }
func (srv *Service) loopUpdate() { func (srv *Service) loopUpdate() {
@@ -121,7 +135,7 @@ func (srv *Service) loopUpdate() {
func Now() time.Time { func Now() time.Time {
now := time.Now() now := time.Now()
if service.Running() && offset.Abs() > 0 { if offset := globalSrv.Load().Offset(); offset.Abs() > 0 {
now = now.Add(offset) now = now.Add(offset)
} }
return now return now

View File

@@ -10,11 +10,11 @@ include $(TOPDIR)/rules.mk
PKG_ARCH_quickstart:=$(ARCH) PKG_ARCH_quickstart:=$(ARCH)
PKG_NAME:=quickstart PKG_NAME:=quickstart
PKG_VERSION:=0.11.6 PKG_VERSION:=0.11.7
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/linkease/istore-packages/releases/download/prebuilt/ PKG_SOURCE_URL:=https://github.com/linkease/istore-packages/releases/download/prebuilt/
PKG_HASH:=1b3d206156b615cc227b3936d4e2cabba429f205b8cbd4d1a297ebc6870efce6 PKG_HASH:=8d70a4e8a6c17c767d3507037567185aa6f80e388b22a2eede9d2e253fbad233
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-binary-$(PKG_VERSION) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-binary-$(PKG_VERSION)

View File

@@ -55,22 +55,18 @@ local api = require "luci.passwall.api"
"gfwlist_update","chnroute_update","chnroute6_update", "gfwlist_update","chnroute_update","chnroute6_update",
"chnlist_update","geoip_update","geosite_update" "chnlist_update","geoip_update","geosite_update"
]; ];
const targetNode = document.querySelector('form') || document.body; const bindFlags = () => {
const observer = new MutationObserver(() => { let allBound = true;
flags.forEach(flag => { flags.forEach(flag => {
const orig = Array.from(document.querySelectorAll(`input[name$=".${flag}"]`)).find(i => i.type === 'checkbox'); const orig = Array.from(document.querySelectorAll(`input[name$=".${flag}"]`)).find(i => i.type === 'checkbox');
if (!orig) { if (!orig) { allBound = false; return; }
return;
}
// 隐藏最外层 div // 隐藏最外层 div
const wrapper = orig.closest('.cbi-value'); const wrapper = orig.closest('.cbi-value');
if (wrapper && wrapper.style.display !== 'none') { if (wrapper && wrapper.style.display !== 'none') {
wrapper.style.display = 'none'; wrapper.style.display = 'none';
} }
const custom = document.querySelector(`.cbi-input-checkbox[name="${flag.replace('_update','')}"]`); const custom = document.querySelector(`.cbi-input-checkbox[name="${flag.replace('_update','')}"]`);
if (!custom) { if (!custom) { allBound = false; return; }
return;
}
custom.checked = orig.checked; custom.checked = orig.checked;
// 自定义选择框与原生Flag双向绑定 // 自定义选择框与原生Flag双向绑定
if (!custom._binded) { if (!custom._binded) {
@@ -84,8 +80,13 @@ local api = require "luci.passwall.api"
}); });
} }
}); });
}); return allBound;
observer.observe(targetNode, { childList: true, subtree: true }); };
const target = document.querySelector('form') || document.body;
const observer = new MutationObserver(() => bindFlags() ? observer.disconnect() : 0);
observer.observe(target, { childList: true, subtree: true });
const timer = setInterval(() => bindFlags() ? (clearInterval(timer), observer.disconnect()) : 0, 300);
setTimeout(() => { clearInterval(timer); observer.disconnect(); }, 5000);
}); });
function update_rules(btn) { function update_rules(btn) {

View File

@@ -432,7 +432,8 @@ jobs:
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }} SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
build_apple: build_apple:
name: Build Apple clients name: Build Apple clients
runs-on: macos-15 runs-on: macos-26
if: false
needs: needs:
- calculate_version - calculate_version
strategy: strategy:
@@ -479,14 +480,6 @@ jobs:
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.1
- name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Setup Xcode beta
if: matrix.if && github.ref == 'refs/heads/dev-next'
run: |-
sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Set tag - name: Set tag
if: matrix.if if: matrix.if
run: |- run: |-

View File

@@ -17,6 +17,10 @@ build:
export GOTOOLCHAIN=local && \ export GOTOOLCHAIN=local && \
go build $(MAIN_PARAMS) $(MAIN) go build $(MAIN_PARAMS) $(MAIN)
race:
export GOTOOLCHAIN=local && \
go build -race $(MAIN_PARAMS) $(MAIN)
ci_build: ci_build:
export GOTOOLCHAIN=local && \ export GOTOOLCHAIN=local && \
go build $(PARAMS) $(MAIN) && \ go build $(PARAMS) $(MAIN) && \

View File

@@ -1,3 +1,3 @@
VERSION_CODE=564 VERSION_CODE=566
VERSION_NAME=1.12.5 VERSION_NAME=1.12.6
GO_VERSION=go1.25.1 GO_VERSION=go1.25.1

View File

@@ -7,6 +7,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@@ -21,6 +22,7 @@ import (
var _ adapter.CertificateStore = (*Store)(nil) var _ adapter.CertificateStore = (*Store)(nil)
type Store struct { type Store struct {
access sync.RWMutex
systemPool *x509.CertPool systemPool *x509.CertPool
currentPool *x509.CertPool currentPool *x509.CertPool
certificate string certificate string
@@ -115,10 +117,14 @@ func (s *Store) Close() error {
} }
func (s *Store) Pool() *x509.CertPool { func (s *Store) Pool() *x509.CertPool {
s.access.RLock()
defer s.access.RUnlock()
return s.currentPool return s.currentPool
} }
func (s *Store) update() error { func (s *Store) update() error {
s.access.Lock()
defer s.access.Unlock()
var currentPool *x509.CertPool var currentPool *x509.CertPool
if s.systemPool == nil { if s.systemPool == nil {
currentPool = x509.NewCertPool() currentPool = x509.NewCertPool()

View File

@@ -69,11 +69,7 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
} else { } else {
return E.New("missing ECH keys") return E.New("missing ECH keys")
} }
block, rest := pem.Decode(echKey) echKeys, err := parseECHKeys(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil { if err != nil {
return E.Cause(err, "parse ECH keys") return E.Cause(err, "parse ECH keys")
} }
@@ -85,21 +81,29 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
return nil return nil
} }
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error { func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
echKey, err := os.ReadFile(echKeyPath) echKeys, err := parseECHKeys(echKey)
if err != nil { if err != nil {
return E.Cause(err, "reload ECH keys from ", echKeyPath) return err
} }
c.access.Lock()
config := c.config.Clone()
config.EncryptedClientHelloKeys = echKeys
c.config = config
c.access.Unlock()
return nil
}
func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) {
block, _ := pem.Decode(echKey) block, _ := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" { if block == nil || block.Type != "ECH KEYS" {
return E.New("invalid ECH keys pem") return nil, E.New("invalid ECH keys pem")
} }
echKeys, err := UnmarshalECHKeys(block.Bytes) echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil { if err != nil {
return E.Cause(err, "parse ECH keys") return nil, E.Cause(err, "parse ECH keys")
} }
tlsConfig.EncryptedClientHelloKeys = echKeys return echKeys, nil
return nil
} }
type ECHClientConfig struct { type ECHClientConfig struct {

View File

@@ -18,6 +18,6 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
return E.New("ECH requires go1.24, please recompile your binary.") return E.New("ECH requires go1.24, please recompile your binary.")
} }
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error { func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
return E.New("ECH requires go1.24, please recompile your binary.") panic("unreachable")
} }

View File

@@ -6,6 +6,7 @@ import (
"net" "net"
"os" "os"
"strings" "strings"
"sync"
"time" "time"
"github.com/sagernet/fswatch" "github.com/sagernet/fswatch"
@@ -21,6 +22,7 @@ import (
var errInsecureUnused = E.New("tls: insecure unused") var errInsecureUnused = E.New("tls: insecure unused")
type STDServerConfig struct { type STDServerConfig struct {
access sync.RWMutex
config *tls.Config config *tls.Config
logger log.Logger logger log.Logger
acmeService adapter.SimpleLifecycle acmeService adapter.SimpleLifecycle
@@ -33,14 +35,22 @@ type STDServerConfig struct {
} }
func (c *STDServerConfig) ServerName() string { func (c *STDServerConfig) ServerName() string {
c.access.RLock()
defer c.access.RUnlock()
return c.config.ServerName return c.config.ServerName
} }
func (c *STDServerConfig) SetServerName(serverName string) { func (c *STDServerConfig) SetServerName(serverName string) {
c.config.ServerName = serverName c.access.Lock()
defer c.access.Unlock()
config := c.config.Clone()
config.ServerName = serverName
c.config = config
} }
func (c *STDServerConfig) NextProtos() []string { func (c *STDServerConfig) NextProtos() []string {
c.access.RLock()
defer c.access.RUnlock()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
return c.config.NextProtos[1:] return c.config.NextProtos[1:]
} else { } else {
@@ -49,11 +59,15 @@ func (c *STDServerConfig) NextProtos() []string {
} }
func (c *STDServerConfig) SetNextProtos(nextProto []string) { func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.access.Lock()
defer c.access.Unlock()
config := c.config.Clone()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol { if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
c.config.NextProtos = append(c.config.NextProtos[:1], nextProto...) config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
} else { } else {
c.config.NextProtos = nextProto config.NextProtos = nextProto
} }
c.config = config
} }
func (c *STDServerConfig) STDConfig() (*STDConfig, error) { func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
@@ -78,9 +92,6 @@ func (c *STDServerConfig) Start() error {
if c.acmeService != nil { if c.acmeService != nil {
return c.acmeService.Start() return c.acmeService.Start()
} else { } else {
if c.certificatePath == "" && c.keyPath == "" {
return nil
}
err := c.startWatcher() err := c.startWatcher()
if err != nil { if err != nil {
c.logger.Warn("create fsnotify watcher: ", err) c.logger.Warn("create fsnotify watcher: ", err)
@@ -100,6 +111,9 @@ func (c *STDServerConfig) startWatcher() error {
if c.echKeyPath != "" { if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath) watchPath = append(watchPath, c.echKeyPath)
} }
if len(watchPath) == 0 {
return nil
}
watcher, err := fswatch.NewWatcher(fswatch.Options{ watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath, Path: watchPath,
Callback: func(path string) { Callback: func(path string) {
@@ -139,10 +153,18 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
if err != nil { if err != nil {
return E.Cause(err, "reload key pair") return E.Cause(err, "reload key pair")
} }
c.config.Certificates = []tls.Certificate{keyPair} c.access.Lock()
config := c.config.Clone()
config.Certificates = []tls.Certificate{keyPair}
c.config = config
c.access.Unlock()
c.logger.Info("reloaded TLS certificate") c.logger.Info("reloaded TLS certificate")
} else if path == c.echKeyPath { } else if path == c.echKeyPath {
err := reloadECHKeys(c.echKeyPath, c.config) echKey, err := os.ReadFile(c.echKeyPath)
if err != nil {
return E.Cause(err, "reload ECH keys from ", c.echKeyPath)
}
err = c.setECHServerConfig(echKey)
if err != nil { if err != nil {
return err return err
} }
@@ -263,7 +285,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
return nil, err return nil, err
} }
} }
var config ServerConfig = &STDServerConfig{ serverConfig := &STDServerConfig{
config: tlsConfig, config: tlsConfig,
logger: logger, logger: logger,
acmeService: acmeService, acmeService: acmeService,
@@ -273,6 +295,12 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
keyPath: options.KeyPath, keyPath: options.KeyPath,
echKeyPath: echKeyPath, echKeyPath: echKeyPath,
} }
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
serverConfig.access.Lock()
defer serverConfig.access.Unlock()
return serverConfig.config, nil
}
var config ServerConfig = serverConfig
if options.KernelTx || options.KernelRx { if options.KernelTx || options.KernelRx {
if !C.IsLinux { if !C.IsLinux {
return nil, E.New("kTLS is only supported on Linux") return nil, E.New("kTLS is only supported on Linux")

View File

@@ -46,15 +46,15 @@ func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory
func (s *HistoryStorage) DeleteURLTestHistory(tag string) { func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
s.access.Lock() s.access.Lock()
delete(s.delayHistory, tag) delete(s.delayHistory, tag)
s.access.Unlock()
s.notifyUpdated() s.notifyUpdated()
s.access.Unlock()
} }
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) { func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
s.access.Lock() s.access.Lock()
s.delayHistory[tag] = history s.delayHistory[tag] = history
s.access.Unlock()
s.notifyUpdated() s.notifyUpdated()
s.access.Unlock()
} }
func (s *HistoryStorage) notifyUpdated() { func (s *HistoryStorage) notifyUpdated() {
@@ -68,6 +68,8 @@ func (s *HistoryStorage) notifyUpdated() {
} }
func (s *HistoryStorage) Close() error { func (s *HistoryStorage) Close() error {
s.access.Lock()
defer s.access.Unlock()
s.updateHook = nil s.updateHook = nil
return nil return nil
} }

View File

@@ -280,7 +280,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
} }
} }
logExchangedResponse(c.logger, ctx, response, timeToLive) logExchangedResponse(c.logger, ctx, response, timeToLive)
return response, err return response, nil
} }
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) { func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {

View File

@@ -5,6 +5,7 @@ import (
) )
const ( const (
RcodeSuccess RcodeError = mDNS.RcodeSuccess
RcodeFormatError RcodeError = mDNS.RcodeFormatError RcodeFormatError RcodeError = mDNS.RcodeFormatError
RcodeNameError RcodeError = mDNS.RcodeNameError RcodeNameError RcodeError = mDNS.RcodeNameError
RcodeRefused RcodeError = mDNS.RcodeRefused RcodeRefused RcodeError = mDNS.RcodeRefused

View File

@@ -2,12 +2,13 @@ package dhcp
import ( import (
"context" "context"
"errors"
"math/rand" "math/rand"
"strings" "strings"
"time" "syscall"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
@@ -43,7 +44,7 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr,
if response.Rcode != mDNS.RcodeSuccess { if response.Rcode != mDNS.RcodeSuccess {
err = dns.RcodeError(response.Rcode) err = dns.RcodeError(response.Rcode)
} else if len(dns.MessageToAddresses(response)) == 0 { } else if len(dns.MessageToAddresses(response)) == 0 {
err = E.New(fqdn, ": empty result") err = dns.RcodeSuccess
} }
} }
select { select {
@@ -83,7 +84,7 @@ func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn
server := servers[j] server := servers[j]
question := message.Question[0] question := message.Question[0]
question.Name = fqdn question.Name = fqdn
response, err := t.exchangeOne(ctx, server, question, C.DNSTimeout, false, true) response, err := t.exchangeOne(ctx, server, question)
if err != nil { if err != nil {
lastErr = err lastErr = err
continue continue
@@ -94,32 +95,25 @@ func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn
return nil, E.Cause(lastErr, fqdn) return nil, E.Cause(lastErr, fqdn)
} }
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) { func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question) (*mDNS.Msg, error) {
if server.Port == 0 { if server.Port == 0 {
server.Port = 53 server.Port = 53
} }
var networks []string
if useTCP {
networks = []string{N.NetworkTCP}
} else {
networks = []string{N.NetworkUDP, N.NetworkTCP}
}
request := &mDNS.Msg{ request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{ MsgHdr: mDNS.MsgHdr{
Id: uint16(rand.Uint32()), Id: uint16(rand.Uint32()),
RecursionDesired: true, RecursionDesired: true,
AuthenticatedData: ad, AuthenticatedData: true,
}, },
Question: []mDNS.Question{question}, Question: []mDNS.Question{question},
Compress: true, Compress: true,
} }
request.SetEdns0(buf.UDPBufferSize, false) request.SetEdns0(buf.UDPBufferSize, false)
buffer := buf.Get(buf.UDPBufferSize) return t.exchangeUDP(ctx, server, request)
defer buf.Put(buffer) }
for _, network := range networks {
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout)) func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
defer cancel() conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
conn, err := t.dialer.DialContext(ctx, network, server)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -127,16 +121,24 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
conn.SetDeadline(deadline) conn.SetDeadline(deadline)
} }
buffer := buf.Get(1 + request.Len())
defer buf.Put(buffer)
rawMessage, err := request.PackBuffer(buffer) rawMessage, err := request.PackBuffer(buffer)
if err != nil { if err != nil {
return nil, E.Cause(err, "pack request") return nil, E.Cause(err, "pack request")
} }
_, err = conn.Write(rawMessage) _, err = conn.Write(rawMessage)
if err != nil { if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request)
}
return nil, E.Cause(err, "write request") return nil, E.Cause(err, "write request")
} }
n, err := conn.Read(buffer) n, err := conn.Read(buffer)
if err != nil { if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request)
}
return nil, E.Cause(err, "read response") return nil, E.Cause(err, "read response")
} }
var response mDNS.Msg var response mDNS.Msg
@@ -144,12 +146,26 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
if err != nil { if err != nil {
return nil, E.Cause(err, "unpack response") return nil, E.Cause(err, "unpack response")
} }
if response.Truncated && network == N.NetworkUDP { if response.Truncated {
continue return t.exchangeTCP(ctx, server, request)
} }
return &response, nil return &response, nil
}
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
if err != nil {
return nil, err
} }
panic("unexpected") defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
conn.SetDeadline(deadline)
}
err = transport.WriteMessage(conn, 0, request)
if err != nil {
return nil, err
}
return transport.ReadMessage(conn)
} }
func (t *Transport) nameList(name string) []string { func (t *Transport) nameList(name string) []string {

View File

@@ -2,11 +2,13 @@ package local
import ( import (
"context" "context"
"fmt" "errors"
"math/rand" "math/rand"
"syscall"
"time" "time"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
@@ -17,7 +19,6 @@ import (
func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, domain string) (*mDNS.Msg, error) { func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, domain string) (*mDNS.Msg, error) {
systemConfig := getSystemDNSConfig(t.ctx) systemConfig := getSystemDNSConfig(t.ctx)
fmt.Println(systemConfig.servers)
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) { if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
return t.exchangeSingleRequest(ctx, systemConfig, message, domain) return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
} else { } else {
@@ -108,12 +109,6 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
if server.Port == 0 { if server.Port == 0 {
server.Port = 53 server.Port = 53
} }
var networks []string
if useTCP {
networks = []string{N.NetworkTCP}
} else {
networks = []string{N.NetworkUDP, N.NetworkTCP}
}
request := &mDNS.Msg{ request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{ MsgHdr: mDNS.MsgHdr{
Id: uint16(rand.Uint32()), Id: uint16(rand.Uint32()),
@@ -124,29 +119,44 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
Compress: true, Compress: true,
} }
request.SetEdns0(buf.UDPBufferSize, false) request.SetEdns0(buf.UDPBufferSize, false)
buffer := buf.Get(buf.UDPBufferSize) if !useTCP {
defer buf.Put(buffer) return t.exchangeUDP(ctx, server, request, timeout)
for _, network := range networks { } else {
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout)) return t.exchangeTCP(ctx, server, request, timeout)
defer cancel() }
conn, err := t.dialer.DialContext(ctx, network, server) }
func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer conn.Close() defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
newDeadline := time.Now().Add(timeout)
if deadline.After(newDeadline) {
deadline = newDeadline
}
conn.SetDeadline(deadline) conn.SetDeadline(deadline)
} }
buffer := buf.Get(1 + request.Len())
defer buf.Put(buffer)
rawMessage, err := request.PackBuffer(buffer) rawMessage, err := request.PackBuffer(buffer)
if err != nil { if err != nil {
return nil, E.Cause(err, "pack request") return nil, E.Cause(err, "pack request")
} }
_, err = conn.Write(rawMessage) _, err = conn.Write(rawMessage)
if err != nil { if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request, timeout)
}
return nil, E.Cause(err, "write request") return nil, E.Cause(err, "write request")
} }
n, err := conn.Read(buffer) n, err := conn.Read(buffer)
if err != nil { if err != nil {
if errors.Is(err, syscall.EMSGSIZE) {
return t.exchangeTCP(ctx, server, request, timeout)
}
return nil, E.Cause(err, "read response") return nil, E.Cause(err, "read response")
} }
var response mDNS.Msg var response mDNS.Msg
@@ -154,10 +164,28 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
if err != nil { if err != nil {
return nil, E.Cause(err, "unpack response") return nil, E.Cause(err, "unpack response")
} }
if response.Truncated && network == N.NetworkUDP { if response.Truncated {
continue return t.exchangeTCP(ctx, server, request, timeout)
} }
return &response, nil return &response, nil
} }
panic("unexpected")
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
if err != nil {
return nil, err
}
defer conn.Close()
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
newDeadline := time.Now().Add(timeout)
if deadline.After(newDeadline) {
deadline = newDeadline
}
conn.SetDeadline(deadline)
}
err = transport.WriteMessage(conn, 0, request)
if err != nil {
return nil, err
}
return transport.ReadMessage(conn)
} }

View File

@@ -2,6 +2,14 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.13.0-alpha.12
* Fixes and improvements
#### 1.12.6
* Fixes and improvements
#### 1.13.0-alpha.11 #### 1.13.0-alpha.11
* Fixes and improvements * Fixes and improvements

View File

@@ -14,7 +14,7 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch" "github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"

View File

@@ -30,11 +30,11 @@ type LocalRuleSet struct {
ctx context.Context ctx context.Context
logger logger.Logger logger logger.Logger
tag string tag string
access sync.RWMutex
rules []adapter.HeadlessRule rules []adapter.HeadlessRule
metadata adapter.RuleSetMetadata metadata adapter.RuleSetMetadata
fileFormat string fileFormat string
watcher *fswatch.Watcher watcher *fswatch.Watcher
callbackAccess sync.Mutex
callbacks list.List[adapter.RuleSetUpdateCallback] callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32 refs atomic.Int32
} }
@@ -141,11 +141,11 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule) metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule)
metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule) metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule)
metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule) metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
s.access.Lock()
s.rules = rules s.rules = rules
s.metadata = metadata s.metadata = metadata
s.callbackAccess.Lock()
callbacks := s.callbacks.Array() callbacks := s.callbacks.Array()
s.callbackAccess.Unlock() s.access.Unlock()
for _, callback := range callbacks { for _, callback := range callbacks {
callback(s) callback(s)
} }
@@ -157,10 +157,14 @@ func (s *LocalRuleSet) PostStart() error {
} }
func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata { func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
s.access.RLock()
defer s.access.RUnlock()
return s.metadata return s.metadata
} }
func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet { func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet {
s.access.RLock()
defer s.access.RUnlock()
return common.FlatMap(s.rules, extractIPSetFromRule) return common.FlatMap(s.rules, extractIPSetFromRule)
} }
@@ -181,14 +185,14 @@ func (s *LocalRuleSet) Cleanup() {
} }
func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
s.callbackAccess.Lock() s.access.Lock()
defer s.callbackAccess.Unlock() defer s.access.Unlock()
return s.callbacks.PushBack(callback) return s.callbacks.PushBack(callback)
} }
func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
s.callbackAccess.Lock() s.access.Lock()
defer s.callbackAccess.Unlock() defer s.access.Unlock()
s.callbacks.Remove(element) s.callbacks.Remove(element)
} }

View File

@@ -40,16 +40,16 @@ type RemoteRuleSet struct {
logger logger.ContextLogger logger logger.ContextLogger
outbound adapter.OutboundManager outbound adapter.OutboundManager
options option.RuleSet options option.RuleSet
metadata adapter.RuleSetMetadata
updateInterval time.Duration updateInterval time.Duration
dialer N.Dialer dialer N.Dialer
access sync.RWMutex
rules []adapter.HeadlessRule rules []adapter.HeadlessRule
metadata adapter.RuleSetMetadata
lastUpdated time.Time lastUpdated time.Time
lastEtag string lastEtag string
updateTicker *time.Ticker updateTicker *time.Ticker
cacheFile adapter.CacheFile cacheFile adapter.CacheFile
pauseManager pause.Manager pauseManager pause.Manager
callbackAccess sync.Mutex
callbacks list.List[adapter.RuleSetUpdateCallback] callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32 refs atomic.Int32
} }
@@ -120,10 +120,14 @@ func (s *RemoteRuleSet) PostStart() error {
} }
func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata { func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata {
s.access.RLock()
defer s.access.RUnlock()
return s.metadata return s.metadata
} }
func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet { func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet {
s.access.RLock()
defer s.access.RUnlock()
return common.FlatMap(s.rules, extractIPSetFromRule) return common.FlatMap(s.rules, extractIPSetFromRule)
} }
@@ -144,14 +148,14 @@ func (s *RemoteRuleSet) Cleanup() {
} }
func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] { func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
s.callbackAccess.Lock() s.access.Lock()
defer s.callbackAccess.Unlock() defer s.access.Unlock()
return s.callbacks.PushBack(callback) return s.callbacks.PushBack(callback)
} }
func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) { func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
s.callbackAccess.Lock() s.access.Lock()
defer s.callbackAccess.Unlock() defer s.access.Unlock()
s.callbacks.Remove(element) s.callbacks.Remove(element)
} }
@@ -185,13 +189,13 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
return E.Cause(err, "parse rule_set.rules.[", i, "]") return E.Cause(err, "parse rule_set.rules.[", i, "]")
} }
} }
s.access.Lock()
s.metadata.ContainsProcessRule = HasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule) s.metadata.ContainsProcessRule = HasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
s.metadata.ContainsWIFIRule = HasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule) s.metadata.ContainsWIFIRule = HasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
s.metadata.ContainsIPCIDRRule = HasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule) s.metadata.ContainsIPCIDRRule = HasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
s.rules = rules s.rules = rules
s.callbackAccess.Lock()
callbacks := s.callbacks.Array() callbacks := s.callbacks.Array()
s.callbackAccess.Unlock() s.access.Unlock()
for _, callback := range callbacks { for _, callback := range callbacks {
callback(s) callback(s)
} }

View File

@@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=hysteria PKG_NAME:=hysteria
PKG_VERSION:=2.6.2 PKG_VERSION:=2.6.3
PKG_RELEASE:=1 PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/apernet/hysteria/tar.gz/app/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/apernet/hysteria/tar.gz/app/v$(PKG_VERSION)?
PKG_HASH:=4699431f0bc826da2bbd3939c0a78c4e7bfc02773fc3a62b24615c37ee89b266 PKG_HASH:=bed1ece93dfaa07fbf709136efadaf4ccb09e0375844de3e28c5644ebe518eb0
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-app-v$(PKG_VERSION) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-app-v$(PKG_VERSION)
PKG_LICENSE:=MIT PKG_LICENSE:=MIT

View File

@@ -711,10 +711,9 @@ return view.extend({
o.modalonly = true; o.modalonly = true;
} }
if (features.with_reality_server) {
o = s.option(form.Flag, 'tls_reality', _('REALITY')); o = s.option(form.Flag, 'tls_reality', _('REALITY'));
o.depends({'tls': '1', 'tls_acme': '0', 'type': 'vless'}); o.depends({'tls': '1', 'tls_acme': '0', 'type': /^(anytls|vless)$/});
o.depends({'tls': '1', 'tls_acme': null, 'type': 'vless'}); o.depends({'tls': '1', 'tls_acme': null, 'type': /^(anytls|vless)$/});
o.modalonly = true; o.modalonly = true;
o = s.option(form.Value, 'tls_reality_private_key', _('REALITY private key')); o = s.option(form.Value, 'tls_reality_private_key', _('REALITY private key'));
@@ -743,7 +742,6 @@ return view.extend({
o.depends('tls_reality', '1'); o.depends('tls_reality', '1');
o.rmempty = false; o.rmempty = false;
o.modalonly = true; o.modalonly = true;
}
o = s.option(form.Value, 'tls_cert_path', _('Certificate path'), o = s.option(form.Value, 'tls_cert_path', _('Certificate path'),
_('The server public key, in PEM format.')); _('The server public key, in PEM format.'));

View File

@@ -31,7 +31,7 @@ const css = ' \
const hp_dir = '/var/run/homeproxy'; const hp_dir = '/var/run/homeproxy';
function getConnStat(self, site) { function getConnStat(o, site) {
const callConnStat = rpc.declare({ const callConnStat = rpc.declare({
object: 'luci.homeproxy', object: 'luci.homeproxy',
method: 'connection_check', method: 'connection_check',
@@ -39,12 +39,12 @@ function getConnStat(self, site) {
expect: { '': {} } expect: { '': {} }
}); });
self.default = E('div', { 'style': 'cbi-value-field' }, [ o.default = E('div', { 'style': 'cbi-value-field' }, [
E('button', { E('button', {
'class': 'btn cbi-button cbi-button-action', 'class': 'btn cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, function() { 'click': ui.createHandlerFn(this, function() {
return L.resolveDefault(callConnStat(site), {}).then((ret) => { return L.resolveDefault(callConnStat(site), {}).then((ret) => {
let ele = self.default.firstElementChild.nextElementSibling; let ele = o.default.firstElementChild.nextElementSibling;
if (ret.result) { if (ret.result) {
ele.style.setProperty('color', 'green'); ele.style.setProperty('color', 'green');
ele.innerHTML = _('passed'); ele.innerHTML = _('passed');
@@ -60,7 +60,7 @@ function getConnStat(self, site) {
]); ]);
} }
function getResVersion(self, type) { function getResVersion(o, type) {
const callResVersion = rpc.declare({ const callResVersion = rpc.declare({
object: 'luci.homeproxy', object: 'luci.homeproxy',
method: 'resources_get_version', method: 'resources_get_version',
@@ -83,23 +83,23 @@ function getResVersion(self, type) {
return L.resolveDefault(callResUpdate(type), {}).then((res) => { return L.resolveDefault(callResUpdate(type), {}).then((res) => {
switch (res.status) { switch (res.status) {
case 0: case 0:
self.description = _('Successfully updated.'); o.description = _('Successfully updated.');
break; break;
case 1: case 1:
self.description = _('Update failed.'); o.description = _('Update failed.');
break; break;
case 2: case 2:
self.description = _('Already in updating.'); o.description = _('Already in updating.');
break; break;
case 3: case 3:
self.description = _('Already at the latest version.'); o.description = _('Already at the latest version.');
break; break;
default: default:
self.description = _('Unknown error.'); o.description = _('Unknown error.');
break; break;
} }
return self.map.reset(); return o.map.reset();
}); });
}) })
}, [ _('Check update') ]), }, [ _('Check update') ]),
@@ -109,11 +109,57 @@ function getResVersion(self, type) {
), ),
]); ]);
self.default = spanTemp; o.default = spanTemp;
}); });
} }
function getRuntimeLog(name, filename) { function getRuntimeLog(o, name, _option_index, section_id, _in_table) {
const filename = o.option.split('_')[1];
let section, log_level_el;
switch (filename) {
case 'homeproxy':
section = null;
break;
case 'sing-box-c':
section = 'config';
break;
case 'sing-box-s':
section = 'server';
break;
}
if (section) {
const selected = uci.get('homeproxy', section, 'log_level') || 'warn';
const choices = {
trace: _('Trace'),
debug: _('Debug'),
info: _('Info'),
warn: _('Warn'),
error: _('Error'),
fatal: _('Fatal'),
panic: _('Panic')
};
log_level_el = E('select', {
'id': o.cbid(section_id),
'class': 'cbi-input-select',
'style': 'margin-left: 4px; width: 6em;',
'change': ui.createHandlerFn(this, function(ev) {
uci.set('homeproxy', section, 'log_level', ev.target.value);
ui.changes.apply(true);
return o.map.save(null, true);
})
});
Object.keys(choices).forEach((v) => {
log_level_el.appendChild(E('option', {
'value': v,
'selected': (v === selected) ? '' : null
}, [ choices[v] ]));
});
}
const callLogClean = rpc.declare({ const callLogClean = rpc.declare({
object: 'luci.homeproxy', object: 'luci.homeproxy',
method: 'log_clean', method: 'log_clean',
@@ -121,7 +167,7 @@ function getRuntimeLog(name, filename) {
expect: { '': {} } expect: { '': {} }
}); });
let log_textarea = E('div', { 'id': 'log_textarea' }, const log_textarea = E('div', { 'id': 'log_textarea' },
E('img', { E('img', {
'src': L.resource('icons/loading.svg'), 'src': L.resource('icons/loading.svg'),
'alt': _('Loading'), 'alt': _('Loading'),
@@ -155,11 +201,12 @@ function getRuntimeLog(name, filename) {
return E([ return E([
E('style', [ css ]), E('style', [ css ]),
E('div', {'class': 'cbi-map'}, [ E('div', {'class': 'cbi-map'}, [
E('h3', {'name': 'content'}, [ E('h3', {'name': 'content', 'style': 'align-items: center; display: flex;'}, [
_('%s log').format(name), _('%s log').format(name),
' ', log_level_el || '',
E('button', { E('button', {
'class': 'btn cbi-button cbi-button-action', 'class': 'btn cbi-button cbi-button-action',
'style': 'margin-left: 4px;',
'click': ui.createHandlerFn(this, function() { 'click': ui.createHandlerFn(this, function() {
return L.resolveDefault(callLogClean(filename), {}); return L.resolveDefault(callLogClean(filename), {});
}) })
@@ -185,29 +232,28 @@ return view.extend({
s.anonymous = true; s.anonymous = true;
o = s.option(form.DummyValue, '_check_baidu', _('BaiDu')); o = s.option(form.DummyValue, '_check_baidu', _('BaiDu'));
o.cfgvalue = function() { return getConnStat(this, 'baidu') }; o.cfgvalue = L.bind(getConnStat, this, o, 'baidu');
o = s.option(form.DummyValue, '_check_google', _('Google')); o = s.option(form.DummyValue, '_check_google', _('Google'));
o.cfgvalue = function() { return getConnStat(this, 'google') }; o.cfgvalue = L.bind(getConnStat, this, o, 'google');
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management')); s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
s.anonymous = true; s.anonymous = true;
o = s.option(form.DummyValue, '_china_ip4_version', _('China IPv4 list version')); o = s.option(form.DummyValue, '_china_ip4_version', _('China IPv4 list version'));
o.cfgvalue = function() { return getResVersion(this, 'china_ip4') }; o.cfgvalue = L.bind(getResVersion, this, o, 'china_ip4');
o.rawhtml = true; o.rawhtml = true;
o = s.option(form.DummyValue, '_china_ip6_version', _('China IPv6 list version')); o = s.option(form.DummyValue, '_china_ip6_version', _('China IPv6 list version'));
o.cfgvalue = function() { return getResVersion(this, 'china_ip6') }; o.cfgvalue = L.bind(getResVersion, this, o, 'china_ip6');
o.rawhtml = true; o.rawhtml = true;
o = s.option(form.DummyValue, '_china_list_version', _('China list version')); o = s.option(form.DummyValue, '_china_list_version', _('China list version'));
o.cfgvalue = function() { return getResVersion(this, 'china_list') }; o.cfgvalue = L.bind(getResVersion, this, o, 'china_list');
o.rawhtml = true; o.rawhtml = true;
o = s.option(form.DummyValue, '_gfw_list_version', _('GFW list version')); o = s.option(form.DummyValue, '_gfw_list_version', _('GFW list version'));
o.cfgvalue = function() { return getResVersion(this, 'gfw_list') }; o.cfgvalue = L.bind(getResVersion, this, o, 'gfw_list');
o.rawhtml = true; o.rawhtml = true;
o = s.option(form.Value, 'github_token', _('GitHub token')); o = s.option(form.Value, 'github_token', _('GitHub token'));
@@ -231,13 +277,13 @@ return view.extend({
s.anonymous = true; s.anonymous = true;
o = s.option(form.DummyValue, '_homeproxy_logview'); o = s.option(form.DummyValue, '_homeproxy_logview');
o.render = L.bind(getRuntimeLog, this, _('HomeProxy'), 'homeproxy'); o.render = L.bind(getRuntimeLog, this, o, _('HomeProxy'));
o = s.option(form.DummyValue, '_sing-box-c_logview'); o = s.option(form.DummyValue, '_sing-box-c_logview');
o.render = L.bind(getRuntimeLog, this, _('sing-box client'), 'sing-box-c'); o.render = L.bind(getRuntimeLog, this, o, _('sing-box client'));
o = s.option(form.DummyValue, '_sing-box-s_logview'); o = s.option(form.DummyValue, '_sing-box-s_logview');
o.render = L.bind(getRuntimeLog, this, _('sing-box server'), 'sing-box-s'); o.render = L.bind(getRuntimeLog, this, o, _('sing-box server'));
return m.render(); return m.render();
}, },

View File

@@ -1,11 +1,11 @@
msgid "" msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8" msgstr "Content-Type: text/plain; charset=UTF-8"
#: htdocs/luci-static/resources/view/homeproxy/status.js:159 #: htdocs/luci-static/resources/view/homeproxy/status.js:205
msgid "%s log" msgid "%s log"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1454 #: htdocs/luci-static/resources/view/homeproxy/node.js:1449
msgid "%s nodes removed" msgid "%s nodes removed"
msgstr "" msgstr ""
@@ -25,9 +25,9 @@ msgid ""
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1082 #: htdocs/luci-static/resources/view/homeproxy/node.js:1082
#: htdocs/luci-static/resources/view/homeproxy/node.js:1106 #: htdocs/luci-static/resources/view/homeproxy/node.js:1101
#: htdocs/luci-static/resources/view/homeproxy/server.js:760 #: htdocs/luci-static/resources/view/homeproxy/server.js:758
#: htdocs/luci-static/resources/view/homeproxy/server.js:779 #: htdocs/luci-static/resources/view/homeproxy/server.js:777
msgid "<strong>Save your configuration before uploading files!</strong>" msgid "<strong>Save your configuration before uploading files!</strong>"
msgstr "" msgstr ""
@@ -123,7 +123,7 @@ msgid "Allow access from the Internet."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1036 #: htdocs/luci-static/resources/view/homeproxy/node.js:1036
#: htdocs/luci-static/resources/view/homeproxy/node.js:1381 #: htdocs/luci-static/resources/view/homeproxy/node.js:1376
msgid "Allow insecure" msgid "Allow insecure"
msgstr "" msgstr ""
@@ -131,7 +131,7 @@ msgstr ""
msgid "Allow insecure connection at TLS client." msgid "Allow insecure connection at TLS client."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1382 #: htdocs/luci-static/resources/view/homeproxy/node.js:1377
msgid "Allow insecure connection by default when add nodes from subscriptions." msgid "Allow insecure connection by default when add nodes from subscriptions."
msgstr "" msgstr ""
@@ -161,7 +161,7 @@ msgstr ""
msgid "Alternative TLS port" msgid "Alternative TLS port"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1417 #: htdocs/luci-static/resources/view/homeproxy/node.js:1412
msgid "An error occurred during updating subscriptions: %s" msgid "An error occurred during updating subscriptions: %s"
msgstr "" msgstr ""
@@ -222,11 +222,11 @@ msgstr ""
msgid "Authentication type" msgid "Authentication type"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1334 #: htdocs/luci-static/resources/view/homeproxy/node.js:1329
msgid "Auto update" msgid "Auto update"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1335 #: htdocs/luci-static/resources/view/homeproxy/node.js:1330
msgid "Auto update subscriptions and geodata." msgid "Auto update subscriptions and geodata."
msgstr "" msgstr ""
@@ -234,7 +234,7 @@ msgstr ""
msgid "BBR" msgid "BBR"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:187 #: htdocs/luci-static/resources/view/homeproxy/status.js:234
msgid "BaiDu" msgid "BaiDu"
msgstr "" msgstr ""
@@ -253,7 +253,7 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:453 #: htdocs/luci-static/resources/view/homeproxy/client.js:453
#: htdocs/luci-static/resources/view/homeproxy/client.js:1410 #: htdocs/luci-static/resources/view/homeproxy/client.js:1410
#: htdocs/luci-static/resources/view/homeproxy/server.js:864 #: htdocs/luci-static/resources/view/homeproxy/server.js:862
msgid "Bind interface" msgid "Bind interface"
msgstr "" msgstr ""
@@ -267,7 +267,7 @@ msgstr ""
msgid "BitTorrent" msgid "BitTorrent"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1368 #: htdocs/luci-static/resources/view/homeproxy/node.js:1363
msgid "Blacklist mode" msgid "Blacklist mode"
msgstr "" msgstr ""
@@ -279,7 +279,7 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:640 #: htdocs/luci-static/resources/view/homeproxy/client.js:640
#: htdocs/luci-static/resources/view/homeproxy/client.js:1090 #: htdocs/luci-static/resources/view/homeproxy/client.js:1090
#: htdocs/luci-static/resources/view/homeproxy/client.js:1100 #: htdocs/luci-static/resources/view/homeproxy/client.js:1100
#: htdocs/luci-static/resources/view/homeproxy/server.js:859 #: htdocs/luci-static/resources/view/homeproxy/server.js:857
msgid "Both" msgid "Both"
msgstr "" msgstr ""
@@ -307,12 +307,12 @@ msgstr ""
msgid "CUBIC" msgid "CUBIC"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1238 #: htdocs/luci-static/resources/view/homeproxy/node.js:1233
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1073 #: htdocs/luci-static/resources/view/homeproxy/node.js:1073
#: htdocs/luci-static/resources/view/homeproxy/server.js:748 #: htdocs/luci-static/resources/view/homeproxy/server.js:746
msgid "Certificate path" msgid "Certificate path"
msgstr "" msgstr ""
@@ -328,15 +328,15 @@ msgstr ""
msgid "China DNS server" msgid "China DNS server"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:197 #: htdocs/luci-static/resources/view/homeproxy/status.js:243
msgid "China IPv4 list version" msgid "China IPv4 list version"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:201 #: htdocs/luci-static/resources/view/homeproxy/status.js:247
msgid "China IPv6 list version" msgid "China IPv6 list version"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:205 #: htdocs/luci-static/resources/view/homeproxy/status.js:251
msgid "China list version" msgid "China list version"
msgstr "" msgstr ""
@@ -353,7 +353,7 @@ msgstr ""
msgid "Cisco Public DNS (208.67.222.222)" msgid "Cisco Public DNS (208.67.222.222)"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:166 #: htdocs/luci-static/resources/view/homeproxy/status.js:213
msgid "Clean log" msgid "Clean log"
msgstr "" msgstr ""
@@ -379,7 +379,7 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:114 #: htdocs/luci-static/resources/view/homeproxy/client.js:114
#: htdocs/luci-static/resources/view/homeproxy/server.js:122 #: htdocs/luci-static/resources/view/homeproxy/server.js:122
#: htdocs/luci-static/resources/view/homeproxy/status.js:129 #: htdocs/luci-static/resources/view/homeproxy/status.js:175
msgid "Collecting data..." msgid "Collecting data..."
msgstr "" msgstr ""
@@ -392,7 +392,7 @@ msgstr ""
msgid "Congestion control algorithm" msgid "Congestion control algorithm"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:184 #: htdocs/luci-static/resources/view/homeproxy/status.js:231
msgid "Connection check" msgid "Connection check"
msgstr "" msgstr ""
@@ -439,6 +439,10 @@ msgstr ""
msgid "DTLS" msgid "DTLS"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:136
msgid "Debug"
msgstr ""
#: htdocs/luci-static/resources/homeproxy.js:17 #: htdocs/luci-static/resources/homeproxy.js:17
#: htdocs/luci-static/resources/view/homeproxy/client.js:433 #: htdocs/luci-static/resources/view/homeproxy/client.js:433
#: htdocs/luci-static/resources/view/homeproxy/client.js:603 #: htdocs/luci-static/resources/view/homeproxy/client.js:603
@@ -482,7 +486,7 @@ msgstr ""
msgid "Default outbound for connections not matched by any routing rules." msgid "Default outbound for connections not matched by any routing rules."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1388 #: htdocs/luci-static/resources/view/homeproxy/node.js:1383
msgid "Default packet encoding" msgid "Default packet encoding"
msgstr "" msgstr ""
@@ -523,8 +527,8 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:498 #: htdocs/luci-static/resources/view/homeproxy/node.js:498
#: htdocs/luci-static/resources/view/homeproxy/node.js:554 #: htdocs/luci-static/resources/view/homeproxy/node.js:554
#: htdocs/luci-static/resources/view/homeproxy/node.js:566 #: htdocs/luci-static/resources/view/homeproxy/node.js:566
#: htdocs/luci-static/resources/view/homeproxy/node.js:1117 #: htdocs/luci-static/resources/view/homeproxy/node.js:1111
#: htdocs/luci-static/resources/view/homeproxy/node.js:1367 #: htdocs/luci-static/resources/view/homeproxy/node.js:1362
#: htdocs/luci-static/resources/view/homeproxy/server.js:267 #: htdocs/luci-static/resources/view/homeproxy/server.js:267
#: htdocs/luci-static/resources/view/homeproxy/server.js:279 #: htdocs/luci-static/resources/view/homeproxy/server.js:279
msgid "Disable" msgid "Disable"
@@ -645,14 +649,14 @@ msgstr ""
msgid "Drop requests" msgid "Drop requests"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1374 #: htdocs/luci-static/resources/view/homeproxy/node.js:1369
msgid "" msgid ""
"Drop/keep nodes that contain the specific keywords. <a target=\"_blank\" " "Drop/keep nodes that contain the specific keywords. <a target=\"_blank\" "
"href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/" "href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/"
"Regular_Expressions\">Regex</a> is supported." "Regular_Expressions\">Regex</a> is supported."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1366 #: htdocs/luci-static/resources/view/homeproxy/node.js:1361
msgid "Drop/keep specific nodes from subscriptions." msgid "Drop/keep specific nodes from subscriptions."
msgstr "" msgstr ""
@@ -664,22 +668,22 @@ msgid ""
"a non-ACME system, such as a CA customer database." "a non-ACME system, such as a CA customer database."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1091 #: htdocs/luci-static/resources/view/homeproxy/node.js:1090
msgid "" msgid ""
"ECH (Encrypted Client Hello) is a TLS extension that allows a client to " "ECH (Encrypted Client Hello) is a TLS extension that allows a client to "
"encrypt the first part of its ClientHello message." "encrypt the first part of its ClientHello message."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1110 #: htdocs/luci-static/resources/view/homeproxy/node.js:1105
#: htdocs/luci-static/resources/view/homeproxy/server.js:825 #: htdocs/luci-static/resources/view/homeproxy/server.js:823
msgid "ECH config" msgid "ECH config"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1099 #: htdocs/luci-static/resources/view/homeproxy/node.js:1094
msgid "ECH config path" msgid "ECH config path"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:786 #: htdocs/luci-static/resources/view/homeproxy/server.js:784
msgid "ECH key" msgid "ECH key"
msgstr "" msgstr ""
@@ -703,7 +707,7 @@ msgstr ""
msgid "Early data is sent in path instead of header by default." msgid "Early data is sent in path instead of header by default."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1210 #: htdocs/luci-static/resources/view/homeproxy/node.js:1205
msgid "Edit nodes" msgid "Edit nodes"
msgstr "" msgstr ""
@@ -738,14 +742,10 @@ msgstr ""
msgid "Enable ACME" msgid "Enable ACME"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1090 #: htdocs/luci-static/resources/view/homeproxy/node.js:1089
msgid "Enable ECH" msgid "Enable ECH"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1095
msgid "Enable PQ signature schemes"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:980 #: htdocs/luci-static/resources/view/homeproxy/node.js:980
#: htdocs/luci-static/resources/view/homeproxy/server.js:522 #: htdocs/luci-static/resources/view/homeproxy/server.js:522
msgid "Enable TCP Brutal" msgid "Enable TCP Brutal"
@@ -756,8 +756,8 @@ msgstr ""
msgid "Enable TCP Brutal congestion control algorithm" msgid "Enable TCP Brutal congestion control algorithm"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1167 #: htdocs/luci-static/resources/view/homeproxy/node.js:1162
#: htdocs/luci-static/resources/view/homeproxy/server.js:845 #: htdocs/luci-static/resources/view/homeproxy/server.js:843
msgid "Enable UDP fragmentation." msgid "Enable UDP fragmentation."
msgstr "" msgstr ""
@@ -770,11 +770,11 @@ msgstr ""
msgid "Enable padding" msgid "Enable padding"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:836 #: htdocs/luci-static/resources/view/homeproxy/server.js:834
msgid "Enable tcp fast open for listener." msgid "Enable tcp fast open for listener."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1171 #: htdocs/luci-static/resources/view/homeproxy/node.js:1166
msgid "" msgid ""
"Enable the SUoT protocol, requires server support. Conflict with multiplex." "Enable the SUoT protocol, requires server support. Conflict with multiplex."
msgstr "" msgstr ""
@@ -785,6 +785,10 @@ msgstr ""
msgid "Encrypt method" msgid "Encrypt method"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:139
msgid "Error"
msgstr ""
#: htdocs/luci-static/resources/homeproxy.js:237 #: htdocs/luci-static/resources/homeproxy.js:237
#: htdocs/luci-static/resources/homeproxy.js:271 #: htdocs/luci-static/resources/homeproxy.js:271
#: htdocs/luci-static/resources/homeproxy.js:279 #: htdocs/luci-static/resources/homeproxy.js:279
@@ -809,10 +813,10 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:1505 #: htdocs/luci-static/resources/view/homeproxy/client.js:1505
#: htdocs/luci-static/resources/view/homeproxy/client.js:1537 #: htdocs/luci-static/resources/view/homeproxy/client.js:1537
#: htdocs/luci-static/resources/view/homeproxy/node.js:487 #: htdocs/luci-static/resources/view/homeproxy/node.js:487
#: htdocs/luci-static/resources/view/homeproxy/node.js:1133 #: htdocs/luci-static/resources/view/homeproxy/node.js:1127
#: htdocs/luci-static/resources/view/homeproxy/node.js:1301 #: htdocs/luci-static/resources/view/homeproxy/node.js:1296
#: htdocs/luci-static/resources/view/homeproxy/node.js:1355 #: htdocs/luci-static/resources/view/homeproxy/node.js:1350
#: htdocs/luci-static/resources/view/homeproxy/node.js:1358 #: htdocs/luci-static/resources/view/homeproxy/node.js:1353
#: htdocs/luci-static/resources/view/homeproxy/server.js:226 #: htdocs/luci-static/resources/view/homeproxy/server.js:226
#: htdocs/luci-static/resources/view/homeproxy/server.js:628 #: htdocs/luci-static/resources/view/homeproxy/server.js:628
#: htdocs/luci-static/resources/view/homeproxy/server.js:630 #: htdocs/luci-static/resources/view/homeproxy/server.js:630
@@ -843,11 +847,15 @@ msgstr ""
msgid "Failed to upload %s, error: %s." msgid "Failed to upload %s, error: %s."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1373 #: htdocs/luci-static/resources/view/homeproxy/status.js:140
msgid "Fatal"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1368
msgid "Filter keywords" msgid "Filter keywords"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1365 #: htdocs/luci-static/resources/view/homeproxy/node.js:1360
msgid "Filter nodes" msgid "Filter nodes"
msgstr "" msgstr ""
@@ -889,7 +897,7 @@ msgstr ""
msgid "GET" msgid "GET"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:209 #: htdocs/luci-static/resources/view/homeproxy/status.js:255
msgid "GFW list version" msgid "GFW list version"
msgstr "" msgstr ""
@@ -915,11 +923,11 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:294 #: htdocs/luci-static/resources/view/homeproxy/server.js:294
#: htdocs/luci-static/resources/view/homeproxy/server.js:355 #: htdocs/luci-static/resources/view/homeproxy/server.js:355
#: htdocs/luci-static/resources/view/homeproxy/server.js:357 #: htdocs/luci-static/resources/view/homeproxy/server.js:357
#: htdocs/luci-static/resources/view/homeproxy/server.js:817 #: htdocs/luci-static/resources/view/homeproxy/server.js:815
msgid "Generate" msgid "Generate"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:213 #: htdocs/luci-static/resources/view/homeproxy/status.js:259
msgid "GitHub token" msgid "GitHub token"
msgstr "" msgstr ""
@@ -947,7 +955,7 @@ msgstr ""
msgid "Global settings" msgid "Global settings"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:190 #: htdocs/luci-static/resources/view/homeproxy/status.js:237
msgid "Google" msgid "Google"
msgstr "" msgstr ""
@@ -987,11 +995,11 @@ msgstr ""
msgid "HTTPUpgrade" msgid "HTTPUpgrade"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:735 #: htdocs/luci-static/resources/view/homeproxy/server.js:734
msgid "Handshake server address" msgid "Handshake server address"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:741 #: htdocs/luci-static/resources/view/homeproxy/server.js:740
msgid "Handshake server port" msgid "Handshake server port"
msgstr "" msgstr ""
@@ -1007,7 +1015,7 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:55 #: htdocs/luci-static/resources/view/homeproxy/client.js:55
#: htdocs/luci-static/resources/view/homeproxy/client.js:57 #: htdocs/luci-static/resources/view/homeproxy/client.js:57
#: htdocs/luci-static/resources/view/homeproxy/client.js:101 #: htdocs/luci-static/resources/view/homeproxy/client.js:101
#: htdocs/luci-static/resources/view/homeproxy/status.js:234 #: htdocs/luci-static/resources/view/homeproxy/status.js:280
#: root/usr/share/luci/menu.d/luci-app-homeproxy.json:3 #: root/usr/share/luci/menu.d/luci-app-homeproxy.json:3
msgid "HomeProxy" msgid "HomeProxy"
msgstr "" msgstr ""
@@ -1150,18 +1158,18 @@ msgstr ""
msgid "Ignore client bandwidth" msgid "Ignore client bandwidth"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1284 #: htdocs/luci-static/resources/view/homeproxy/node.js:1279
msgid "Import" msgid "Import"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1231 #: htdocs/luci-static/resources/view/homeproxy/node.js:1226
#: htdocs/luci-static/resources/view/homeproxy/node.js:1310 #: htdocs/luci-static/resources/view/homeproxy/node.js:1305
#: htdocs/luci-static/resources/view/homeproxy/node.js:1312 #: htdocs/luci-static/resources/view/homeproxy/node.js:1307
msgid "Import share links" msgid "Import share links"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:336 #: htdocs/luci-static/resources/view/homeproxy/client.js:336
#: htdocs/luci-static/resources/view/homeproxy/server.js:850 #: htdocs/luci-static/resources/view/homeproxy/server.js:848
msgid "In seconds." msgid "In seconds."
msgstr "" msgstr ""
@@ -1184,6 +1192,10 @@ msgstr ""
msgid "Independent cache per server" msgid "Independent cache per server"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:137
msgid "Info"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:1403 #: htdocs/luci-static/resources/view/homeproxy/client.js:1403
msgid "Interface Control" msgid "Interface Control"
msgstr "" msgstr ""
@@ -1217,7 +1229,7 @@ msgstr ""
msgid "Invert match result." msgid "Invert match result."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:767 #: htdocs/luci-static/resources/view/homeproxy/server.js:765
msgid "Key path" msgid "Key path"
msgstr "" msgstr ""
@@ -1290,7 +1302,7 @@ msgstr ""
msgid "Listen port" msgid "Listen port"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:127 #: htdocs/luci-static/resources/view/homeproxy/status.js:173
msgid "Loading" msgid "Loading"
msgstr "" msgstr ""
@@ -1302,11 +1314,11 @@ msgstr ""
msgid "Local address" msgid "Local address"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:144 #: htdocs/luci-static/resources/view/homeproxy/status.js:190
msgid "Log file does not exist." msgid "Log file does not exist."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:137 #: htdocs/luci-static/resources/view/homeproxy/status.js:183
msgid "Log is empty." msgid "Log is empty."
msgstr "" msgstr ""
@@ -1451,7 +1463,7 @@ msgstr ""
msgid "Max download speed in Mbps." msgid "Max download speed in Mbps."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:730 #: htdocs/luci-static/resources/view/homeproxy/server.js:729
msgid "Max time difference" msgid "Max time difference"
msgstr "" msgstr ""
@@ -1527,8 +1539,8 @@ msgstr ""
msgid "Mode" msgid "Mode"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1163 #: htdocs/luci-static/resources/view/homeproxy/node.js:1158
#: htdocs/luci-static/resources/view/homeproxy/server.js:840 #: htdocs/luci-static/resources/view/homeproxy/server.js:838
msgid "MultiPath TCP" msgid "MultiPath TCP"
msgstr "" msgstr ""
@@ -1546,7 +1558,7 @@ msgstr ""
msgid "NOT RUNNING" msgid "NOT RUNNING"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1394 #: htdocs/luci-static/resources/view/homeproxy/node.js:1389
msgid "NOTE: Save current settings before updating subscriptions." msgid "NOTE: Save current settings before updating subscriptions."
msgstr "" msgstr ""
@@ -1564,7 +1576,7 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:637 #: htdocs/luci-static/resources/view/homeproxy/client.js:637
#: htdocs/luci-static/resources/view/homeproxy/client.js:1097 #: htdocs/luci-static/resources/view/homeproxy/client.js:1097
#: htdocs/luci-static/resources/view/homeproxy/server.js:856 #: htdocs/luci-static/resources/view/homeproxy/server.js:854
msgid "Network" msgid "Network"
msgstr "" msgstr ""
@@ -1584,15 +1596,15 @@ msgstr ""
msgid "No additional encryption support: It's basically duplicate encryption." msgid "No additional encryption support: It's basically duplicate encryption."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1410 #: htdocs/luci-static/resources/view/homeproxy/node.js:1405
msgid "No subscription available" msgid "No subscription available"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1435 #: htdocs/luci-static/resources/view/homeproxy/node.js:1430
msgid "No subscription node" msgid "No subscription node"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1270 #: htdocs/luci-static/resources/view/homeproxy/node.js:1265
msgid "No valid share link found." msgid "No valid share link found."
msgstr "" msgstr ""
@@ -1605,7 +1617,7 @@ msgstr ""
msgid "Node Settings" msgid "Node Settings"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1216 #: htdocs/luci-static/resources/view/homeproxy/node.js:1211
msgid "Nodes" msgid "Nodes"
msgstr "" msgstr ""
@@ -1687,6 +1699,10 @@ msgstr ""
msgid "Padding scheme" msgid "Padding scheme"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:141
msgid "Panic"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:463 #: htdocs/luci-static/resources/view/homeproxy/node.js:463
#: htdocs/luci-static/resources/view/homeproxy/server.js:190 #: htdocs/luci-static/resources/view/homeproxy/server.js:190
msgid "Password" msgid "Password"
@@ -1899,21 +1915,21 @@ msgstr ""
msgid "RDRC timeout" msgid "RDRC timeout"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1144 #: htdocs/luci-static/resources/view/homeproxy/node.js:1138
#: htdocs/luci-static/resources/view/homeproxy/server.js:715 #: htdocs/luci-static/resources/view/homeproxy/server.js:714
msgid "REALITY" msgid "REALITY"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:720 #: htdocs/luci-static/resources/view/homeproxy/server.js:719
msgid "REALITY private key" msgid "REALITY private key"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1148 #: htdocs/luci-static/resources/view/homeproxy/node.js:1143
msgid "REALITY public key" msgid "REALITY public key"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1153 #: htdocs/luci-static/resources/view/homeproxy/node.js:1148
#: htdocs/luci-static/resources/view/homeproxy/server.js:725 #: htdocs/luci-static/resources/view/homeproxy/server.js:724
msgid "REALITY short ID" msgid "REALITY short ID"
msgstr "" msgstr ""
@@ -1946,7 +1962,7 @@ msgstr ""
msgid "Redirect TCP + Tun UDP" msgid "Redirect TCP + Tun UDP"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:171 #: htdocs/luci-static/resources/view/homeproxy/status.js:218
msgid "Refresh every %s seconds." msgid "Refresh every %s seconds."
msgstr "" msgstr ""
@@ -1963,11 +1979,11 @@ msgstr ""
msgid "Remote" msgid "Remote"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1432 #: htdocs/luci-static/resources/view/homeproxy/node.js:1427
msgid "Remove %s nodes" msgid "Remove %s nodes"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1422 #: htdocs/luci-static/resources/view/homeproxy/node.js:1417
msgid "Remove all nodes from subscriptions" msgid "Remove all nodes from subscriptions"
msgstr "" msgstr ""
@@ -1991,15 +2007,15 @@ msgstr ""
msgid "Resolve strategy" msgid "Resolve strategy"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:194 #: htdocs/luci-static/resources/view/homeproxy/status.js:240
msgid "Resources management" msgid "Resources management"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:870 #: htdocs/luci-static/resources/view/homeproxy/server.js:868
msgid "Reuse address" msgid "Reuse address"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:871 #: htdocs/luci-static/resources/view/homeproxy/server.js:869
msgid "Reuse listener address." msgid "Reuse listener address."
msgstr "" msgstr ""
@@ -2081,7 +2097,7 @@ msgstr ""
msgid "STUN" msgid "STUN"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1176 #: htdocs/luci-static/resources/view/homeproxy/node.js:1171
msgid "SUoT version" msgid "SUoT version"
msgstr "" msgstr ""
@@ -2098,16 +2114,16 @@ msgstr ""
msgid "Same as main node" msgid "Same as main node"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:220 #: htdocs/luci-static/resources/view/homeproxy/status.js:266
#: htdocs/luci-static/resources/view/homeproxy/status.js:225 #: htdocs/luci-static/resources/view/homeproxy/status.js:271
msgid "Save" msgid "Save"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1396 #: htdocs/luci-static/resources/view/homeproxy/node.js:1391
msgid "Save current settings" msgid "Save current settings"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1393 #: htdocs/luci-static/resources/view/homeproxy/node.js:1388
msgid "Save subscriptions settings" msgid "Save subscriptions settings"
msgstr "" msgstr ""
@@ -2254,19 +2270,19 @@ msgstr ""
msgid "String" msgid "String"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1321 #: htdocs/luci-static/resources/view/homeproxy/node.js:1316
msgid "Sub (%s)" msgid "Sub (%s)"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1348 #: htdocs/luci-static/resources/view/homeproxy/node.js:1343
msgid "Subscription URL-s" msgid "Subscription URL-s"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1332 #: htdocs/luci-static/resources/view/homeproxy/node.js:1327
msgid "Subscriptions" msgid "Subscriptions"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1272 #: htdocs/luci-static/resources/view/homeproxy/node.js:1267
msgid "Successfully imported %s nodes of total %s." msgid "Successfully imported %s nodes of total %s."
msgstr "" msgstr ""
@@ -2274,8 +2290,8 @@ msgstr ""
msgid "Successfully updated." msgid "Successfully updated."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1232 #: htdocs/luci-static/resources/view/homeproxy/node.js:1227
#: htdocs/luci-static/resources/view/homeproxy/node.js:1349 #: htdocs/luci-static/resources/view/homeproxy/node.js:1344
msgid "" msgid ""
"Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) " "Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) "
"online configuration delivery standard." "online configuration delivery standard."
@@ -2302,12 +2318,12 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:638 #: htdocs/luci-static/resources/view/homeproxy/client.js:638
#: htdocs/luci-static/resources/view/homeproxy/client.js:949 #: htdocs/luci-static/resources/view/homeproxy/client.js:949
#: htdocs/luci-static/resources/view/homeproxy/client.js:1098 #: htdocs/luci-static/resources/view/homeproxy/client.js:1098
#: htdocs/luci-static/resources/view/homeproxy/server.js:857 #: htdocs/luci-static/resources/view/homeproxy/server.js:855
msgid "TCP" msgid "TCP"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1160 #: htdocs/luci-static/resources/view/homeproxy/node.js:1155
#: htdocs/luci-static/resources/view/homeproxy/server.js:835 #: htdocs/luci-static/resources/view/homeproxy/server.js:833
msgid "TCP fast open" msgid "TCP fast open"
msgstr "" msgstr ""
@@ -2507,7 +2523,7 @@ msgid ""
"allowed to open." "allowed to open."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:731 #: htdocs/luci-static/resources/view/homeproxy/server.js:730
msgid "The maximum time difference between the server and the client." msgid "The maximum time difference between the server and the client."
msgstr "" msgstr ""
@@ -2522,7 +2538,7 @@ msgid "The modern ImmortalWrt proxy platform for ARM64/AMD64."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:454 #: htdocs/luci-static/resources/view/homeproxy/client.js:454
#: htdocs/luci-static/resources/view/homeproxy/server.js:865 #: htdocs/luci-static/resources/view/homeproxy/server.js:863
msgid "The network interface to bind to." msgid "The network interface to bind to."
msgstr "" msgstr ""
@@ -2530,7 +2546,7 @@ msgstr ""
msgid "The path of the DNS server." msgid "The path of the DNS server."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1100 #: htdocs/luci-static/resources/view/homeproxy/node.js:1095
msgid "" msgid ""
"The path to the ECH config, in PEM format. If empty, load from DNS will be " "The path to the ECH config, in PEM format. If empty, load from DNS will be "
"attempted." "attempted."
@@ -2552,11 +2568,11 @@ msgstr ""
msgid "The response code." msgid "The response code."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:768 #: htdocs/luci-static/resources/view/homeproxy/server.js:766
msgid "The server private key, in PEM format." msgid "The server private key, in PEM format."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:749 #: htdocs/luci-static/resources/view/homeproxy/server.js:747
msgid "The server public key, in PEM format." msgid "The server public key, in PEM format."
msgstr "" msgstr ""
@@ -2587,7 +2603,7 @@ msgid ""
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1039 #: htdocs/luci-static/resources/view/homeproxy/node.js:1039
#: htdocs/luci-static/resources/view/homeproxy/node.js:1384 #: htdocs/luci-static/resources/view/homeproxy/node.js:1379
msgid "" msgid ""
"This is <strong>DANGEROUS</strong>, your traffic is almost like " "This is <strong>DANGEROUS</strong>, your traffic is almost like "
"<strong>PLAIN TEXT</strong>! Use at your own risk!" "<strong>PLAIN TEXT</strong>! Use at your own risk!"
@@ -2628,6 +2644,10 @@ msgid ""
"<code>kmod-tun</code>" "<code>kmod-tun</code>"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:135
msgid "Trace"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:769 #: htdocs/luci-static/resources/view/homeproxy/node.js:769
#: htdocs/luci-static/resources/view/homeproxy/server.js:409 #: htdocs/luci-static/resources/view/homeproxy/server.js:409
msgid "Transport" msgid "Transport"
@@ -2657,21 +2677,21 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:639 #: htdocs/luci-static/resources/view/homeproxy/client.js:639
#: htdocs/luci-static/resources/view/homeproxy/client.js:948 #: htdocs/luci-static/resources/view/homeproxy/client.js:948
#: htdocs/luci-static/resources/view/homeproxy/client.js:1099 #: htdocs/luci-static/resources/view/homeproxy/client.js:1099
#: htdocs/luci-static/resources/view/homeproxy/server.js:858 #: htdocs/luci-static/resources/view/homeproxy/server.js:856
msgid "UDP" msgid "UDP"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1166 #: htdocs/luci-static/resources/view/homeproxy/node.js:1161
#: htdocs/luci-static/resources/view/homeproxy/server.js:844 #: htdocs/luci-static/resources/view/homeproxy/server.js:842
msgid "UDP Fragment" msgid "UDP Fragment"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:335 #: htdocs/luci-static/resources/view/homeproxy/client.js:335
#: htdocs/luci-static/resources/view/homeproxy/server.js:849 #: htdocs/luci-static/resources/view/homeproxy/server.js:847
msgid "UDP NAT expiration time" msgid "UDP NAT expiration time"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1170 #: htdocs/luci-static/resources/view/homeproxy/node.js:1165
msgid "UDP over TCP" msgid "UDP over TCP"
msgstr "" msgstr ""
@@ -2712,15 +2732,15 @@ msgstr ""
msgid "Unknown error." msgid "Unknown error."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:148 #: htdocs/luci-static/resources/view/homeproxy/status.js:194
msgid "Unknown error: %s" msgid "Unknown error: %s"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1137 #: htdocs/luci-static/resources/view/homeproxy/node.js:1131
msgid "Unsupported fingerprint!" msgid "Unsupported fingerprint!"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1407 #: htdocs/luci-static/resources/view/homeproxy/node.js:1402
msgid "Update %s subscriptions" msgid "Update %s subscriptions"
msgstr "" msgstr ""
@@ -2736,23 +2756,23 @@ msgstr ""
msgid "Update interval of rule set." msgid "Update interval of rule set."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1402 #: htdocs/luci-static/resources/view/homeproxy/node.js:1397
msgid "Update nodes from subscriptions" msgid "Update nodes from subscriptions"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1345 #: htdocs/luci-static/resources/view/homeproxy/node.js:1340
msgid "Update subscriptions via proxy." msgid "Update subscriptions via proxy."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1338 #: htdocs/luci-static/resources/view/homeproxy/node.js:1333
msgid "Update time" msgid "Update time"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1344 #: htdocs/luci-static/resources/view/homeproxy/node.js:1339
msgid "Update via proxy" msgid "Update via proxy"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1105 #: htdocs/luci-static/resources/view/homeproxy/node.js:1100
msgid "Upload ECH config" msgid "Upload ECH config"
msgstr "" msgstr ""
@@ -2767,18 +2787,18 @@ msgid "Upload bandwidth in Mbps."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1081 #: htdocs/luci-static/resources/view/homeproxy/node.js:1081
#: htdocs/luci-static/resources/view/homeproxy/server.js:759 #: htdocs/luci-static/resources/view/homeproxy/server.js:757
msgid "Upload certificate" msgid "Upload certificate"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:778 #: htdocs/luci-static/resources/view/homeproxy/server.js:776
msgid "Upload key" msgid "Upload key"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1084 #: htdocs/luci-static/resources/view/homeproxy/node.js:1084
#: htdocs/luci-static/resources/view/homeproxy/node.js:1108 #: htdocs/luci-static/resources/view/homeproxy/node.js:1103
#: htdocs/luci-static/resources/view/homeproxy/server.js:762 #: htdocs/luci-static/resources/view/homeproxy/server.js:760
#: htdocs/luci-static/resources/view/homeproxy/server.js:781 #: htdocs/luci-static/resources/view/homeproxy/server.js:779
msgid "Upload..." msgid "Upload..."
msgstr "" msgstr ""
@@ -2802,7 +2822,7 @@ msgstr ""
msgid "User" msgid "User"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1378 #: htdocs/luci-static/resources/view/homeproxy/node.js:1373
msgid "User-Agent" msgid "User-Agent"
msgstr "" msgstr ""
@@ -2830,12 +2850,16 @@ msgstr ""
msgid "WAN IP Policy" msgid "WAN IP Policy"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:138
msgid "Warn"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:776 #: htdocs/luci-static/resources/view/homeproxy/node.js:776
#: htdocs/luci-static/resources/view/homeproxy/server.js:416 #: htdocs/luci-static/resources/view/homeproxy/server.js:416
msgid "WebSocket" msgid "WebSocket"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1369 #: htdocs/luci-static/resources/view/homeproxy/node.js:1364
msgid "Whitelist mode" msgid "Whitelist mode"
msgstr "" msgstr ""
@@ -2860,7 +2884,7 @@ msgid "Write proxy protocol in the connection header."
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:885 #: htdocs/luci-static/resources/view/homeproxy/node.js:885
#: htdocs/luci-static/resources/view/homeproxy/node.js:1391 #: htdocs/luci-static/resources/view/homeproxy/node.js:1386
msgid "Xudp (Xray-core)" msgid "Xudp (Xray-core)"
msgstr "" msgstr ""
@@ -2873,7 +2897,7 @@ msgid "ZeroSSL"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1086 #: htdocs/luci-static/resources/view/homeproxy/node.js:1086
#: htdocs/luci-static/resources/view/homeproxy/server.js:764 #: htdocs/luci-static/resources/view/homeproxy/server.js:762
msgid "certificate" msgid "certificate"
msgstr "" msgstr ""
@@ -2917,19 +2941,19 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:504 #: htdocs/luci-static/resources/view/homeproxy/client.js:504
#: htdocs/luci-static/resources/view/homeproxy/client.js:1355 #: htdocs/luci-static/resources/view/homeproxy/client.js:1355
#: htdocs/luci-static/resources/view/homeproxy/node.js:487 #: htdocs/luci-static/resources/view/homeproxy/node.js:487
#: htdocs/luci-static/resources/view/homeproxy/node.js:1133 #: htdocs/luci-static/resources/view/homeproxy/node.js:1127
#: htdocs/luci-static/resources/view/homeproxy/server.js:226 #: htdocs/luci-static/resources/view/homeproxy/server.js:226
msgid "non-empty value" msgid "non-empty value"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:628 #: htdocs/luci-static/resources/view/homeproxy/node.js:628
#: htdocs/luci-static/resources/view/homeproxy/node.js:883 #: htdocs/luci-static/resources/view/homeproxy/node.js:883
#: htdocs/luci-static/resources/view/homeproxy/node.js:1389 #: htdocs/luci-static/resources/view/homeproxy/node.js:1384
msgid "none" msgid "none"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:884 #: htdocs/luci-static/resources/view/homeproxy/node.js:884
#: htdocs/luci-static/resources/view/homeproxy/node.js:1390 #: htdocs/luci-static/resources/view/homeproxy/node.js:1385
msgid "packet addr (v2ray-core v5+)" msgid "packet addr (v2ray-core v5+)"
msgstr "" msgstr ""
@@ -2937,7 +2961,7 @@ msgstr ""
msgid "passed" msgid "passed"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:783 #: htdocs/luci-static/resources/view/homeproxy/server.js:781
msgid "private key" msgid "private key"
msgstr "" msgstr ""
@@ -2945,19 +2969,19 @@ msgstr ""
msgid "quic-go / uquic chrome" msgid "quic-go / uquic chrome"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:237 #: htdocs/luci-static/resources/view/homeproxy/status.js:283
msgid "sing-box client" msgid "sing-box client"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:240 #: htdocs/luci-static/resources/view/homeproxy/status.js:286
msgid "sing-box server" msgid "sing-box server"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1115 #: htdocs/luci-static/resources/view/homeproxy/node.js:1109
msgid "uTLS fingerprint" msgid "uTLS fingerprint"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1116 #: htdocs/luci-static/resources/view/homeproxy/node.js:1110
msgid "" msgid ""
"uTLS is a fork of \"crypto/tls\", which provides ClientHello fingerprinting " "uTLS is a fork of \"crypto/tls\", which provides ClientHello fingerprinting "
"resistance." "resistance."
@@ -2968,7 +2992,7 @@ msgid "unchecked"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/homeproxy.js:237 #: htdocs/luci-static/resources/homeproxy.js:237
#: htdocs/luci-static/resources/view/homeproxy/node.js:1301 #: htdocs/luci-static/resources/view/homeproxy/node.js:1296
msgid "unique UCI identifier" msgid "unique UCI identifier"
msgstr "" msgstr ""
@@ -2978,13 +3002,13 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:499 #: htdocs/luci-static/resources/view/homeproxy/node.js:499
#: htdocs/luci-static/resources/view/homeproxy/node.js:642 #: htdocs/luci-static/resources/view/homeproxy/node.js:642
#: htdocs/luci-static/resources/view/homeproxy/node.js:1177 #: htdocs/luci-static/resources/view/homeproxy/node.js:1172
msgid "v1" msgid "v1"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:500 #: htdocs/luci-static/resources/view/homeproxy/node.js:500
#: htdocs/luci-static/resources/view/homeproxy/node.js:643 #: htdocs/luci-static/resources/view/homeproxy/node.js:643
#: htdocs/luci-static/resources/view/homeproxy/node.js:1178 #: htdocs/luci-static/resources/view/homeproxy/node.js:1173
msgid "v2" msgid "v2"
msgstr "" msgstr ""
@@ -3003,8 +3027,8 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:521 #: htdocs/luci-static/resources/view/homeproxy/client.js:521
#: htdocs/luci-static/resources/view/homeproxy/client.js:1360 #: htdocs/luci-static/resources/view/homeproxy/client.js:1360
#: htdocs/luci-static/resources/view/homeproxy/client.js:1363 #: htdocs/luci-static/resources/view/homeproxy/client.js:1363
#: htdocs/luci-static/resources/view/homeproxy/node.js:1355 #: htdocs/luci-static/resources/view/homeproxy/node.js:1350
#: htdocs/luci-static/resources/view/homeproxy/node.js:1358 #: htdocs/luci-static/resources/view/homeproxy/node.js:1353
msgid "valid URL" msgid "valid URL"
msgstr "" msgstr ""

View File

@@ -8,11 +8,11 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: htdocs/luci-static/resources/view/homeproxy/status.js:159 #: htdocs/luci-static/resources/view/homeproxy/status.js:205
msgid "%s log" msgid "%s log"
msgstr "%s 日志" msgstr "%s 日志"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1454 #: htdocs/luci-static/resources/view/homeproxy/node.js:1449
msgid "%s nodes removed" msgid "%s nodes removed"
msgstr "移除了 %s 个节点" msgstr "移除了 %s 个节点"
@@ -34,9 +34,9 @@ msgstr ""
"<code>%s</code>。" "<code>%s</code>。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1082 #: htdocs/luci-static/resources/view/homeproxy/node.js:1082
#: htdocs/luci-static/resources/view/homeproxy/node.js:1106 #: htdocs/luci-static/resources/view/homeproxy/node.js:1101
#: htdocs/luci-static/resources/view/homeproxy/server.js:760 #: htdocs/luci-static/resources/view/homeproxy/server.js:758
#: htdocs/luci-static/resources/view/homeproxy/server.js:779 #: htdocs/luci-static/resources/view/homeproxy/server.js:777
msgid "<strong>Save your configuration before uploading files!</strong>" msgid "<strong>Save your configuration before uploading files!</strong>"
msgstr "<strong>上传文件前请先保存配置!</strong>" msgstr "<strong>上传文件前请先保存配置!</strong>"
@@ -132,7 +132,7 @@ msgid "Allow access from the Internet."
msgstr "允许来自互联网的访问。" msgstr "允许来自互联网的访问。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1036 #: htdocs/luci-static/resources/view/homeproxy/node.js:1036
#: htdocs/luci-static/resources/view/homeproxy/node.js:1381 #: htdocs/luci-static/resources/view/homeproxy/node.js:1376
msgid "Allow insecure" msgid "Allow insecure"
msgstr "允许不安全连接" msgstr "允许不安全连接"
@@ -140,7 +140,7 @@ msgstr "允许不安全连接"
msgid "Allow insecure connection at TLS client." msgid "Allow insecure connection at TLS client."
msgstr "允许 TLS 客户端侧的不安全连接。" msgstr "允许 TLS 客户端侧的不安全连接。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1382 #: htdocs/luci-static/resources/view/homeproxy/node.js:1377
msgid "Allow insecure connection by default when add nodes from subscriptions." msgid "Allow insecure connection by default when add nodes from subscriptions."
msgstr "从订阅获取节点时,默认允许不安全连接。" msgstr "从订阅获取节点时,默认允许不安全连接。"
@@ -170,7 +170,7 @@ msgstr "替代 HTTP 端口"
msgid "Alternative TLS port" msgid "Alternative TLS port"
msgstr "替代 HTTPS 端口" msgstr "替代 HTTPS 端口"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1417 #: htdocs/luci-static/resources/view/homeproxy/node.js:1412
msgid "An error occurred during updating subscriptions: %s" msgid "An error occurred during updating subscriptions: %s"
msgstr "更新订阅时发生错误:%s" msgstr "更新订阅时发生错误:%s"
@@ -233,11 +233,11 @@ msgstr "认证载荷"
msgid "Authentication type" msgid "Authentication type"
msgstr "认证类型" msgstr "认证类型"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1334 #: htdocs/luci-static/resources/view/homeproxy/node.js:1329
msgid "Auto update" msgid "Auto update"
msgstr "自动更新" msgstr "自动更新"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1335 #: htdocs/luci-static/resources/view/homeproxy/node.js:1330
msgid "Auto update subscriptions and geodata." msgid "Auto update subscriptions and geodata."
msgstr "自动更新订阅和地理数据。" msgstr "自动更新订阅和地理数据。"
@@ -245,7 +245,7 @@ msgstr "自动更新订阅和地理数据。"
msgid "BBR" msgid "BBR"
msgstr "BBR" msgstr "BBR"
#: htdocs/luci-static/resources/view/homeproxy/status.js:187 #: htdocs/luci-static/resources/view/homeproxy/status.js:234
msgid "BaiDu" msgid "BaiDu"
msgstr "百度" msgstr "百度"
@@ -264,7 +264,7 @@ msgstr "二进制文件"
#: htdocs/luci-static/resources/view/homeproxy/client.js:453 #: htdocs/luci-static/resources/view/homeproxy/client.js:453
#: htdocs/luci-static/resources/view/homeproxy/client.js:1410 #: htdocs/luci-static/resources/view/homeproxy/client.js:1410
#: htdocs/luci-static/resources/view/homeproxy/server.js:864 #: htdocs/luci-static/resources/view/homeproxy/server.js:862
msgid "Bind interface" msgid "Bind interface"
msgstr "绑定接口" msgstr "绑定接口"
@@ -278,7 +278,7 @@ msgstr "绑定出站流量至指定端口。留空自动检测。"
msgid "BitTorrent" msgid "BitTorrent"
msgstr "BitTorrent" msgstr "BitTorrent"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1368 #: htdocs/luci-static/resources/view/homeproxy/node.js:1363
msgid "Blacklist mode" msgid "Blacklist mode"
msgstr "黑名单模式" msgstr "黑名单模式"
@@ -290,7 +290,7 @@ msgstr "封锁"
#: htdocs/luci-static/resources/view/homeproxy/client.js:640 #: htdocs/luci-static/resources/view/homeproxy/client.js:640
#: htdocs/luci-static/resources/view/homeproxy/client.js:1090 #: htdocs/luci-static/resources/view/homeproxy/client.js:1090
#: htdocs/luci-static/resources/view/homeproxy/client.js:1100 #: htdocs/luci-static/resources/view/homeproxy/client.js:1100
#: htdocs/luci-static/resources/view/homeproxy/server.js:859 #: htdocs/luci-static/resources/view/homeproxy/server.js:857
msgid "Both" msgid "Both"
msgstr "全部" msgstr "全部"
@@ -318,12 +318,12 @@ msgstr "CNNIC 公共 DNS210.2.4.8"
msgid "CUBIC" msgid "CUBIC"
msgstr "CUBIC" msgstr "CUBIC"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1238 #: htdocs/luci-static/resources/view/homeproxy/node.js:1233
msgid "Cancel" msgid "Cancel"
msgstr "取消" msgstr "取消"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1073 #: htdocs/luci-static/resources/view/homeproxy/node.js:1073
#: htdocs/luci-static/resources/view/homeproxy/server.js:748 #: htdocs/luci-static/resources/view/homeproxy/server.js:746
msgid "Certificate path" msgid "Certificate path"
msgstr "证书路径" msgstr "证书路径"
@@ -339,15 +339,15 @@ msgstr "检查更新"
msgid "China DNS server" msgid "China DNS server"
msgstr "国内 DNS 服务器" msgstr "国内 DNS 服务器"
#: htdocs/luci-static/resources/view/homeproxy/status.js:197 #: htdocs/luci-static/resources/view/homeproxy/status.js:243
msgid "China IPv4 list version" msgid "China IPv4 list version"
msgstr "国内 IPv4 库版本" msgstr "国内 IPv4 库版本"
#: htdocs/luci-static/resources/view/homeproxy/status.js:201 #: htdocs/luci-static/resources/view/homeproxy/status.js:247
msgid "China IPv6 list version" msgid "China IPv6 list version"
msgstr "国内 IPv6 库版本" msgstr "国内 IPv6 库版本"
#: htdocs/luci-static/resources/view/homeproxy/status.js:205 #: htdocs/luci-static/resources/view/homeproxy/status.js:251
msgid "China list version" msgid "China list version"
msgstr "国内域名列表版本" msgstr "国内域名列表版本"
@@ -364,7 +364,7 @@ msgstr "密码套件"
msgid "Cisco Public DNS (208.67.222.222)" msgid "Cisco Public DNS (208.67.222.222)"
msgstr "思科公共 DNS208.67.222.222" msgstr "思科公共 DNS208.67.222.222"
#: htdocs/luci-static/resources/view/homeproxy/status.js:166 #: htdocs/luci-static/resources/view/homeproxy/status.js:213
msgid "Clean log" msgid "Clean log"
msgstr "清空日志" msgstr "清空日志"
@@ -390,7 +390,7 @@ msgstr "Cloudflare"
#: htdocs/luci-static/resources/view/homeproxy/client.js:114 #: htdocs/luci-static/resources/view/homeproxy/client.js:114
#: htdocs/luci-static/resources/view/homeproxy/server.js:122 #: htdocs/luci-static/resources/view/homeproxy/server.js:122
#: htdocs/luci-static/resources/view/homeproxy/status.js:129 #: htdocs/luci-static/resources/view/homeproxy/status.js:175
msgid "Collecting data..." msgid "Collecting data..."
msgstr "正在收集数据中..." msgstr "正在收集数据中..."
@@ -403,7 +403,7 @@ msgstr "仅常用端口(绕过 P2P 流量)"
msgid "Congestion control algorithm" msgid "Congestion control algorithm"
msgstr "拥塞控制算法" msgstr "拥塞控制算法"
#: htdocs/luci-static/resources/view/homeproxy/status.js:184 #: htdocs/luci-static/resources/view/homeproxy/status.js:231
msgid "Connection check" msgid "Connection check"
msgstr "连接检查" msgstr "连接检查"
@@ -450,6 +450,10 @@ msgstr "DNS01 验证"
msgid "DTLS" msgid "DTLS"
msgstr "DTLS" msgstr "DTLS"
#: htdocs/luci-static/resources/view/homeproxy/status.js:136
msgid "Debug"
msgstr "调试"
#: htdocs/luci-static/resources/homeproxy.js:17 #: htdocs/luci-static/resources/homeproxy.js:17
#: htdocs/luci-static/resources/view/homeproxy/client.js:433 #: htdocs/luci-static/resources/view/homeproxy/client.js:433
#: htdocs/luci-static/resources/view/homeproxy/client.js:603 #: htdocs/luci-static/resources/view/homeproxy/client.js:603
@@ -493,7 +497,7 @@ msgstr "默认出站 DNS"
msgid "Default outbound for connections not matched by any routing rules." msgid "Default outbound for connections not matched by any routing rules."
msgstr "用于未被任何路由规则匹配的连接的默认出站。" msgstr "用于未被任何路由规则匹配的连接的默认出站。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1388 #: htdocs/luci-static/resources/view/homeproxy/node.js:1383
msgid "Default packet encoding" msgid "Default packet encoding"
msgstr "默认包封装格式" msgstr "默认包封装格式"
@@ -534,8 +538,8 @@ msgstr "直连 MAC 地址"
#: htdocs/luci-static/resources/view/homeproxy/node.js:498 #: htdocs/luci-static/resources/view/homeproxy/node.js:498
#: htdocs/luci-static/resources/view/homeproxy/node.js:554 #: htdocs/luci-static/resources/view/homeproxy/node.js:554
#: htdocs/luci-static/resources/view/homeproxy/node.js:566 #: htdocs/luci-static/resources/view/homeproxy/node.js:566
#: htdocs/luci-static/resources/view/homeproxy/node.js:1117 #: htdocs/luci-static/resources/view/homeproxy/node.js:1111
#: htdocs/luci-static/resources/view/homeproxy/node.js:1367 #: htdocs/luci-static/resources/view/homeproxy/node.js:1362
#: htdocs/luci-static/resources/view/homeproxy/server.js:267 #: htdocs/luci-static/resources/view/homeproxy/server.js:267
#: htdocs/luci-static/resources/view/homeproxy/server.js:279 #: htdocs/luci-static/resources/view/homeproxy/server.js:279
msgid "Disable" msgid "Disable"
@@ -658,7 +662,7 @@ msgstr "丢弃数据包"
msgid "Drop requests" msgid "Drop requests"
msgstr "丢弃请求" msgstr "丢弃请求"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1374 #: htdocs/luci-static/resources/view/homeproxy/node.js:1369
msgid "" msgid ""
"Drop/keep nodes that contain the specific keywords. <a target=\"_blank\" " "Drop/keep nodes that contain the specific keywords. <a target=\"_blank\" "
"href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/" "href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/"
@@ -668,7 +672,7 @@ msgstr ""
"developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions\">" "developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions\">"
"正则表达式</a>。" "正则表达式</a>。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1366 #: htdocs/luci-static/resources/view/homeproxy/node.js:1361
msgid "Drop/keep specific nodes from subscriptions." msgid "Drop/keep specific nodes from subscriptions."
msgstr "从订阅中 丢弃/保留 指定节点" msgstr "从订阅中 丢弃/保留 指定节点"
@@ -683,7 +687,7 @@ msgstr ""
"<br/>外部帐户绑定“用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA " "<br/>外部帐户绑定“用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA "
"客户数据库。" "客户数据库。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1091 #: htdocs/luci-static/resources/view/homeproxy/node.js:1090
msgid "" msgid ""
"ECH (Encrypted Client Hello) is a TLS extension that allows a client to " "ECH (Encrypted Client Hello) is a TLS extension that allows a client to "
"encrypt the first part of its ClientHello message." "encrypt the first part of its ClientHello message."
@@ -691,16 +695,16 @@ msgstr ""
"ECHEncrypted Client Hello是一个 TLS 扩展,它允许客户端加密其 ClientHello " "ECHEncrypted Client Hello是一个 TLS 扩展,它允许客户端加密其 ClientHello "
"信息的第一部分。" "信息的第一部分。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1110 #: htdocs/luci-static/resources/view/homeproxy/node.js:1105
#: htdocs/luci-static/resources/view/homeproxy/server.js:825 #: htdocs/luci-static/resources/view/homeproxy/server.js:823
msgid "ECH config" msgid "ECH config"
msgstr "ECH 配置" msgstr "ECH 配置"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1099 #: htdocs/luci-static/resources/view/homeproxy/node.js:1094
msgid "ECH config path" msgid "ECH config path"
msgstr "ECH 配置路径" msgstr "ECH 配置路径"
#: htdocs/luci-static/resources/view/homeproxy/server.js:786 #: htdocs/luci-static/resources/view/homeproxy/server.js:784
msgid "ECH key" msgid "ECH key"
msgstr "ECH 密钥" msgstr "ECH 密钥"
@@ -724,7 +728,7 @@ msgstr "前置数据标头"
msgid "Early data is sent in path instead of header by default." msgid "Early data is sent in path instead of header by default."
msgstr "前置数据默认发送在路径而不是标头中。" msgstr "前置数据默认发送在路径而不是标头中。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1210 #: htdocs/luci-static/resources/view/homeproxy/node.js:1205
msgid "Edit nodes" msgid "Edit nodes"
msgstr "修改节点" msgstr "修改节点"
@@ -761,14 +765,10 @@ msgstr "启用 0-RTT 握手"
msgid "Enable ACME" msgid "Enable ACME"
msgstr "启用 ACME" msgstr "启用 ACME"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1090 #: htdocs/luci-static/resources/view/homeproxy/node.js:1089
msgid "Enable ECH" msgid "Enable ECH"
msgstr "启用 ECH" msgstr "启用 ECH"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1095
msgid "Enable PQ signature schemes"
msgstr "启用 PQ 签名方案"
#: htdocs/luci-static/resources/view/homeproxy/node.js:980 #: htdocs/luci-static/resources/view/homeproxy/node.js:980
#: htdocs/luci-static/resources/view/homeproxy/server.js:522 #: htdocs/luci-static/resources/view/homeproxy/server.js:522
msgid "Enable TCP Brutal" msgid "Enable TCP Brutal"
@@ -779,8 +779,8 @@ msgstr "启用 TCP Brutal"
msgid "Enable TCP Brutal congestion control algorithm" msgid "Enable TCP Brutal congestion control algorithm"
msgstr "启用 TCP Brutal 拥塞控制算法。" msgstr "启用 TCP Brutal 拥塞控制算法。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1167 #: htdocs/luci-static/resources/view/homeproxy/node.js:1162
#: htdocs/luci-static/resources/view/homeproxy/server.js:845 #: htdocs/luci-static/resources/view/homeproxy/server.js:843
msgid "Enable UDP fragmentation." msgid "Enable UDP fragmentation."
msgstr "启用 UDP 分片。" msgstr "启用 UDP 分片。"
@@ -793,11 +793,11 @@ msgstr "启用端点独立 NAT"
msgid "Enable padding" msgid "Enable padding"
msgstr "启用填充" msgstr "启用填充"
#: htdocs/luci-static/resources/view/homeproxy/server.js:836 #: htdocs/luci-static/resources/view/homeproxy/server.js:834
msgid "Enable tcp fast open for listener." msgid "Enable tcp fast open for listener."
msgstr "为监听器启用 TCP 快速打开。" msgstr "为监听器启用 TCP 快速打开。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1171 #: htdocs/luci-static/resources/view/homeproxy/node.js:1166
msgid "" msgid ""
"Enable the SUoT protocol, requires server support. Conflict with multiplex." "Enable the SUoT protocol, requires server support. Conflict with multiplex."
msgstr "启用 SUoT 协议,需要服务端支持。与多路复用冲突。" msgstr "启用 SUoT 协议,需要服务端支持。与多路复用冲突。"
@@ -808,6 +808,10 @@ msgstr "启用 SUoT 协议,需要服务端支持。与多路复用冲突。"
msgid "Encrypt method" msgid "Encrypt method"
msgstr "加密方式" msgstr "加密方式"
#: htdocs/luci-static/resources/view/homeproxy/status.js:139
msgid "Error"
msgstr "错误"
#: htdocs/luci-static/resources/homeproxy.js:237 #: htdocs/luci-static/resources/homeproxy.js:237
#: htdocs/luci-static/resources/homeproxy.js:271 #: htdocs/luci-static/resources/homeproxy.js:271
#: htdocs/luci-static/resources/homeproxy.js:279 #: htdocs/luci-static/resources/homeproxy.js:279
@@ -832,10 +836,10 @@ msgstr "加密方式"
#: htdocs/luci-static/resources/view/homeproxy/client.js:1505 #: htdocs/luci-static/resources/view/homeproxy/client.js:1505
#: htdocs/luci-static/resources/view/homeproxy/client.js:1537 #: htdocs/luci-static/resources/view/homeproxy/client.js:1537
#: htdocs/luci-static/resources/view/homeproxy/node.js:487 #: htdocs/luci-static/resources/view/homeproxy/node.js:487
#: htdocs/luci-static/resources/view/homeproxy/node.js:1133 #: htdocs/luci-static/resources/view/homeproxy/node.js:1127
#: htdocs/luci-static/resources/view/homeproxy/node.js:1301 #: htdocs/luci-static/resources/view/homeproxy/node.js:1296
#: htdocs/luci-static/resources/view/homeproxy/node.js:1355 #: htdocs/luci-static/resources/view/homeproxy/node.js:1350
#: htdocs/luci-static/resources/view/homeproxy/node.js:1358 #: htdocs/luci-static/resources/view/homeproxy/node.js:1353
#: htdocs/luci-static/resources/view/homeproxy/server.js:226 #: htdocs/luci-static/resources/view/homeproxy/server.js:226
#: htdocs/luci-static/resources/view/homeproxy/server.js:628 #: htdocs/luci-static/resources/view/homeproxy/server.js:628
#: htdocs/luci-static/resources/view/homeproxy/server.js:630 #: htdocs/luci-static/resources/view/homeproxy/server.js:630
@@ -866,11 +870,15 @@ msgstr "生成 %s 失败,错误:%s。"
msgid "Failed to upload %s, error: %s." msgid "Failed to upload %s, error: %s."
msgstr "上传 %s 失败,错误:%s。" msgstr "上传 %s 失败,错误:%s。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1373 #: htdocs/luci-static/resources/view/homeproxy/status.js:140
msgid "Fatal"
msgstr "致命"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1368
msgid "Filter keywords" msgid "Filter keywords"
msgstr "过滤关键词" msgstr "过滤关键词"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1365 #: htdocs/luci-static/resources/view/homeproxy/node.js:1360
msgid "Filter nodes" msgid "Filter nodes"
msgstr "过滤节点" msgstr "过滤节点"
@@ -912,7 +920,7 @@ msgstr "分片回退延迟"
msgid "GET" msgid "GET"
msgstr "GET" msgstr "GET"
#: htdocs/luci-static/resources/view/homeproxy/status.js:209 #: htdocs/luci-static/resources/view/homeproxy/status.js:255
msgid "GFW list version" msgid "GFW list version"
msgstr "GFW 域名列表版本" msgstr "GFW 域名列表版本"
@@ -938,11 +946,11 @@ msgstr "游戏模式 MAC 地址"
#: htdocs/luci-static/resources/view/homeproxy/server.js:294 #: htdocs/luci-static/resources/view/homeproxy/server.js:294
#: htdocs/luci-static/resources/view/homeproxy/server.js:355 #: htdocs/luci-static/resources/view/homeproxy/server.js:355
#: htdocs/luci-static/resources/view/homeproxy/server.js:357 #: htdocs/luci-static/resources/view/homeproxy/server.js:357
#: htdocs/luci-static/resources/view/homeproxy/server.js:817 #: htdocs/luci-static/resources/view/homeproxy/server.js:815
msgid "Generate" msgid "Generate"
msgstr "生成" msgstr "生成"
#: htdocs/luci-static/resources/view/homeproxy/status.js:213 #: htdocs/luci-static/resources/view/homeproxy/status.js:259
msgid "GitHub token" msgid "GitHub token"
msgstr "GitHub 令牌" msgstr "GitHub 令牌"
@@ -970,7 +978,7 @@ msgstr "全局代理 MAC 地址"
msgid "Global settings" msgid "Global settings"
msgstr "全局设置" msgstr "全局设置"
#: htdocs/luci-static/resources/view/homeproxy/status.js:190 #: htdocs/luci-static/resources/view/homeproxy/status.js:237
msgid "Google" msgid "Google"
msgstr "谷歌" msgstr "谷歌"
@@ -1010,11 +1018,11 @@ msgstr "HTTPS"
msgid "HTTPUpgrade" msgid "HTTPUpgrade"
msgstr "HTTPUpgrade" msgstr "HTTPUpgrade"
#: htdocs/luci-static/resources/view/homeproxy/server.js:735 #: htdocs/luci-static/resources/view/homeproxy/server.js:734
msgid "Handshake server address" msgid "Handshake server address"
msgstr "握手服务器地址" msgstr "握手服务器地址"
#: htdocs/luci-static/resources/view/homeproxy/server.js:741 #: htdocs/luci-static/resources/view/homeproxy/server.js:740
msgid "Handshake server port" msgid "Handshake server port"
msgstr "握手服务器端口" msgstr "握手服务器端口"
@@ -1030,7 +1038,7 @@ msgstr "心跳间隔"
#: htdocs/luci-static/resources/view/homeproxy/client.js:55 #: htdocs/luci-static/resources/view/homeproxy/client.js:55
#: htdocs/luci-static/resources/view/homeproxy/client.js:57 #: htdocs/luci-static/resources/view/homeproxy/client.js:57
#: htdocs/luci-static/resources/view/homeproxy/client.js:101 #: htdocs/luci-static/resources/view/homeproxy/client.js:101
#: htdocs/luci-static/resources/view/homeproxy/status.js:234 #: htdocs/luci-static/resources/view/homeproxy/status.js:280
#: root/usr/share/luci/menu.d/luci-app-homeproxy.json:3 #: root/usr/share/luci/menu.d/luci-app-homeproxy.json:3
msgid "HomeProxy" msgid "HomeProxy"
msgstr "HomeProxy" msgstr "HomeProxy"
@@ -1177,18 +1185,18 @@ msgstr "如果你拥有根证书,使用此选项而不是允许不安全连接
msgid "Ignore client bandwidth" msgid "Ignore client bandwidth"
msgstr "忽略客户端带宽" msgstr "忽略客户端带宽"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1284 #: htdocs/luci-static/resources/view/homeproxy/node.js:1279
msgid "Import" msgid "Import"
msgstr "导入" msgstr "导入"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1231 #: htdocs/luci-static/resources/view/homeproxy/node.js:1226
#: htdocs/luci-static/resources/view/homeproxy/node.js:1310 #: htdocs/luci-static/resources/view/homeproxy/node.js:1305
#: htdocs/luci-static/resources/view/homeproxy/node.js:1312 #: htdocs/luci-static/resources/view/homeproxy/node.js:1307
msgid "Import share links" msgid "Import share links"
msgstr "导入分享链接" msgstr "导入分享链接"
#: htdocs/luci-static/resources/view/homeproxy/client.js:336 #: htdocs/luci-static/resources/view/homeproxy/client.js:336
#: htdocs/luci-static/resources/view/homeproxy/server.js:850 #: htdocs/luci-static/resources/view/homeproxy/server.js:848
msgid "In seconds." msgid "In seconds."
msgstr "单位:秒。" msgstr "单位:秒。"
@@ -1211,6 +1219,10 @@ msgstr "在检查中,关闭空闲时间超过此值的会话(单位:秒)
msgid "Independent cache per server" msgid "Independent cache per server"
msgstr "独立缓存" msgstr "独立缓存"
#: htdocs/luci-static/resources/view/homeproxy/status.js:137
msgid "Info"
msgstr "信息"
#: htdocs/luci-static/resources/view/homeproxy/client.js:1403 #: htdocs/luci-static/resources/view/homeproxy/client.js:1403
msgid "Interface Control" msgid "Interface Control"
msgstr "接口控制" msgstr "接口控制"
@@ -1244,7 +1256,7 @@ msgstr "反转"
msgid "Invert match result." msgid "Invert match result."
msgstr "反转匹配结果" msgstr "反转匹配结果"
#: htdocs/luci-static/resources/view/homeproxy/server.js:767 #: htdocs/luci-static/resources/view/homeproxy/server.js:765
msgid "Key path" msgid "Key path"
msgstr "证书路径" msgstr "证书路径"
@@ -1319,7 +1331,7 @@ msgstr "监听接口"
msgid "Listen port" msgid "Listen port"
msgstr "监听端口" msgstr "监听端口"
#: htdocs/luci-static/resources/view/homeproxy/status.js:127 #: htdocs/luci-static/resources/view/homeproxy/status.js:173
msgid "Loading" msgid "Loading"
msgstr "加载中" msgstr "加载中"
@@ -1331,11 +1343,11 @@ msgstr "本地"
msgid "Local address" msgid "Local address"
msgstr "本地地址" msgstr "本地地址"
#: htdocs/luci-static/resources/view/homeproxy/status.js:144 #: htdocs/luci-static/resources/view/homeproxy/status.js:190
msgid "Log file does not exist." msgid "Log file does not exist."
msgstr "日志文件不存在。" msgstr "日志文件不存在。"
#: htdocs/luci-static/resources/view/homeproxy/status.js:137 #: htdocs/luci-static/resources/view/homeproxy/status.js:183
msgid "Log is empty." msgid "Log is empty."
msgstr "日志为空。" msgstr "日志为空。"
@@ -1480,7 +1492,7 @@ msgstr "最大下载速度"
msgid "Max download speed in Mbps." msgid "Max download speed in Mbps."
msgstr "最大下载速度Mbps。" msgstr "最大下载速度Mbps。"
#: htdocs/luci-static/resources/view/homeproxy/server.js:730 #: htdocs/luci-static/resources/view/homeproxy/server.js:729
msgid "Max time difference" msgid "Max time difference"
msgstr "最大时间差" msgstr "最大时间差"
@@ -1558,8 +1570,8 @@ msgstr "混合<code>系统</code> TCP 栈和 <code>gVisor</code> UDP 栈。"
msgid "Mode" msgid "Mode"
msgstr "模式" msgstr "模式"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1163 #: htdocs/luci-static/resources/view/homeproxy/node.js:1158
#: htdocs/luci-static/resources/view/homeproxy/server.js:840 #: htdocs/luci-static/resources/view/homeproxy/server.js:838
msgid "MultiPath TCP" msgid "MultiPath TCP"
msgstr "多路径 TCPMPTCP" msgstr "多路径 TCPMPTCP"
@@ -1577,7 +1589,7 @@ msgstr "多路复用协议。"
msgid "NOT RUNNING" msgid "NOT RUNNING"
msgstr "未运行" msgstr "未运行"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1394 #: htdocs/luci-static/resources/view/homeproxy/node.js:1389
msgid "NOTE: Save current settings before updating subscriptions." msgid "NOTE: Save current settings before updating subscriptions."
msgstr "注意:更新订阅前先保存当前配置。" msgstr "注意:更新订阅前先保存当前配置。"
@@ -1595,7 +1607,7 @@ msgstr "NaïveProxy"
#: htdocs/luci-static/resources/view/homeproxy/client.js:637 #: htdocs/luci-static/resources/view/homeproxy/client.js:637
#: htdocs/luci-static/resources/view/homeproxy/client.js:1097 #: htdocs/luci-static/resources/view/homeproxy/client.js:1097
#: htdocs/luci-static/resources/view/homeproxy/server.js:856 #: htdocs/luci-static/resources/view/homeproxy/server.js:854
msgid "Network" msgid "Network"
msgstr "网络" msgstr "网络"
@@ -1615,15 +1627,15 @@ msgstr "无 TCP 传输层, 纯 HTTP 已合并到 HTTP 传输层。"
msgid "No additional encryption support: It's basically duplicate encryption." msgid "No additional encryption support: It's basically duplicate encryption."
msgstr "无额外加密支持:它基本上是重复加密。" msgstr "无额外加密支持:它基本上是重复加密。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1410 #: htdocs/luci-static/resources/view/homeproxy/node.js:1405
msgid "No subscription available" msgid "No subscription available"
msgstr "无可用订阅" msgstr "无可用订阅"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1435 #: htdocs/luci-static/resources/view/homeproxy/node.js:1430
msgid "No subscription node" msgid "No subscription node"
msgstr "无订阅节点" msgstr "无订阅节点"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1270 #: htdocs/luci-static/resources/view/homeproxy/node.js:1265
msgid "No valid share link found." msgid "No valid share link found."
msgstr "找不到有效分享链接。" msgstr "找不到有效分享链接。"
@@ -1636,7 +1648,7 @@ msgstr "节点"
msgid "Node Settings" msgid "Node Settings"
msgstr "节点设置" msgstr "节点设置"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1216 #: htdocs/luci-static/resources/view/homeproxy/node.js:1211
msgid "Nodes" msgid "Nodes"
msgstr "节点" msgstr "节点"
@@ -1718,6 +1730,10 @@ msgstr "数据包编码"
msgid "Padding scheme" msgid "Padding scheme"
msgstr "填充方案" msgstr "填充方案"
#: htdocs/luci-static/resources/view/homeproxy/status.js:141
msgid "Panic"
msgstr "崩溃"
#: htdocs/luci-static/resources/view/homeproxy/node.js:463 #: htdocs/luci-static/resources/view/homeproxy/node.js:463
#: htdocs/luci-static/resources/view/homeproxy/server.js:190 #: htdocs/luci-static/resources/view/homeproxy/server.js:190
msgid "Password" msgid "Password"
@@ -1930,21 +1946,21 @@ msgstr "RDP"
msgid "RDRC timeout" msgid "RDRC timeout"
msgstr "RDRC 超时" msgstr "RDRC 超时"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1144 #: htdocs/luci-static/resources/view/homeproxy/node.js:1138
#: htdocs/luci-static/resources/view/homeproxy/server.js:715 #: htdocs/luci-static/resources/view/homeproxy/server.js:714
msgid "REALITY" msgid "REALITY"
msgstr "REALITY" msgstr "REALITY"
#: htdocs/luci-static/resources/view/homeproxy/server.js:720 #: htdocs/luci-static/resources/view/homeproxy/server.js:719
msgid "REALITY private key" msgid "REALITY private key"
msgstr "REALITY 私钥" msgstr "REALITY 私钥"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1148 #: htdocs/luci-static/resources/view/homeproxy/node.js:1143
msgid "REALITY public key" msgid "REALITY public key"
msgstr "REALITY 公钥" msgstr "REALITY 公钥"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1153 #: htdocs/luci-static/resources/view/homeproxy/node.js:1148
#: htdocs/luci-static/resources/view/homeproxy/server.js:725 #: htdocs/luci-static/resources/view/homeproxy/server.js:724
msgid "REALITY short ID" msgid "REALITY short ID"
msgstr "REALITY 标识符" msgstr "REALITY 标识符"
@@ -1977,7 +1993,7 @@ msgstr "Redirect TCP + TProxy UDP"
msgid "Redirect TCP + Tun UDP" msgid "Redirect TCP + Tun UDP"
msgstr "Redirect TCP + Tun UDP" msgstr "Redirect TCP + Tun UDP"
#: htdocs/luci-static/resources/view/homeproxy/status.js:171 #: htdocs/luci-static/resources/view/homeproxy/status.js:218
msgid "Refresh every %s seconds." msgid "Refresh every %s seconds."
msgstr "每 %s 秒刷新。" msgstr "每 %s 秒刷新。"
@@ -1994,11 +2010,11 @@ msgstr "拒绝"
msgid "Remote" msgid "Remote"
msgstr "远程" msgstr "远程"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1432 #: htdocs/luci-static/resources/view/homeproxy/node.js:1427
msgid "Remove %s nodes" msgid "Remove %s nodes"
msgstr "移除 %s 个节点" msgstr "移除 %s 个节点"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1422 #: htdocs/luci-static/resources/view/homeproxy/node.js:1417
msgid "Remove all nodes from subscriptions" msgid "Remove all nodes from subscriptions"
msgstr "移除所有订阅节点" msgstr "移除所有订阅节点"
@@ -2022,15 +2038,15 @@ msgstr "解析"
msgid "Resolve strategy" msgid "Resolve strategy"
msgstr "解析策略" msgstr "解析策略"
#: htdocs/luci-static/resources/view/homeproxy/status.js:194 #: htdocs/luci-static/resources/view/homeproxy/status.js:240
msgid "Resources management" msgid "Resources management"
msgstr "资源管理" msgstr "资源管理"
#: htdocs/luci-static/resources/view/homeproxy/server.js:870 #: htdocs/luci-static/resources/view/homeproxy/server.js:868
msgid "Reuse address" msgid "Reuse address"
msgstr "复用地址" msgstr "复用地址"
#: htdocs/luci-static/resources/view/homeproxy/server.js:871 #: htdocs/luci-static/resources/view/homeproxy/server.js:869
msgid "Reuse listener address." msgid "Reuse listener address."
msgstr "复用监听地址。" msgstr "复用监听地址。"
@@ -2112,7 +2128,7 @@ msgstr "SSH"
msgid "STUN" msgid "STUN"
msgstr "STUN" msgstr "STUN"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1176 #: htdocs/luci-static/resources/view/homeproxy/node.js:1171
msgid "SUoT version" msgid "SUoT version"
msgstr "SUoT 版本" msgstr "SUoT 版本"
@@ -2129,16 +2145,16 @@ msgstr "Salamander"
msgid "Same as main node" msgid "Same as main node"
msgstr "保持与主节点一致" msgstr "保持与主节点一致"
#: htdocs/luci-static/resources/view/homeproxy/status.js:220 #: htdocs/luci-static/resources/view/homeproxy/status.js:266
#: htdocs/luci-static/resources/view/homeproxy/status.js:225 #: htdocs/luci-static/resources/view/homeproxy/status.js:271
msgid "Save" msgid "Save"
msgstr "保存" msgstr "保存"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1396 #: htdocs/luci-static/resources/view/homeproxy/node.js:1391
msgid "Save current settings" msgid "Save current settings"
msgstr "保存当前设置" msgstr "保存当前设置"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1393 #: htdocs/luci-static/resources/view/homeproxy/node.js:1388
msgid "Save subscriptions settings" msgid "Save subscriptions settings"
msgstr "保存订阅设置" msgstr "保存订阅设置"
@@ -2296,19 +2312,19 @@ msgstr ""
msgid "String" msgid "String"
msgstr "字符串" msgstr "字符串"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1321 #: htdocs/luci-static/resources/view/homeproxy/node.js:1316
msgid "Sub (%s)" msgid "Sub (%s)"
msgstr "订阅(%s" msgstr "订阅(%s"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1348 #: htdocs/luci-static/resources/view/homeproxy/node.js:1343
msgid "Subscription URL-s" msgid "Subscription URL-s"
msgstr "订阅地址" msgstr "订阅地址"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1332 #: htdocs/luci-static/resources/view/homeproxy/node.js:1327
msgid "Subscriptions" msgid "Subscriptions"
msgstr "订阅" msgstr "订阅"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1272 #: htdocs/luci-static/resources/view/homeproxy/node.js:1267
msgid "Successfully imported %s nodes of total %s." msgid "Successfully imported %s nodes of total %s."
msgstr "成功导入 %s 个节点,共 %s 个。" msgstr "成功导入 %s 个节点,共 %s 个。"
@@ -2316,8 +2332,8 @@ msgstr "成功导入 %s 个节点,共 %s 个。"
msgid "Successfully updated." msgid "Successfully updated."
msgstr "更新成功。" msgstr "更新成功。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1232 #: htdocs/luci-static/resources/view/homeproxy/node.js:1227
#: htdocs/luci-static/resources/view/homeproxy/node.js:1349 #: htdocs/luci-static/resources/view/homeproxy/node.js:1344
msgid "" msgid ""
"Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) " "Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) "
"online configuration delivery standard." "online configuration delivery standard."
@@ -2346,12 +2362,12 @@ msgstr "系统 DNS"
#: htdocs/luci-static/resources/view/homeproxy/client.js:638 #: htdocs/luci-static/resources/view/homeproxy/client.js:638
#: htdocs/luci-static/resources/view/homeproxy/client.js:949 #: htdocs/luci-static/resources/view/homeproxy/client.js:949
#: htdocs/luci-static/resources/view/homeproxy/client.js:1098 #: htdocs/luci-static/resources/view/homeproxy/client.js:1098
#: htdocs/luci-static/resources/view/homeproxy/server.js:857 #: htdocs/luci-static/resources/view/homeproxy/server.js:855
msgid "TCP" msgid "TCP"
msgstr "TCP" msgstr "TCP"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1160 #: htdocs/luci-static/resources/view/homeproxy/node.js:1155
#: htdocs/luci-static/resources/view/homeproxy/server.js:835 #: htdocs/luci-static/resources/view/homeproxy/server.js:833
msgid "TCP fast open" msgid "TCP fast open"
msgstr "TCP 快速打开" msgstr "TCP 快速打开"
@@ -2567,7 +2583,7 @@ msgid ""
"allowed to open." "allowed to open."
msgstr "允许对等点打开的 QUIC 并发双向流的最大数量。" msgstr "允许对等点打开的 QUIC 并发双向流的最大数量。"
#: htdocs/luci-static/resources/view/homeproxy/server.js:731 #: htdocs/luci-static/resources/view/homeproxy/server.js:730
msgid "The maximum time difference between the server and the client." msgid "The maximum time difference between the server and the client."
msgstr "服务器和客户端之间的最大时间差。" msgstr "服务器和客户端之间的最大时间差。"
@@ -2582,7 +2598,7 @@ msgid "The modern ImmortalWrt proxy platform for ARM64/AMD64."
msgstr "为 ARM64/AMD64 设计的现代 ImmortalWrt 代理平台。" msgstr "为 ARM64/AMD64 设计的现代 ImmortalWrt 代理平台。"
#: htdocs/luci-static/resources/view/homeproxy/client.js:454 #: htdocs/luci-static/resources/view/homeproxy/client.js:454
#: htdocs/luci-static/resources/view/homeproxy/server.js:865 #: htdocs/luci-static/resources/view/homeproxy/server.js:863
msgid "The network interface to bind to." msgid "The network interface to bind to."
msgstr "绑定到的网络接口。" msgstr "绑定到的网络接口。"
@@ -2590,7 +2606,7 @@ msgstr "绑定到的网络接口。"
msgid "The path of the DNS server." msgid "The path of the DNS server."
msgstr "DNS 服务器的路径。" msgstr "DNS 服务器的路径。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1100 #: htdocs/luci-static/resources/view/homeproxy/node.js:1095
msgid "" msgid ""
"The path to the ECH config, in PEM format. If empty, load from DNS will be " "The path to the ECH config, in PEM format. If empty, load from DNS will be "
"attempted." "attempted."
@@ -2612,11 +2628,11 @@ msgstr "DNS 服务器的端口。"
msgid "The response code." msgid "The response code."
msgstr "响应代码。" msgstr "响应代码。"
#: htdocs/luci-static/resources/view/homeproxy/server.js:768 #: htdocs/luci-static/resources/view/homeproxy/server.js:766
msgid "The server private key, in PEM format." msgid "The server private key, in PEM format."
msgstr "服务端私钥,需要 PEM 格式。" msgstr "服务端私钥,需要 PEM 格式。"
#: htdocs/luci-static/resources/view/homeproxy/server.js:749 #: htdocs/luci-static/resources/view/homeproxy/server.js:747
msgid "The server public key, in PEM format." msgid "The server public key, in PEM format."
msgstr "服务端公钥,需要 PEM 格式。" msgstr "服务端公钥,需要 PEM 格式。"
@@ -2649,7 +2665,7 @@ msgstr ""
"检测到任何活动,则会关闭连接。" "检测到任何活动,则会关闭连接。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1039 #: htdocs/luci-static/resources/view/homeproxy/node.js:1039
#: htdocs/luci-static/resources/view/homeproxy/node.js:1384 #: htdocs/luci-static/resources/view/homeproxy/node.js:1379
msgid "" msgid ""
"This is <strong>DANGEROUS</strong>, your traffic is almost like " "This is <strong>DANGEROUS</strong>, your traffic is almost like "
"<strong>PLAIN TEXT</strong>! Use at your own risk!" "<strong>PLAIN TEXT</strong>! Use at your own risk!"
@@ -2697,6 +2713,10 @@ msgid ""
msgstr "" msgstr ""
"要启用 Tun 支持,您需要安装 <code>ip-full</code> 和 <code>kmod-tun</code>。" "要启用 Tun 支持,您需要安装 <code>ip-full</code> 和 <code>kmod-tun</code>。"
#: htdocs/luci-static/resources/view/homeproxy/status.js:135
msgid "Trace"
msgstr "跟踪"
#: htdocs/luci-static/resources/view/homeproxy/node.js:769 #: htdocs/luci-static/resources/view/homeproxy/node.js:769
#: htdocs/luci-static/resources/view/homeproxy/server.js:409 #: htdocs/luci-static/resources/view/homeproxy/server.js:409
msgid "Transport" msgid "Transport"
@@ -2726,21 +2746,21 @@ msgstr "类型"
#: htdocs/luci-static/resources/view/homeproxy/client.js:639 #: htdocs/luci-static/resources/view/homeproxy/client.js:639
#: htdocs/luci-static/resources/view/homeproxy/client.js:948 #: htdocs/luci-static/resources/view/homeproxy/client.js:948
#: htdocs/luci-static/resources/view/homeproxy/client.js:1099 #: htdocs/luci-static/resources/view/homeproxy/client.js:1099
#: htdocs/luci-static/resources/view/homeproxy/server.js:858 #: htdocs/luci-static/resources/view/homeproxy/server.js:856
msgid "UDP" msgid "UDP"
msgstr "UDP" msgstr "UDP"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1166 #: htdocs/luci-static/resources/view/homeproxy/node.js:1161
#: htdocs/luci-static/resources/view/homeproxy/server.js:844 #: htdocs/luci-static/resources/view/homeproxy/server.js:842
msgid "UDP Fragment" msgid "UDP Fragment"
msgstr "UDP 分片" msgstr "UDP 分片"
#: htdocs/luci-static/resources/view/homeproxy/client.js:335 #: htdocs/luci-static/resources/view/homeproxy/client.js:335
#: htdocs/luci-static/resources/view/homeproxy/server.js:849 #: htdocs/luci-static/resources/view/homeproxy/server.js:847
msgid "UDP NAT expiration time" msgid "UDP NAT expiration time"
msgstr "UDP NAT 过期时间" msgstr "UDP NAT 过期时间"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1170 #: htdocs/luci-static/resources/view/homeproxy/node.js:1165
msgid "UDP over TCP" msgid "UDP over TCP"
msgstr "UDP over TCP" msgstr "UDP over TCP"
@@ -2781,15 +2801,15 @@ msgstr "UUID"
msgid "Unknown error." msgid "Unknown error."
msgstr "未知错误。" msgstr "未知错误。"
#: htdocs/luci-static/resources/view/homeproxy/status.js:148 #: htdocs/luci-static/resources/view/homeproxy/status.js:194
msgid "Unknown error: %s" msgid "Unknown error: %s"
msgstr "未知错误:%s" msgstr "未知错误:%s"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1137 #: htdocs/luci-static/resources/view/homeproxy/node.js:1131
msgid "Unsupported fingerprint!" msgid "Unsupported fingerprint!"
msgstr "不支持的指纹!" msgstr "不支持的指纹!"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1407 #: htdocs/luci-static/resources/view/homeproxy/node.js:1402
msgid "Update %s subscriptions" msgid "Update %s subscriptions"
msgstr "更新 %s 个订阅" msgstr "更新 %s 个订阅"
@@ -2805,23 +2825,23 @@ msgstr "更新间隔"
msgid "Update interval of rule set." msgid "Update interval of rule set."
msgstr "规则集更新间隔。" msgstr "规则集更新间隔。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1402 #: htdocs/luci-static/resources/view/homeproxy/node.js:1397
msgid "Update nodes from subscriptions" msgid "Update nodes from subscriptions"
msgstr "从订阅更新节点" msgstr "从订阅更新节点"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1345 #: htdocs/luci-static/resources/view/homeproxy/node.js:1340
msgid "Update subscriptions via proxy." msgid "Update subscriptions via proxy."
msgstr "使用代理更新订阅。" msgstr "使用代理更新订阅。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1338 #: htdocs/luci-static/resources/view/homeproxy/node.js:1333
msgid "Update time" msgid "Update time"
msgstr "更新时间" msgstr "更新时间"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1344 #: htdocs/luci-static/resources/view/homeproxy/node.js:1339
msgid "Update via proxy" msgid "Update via proxy"
msgstr "使用代理更新" msgstr "使用代理更新"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1105 #: htdocs/luci-static/resources/view/homeproxy/node.js:1100
msgid "Upload ECH config" msgid "Upload ECH config"
msgstr "上传 ECH 配置" msgstr "上传 ECH 配置"
@@ -2836,18 +2856,18 @@ msgid "Upload bandwidth in Mbps."
msgstr "上传带宽单位Mbps。" msgstr "上传带宽单位Mbps。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1081 #: htdocs/luci-static/resources/view/homeproxy/node.js:1081
#: htdocs/luci-static/resources/view/homeproxy/server.js:759 #: htdocs/luci-static/resources/view/homeproxy/server.js:757
msgid "Upload certificate" msgid "Upload certificate"
msgstr "上传证书" msgstr "上传证书"
#: htdocs/luci-static/resources/view/homeproxy/server.js:778 #: htdocs/luci-static/resources/view/homeproxy/server.js:776
msgid "Upload key" msgid "Upload key"
msgstr "上传密钥" msgstr "上传密钥"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1084 #: htdocs/luci-static/resources/view/homeproxy/node.js:1084
#: htdocs/luci-static/resources/view/homeproxy/node.js:1108 #: htdocs/luci-static/resources/view/homeproxy/node.js:1103
#: htdocs/luci-static/resources/view/homeproxy/server.js:762 #: htdocs/luci-static/resources/view/homeproxy/server.js:760
#: htdocs/luci-static/resources/view/homeproxy/server.js:781 #: htdocs/luci-static/resources/view/homeproxy/server.js:779
msgid "Upload..." msgid "Upload..."
msgstr "上传..." msgstr "上传..."
@@ -2871,7 +2891,7 @@ msgstr "用于验证返回证书上的主机名。"
msgid "User" msgid "User"
msgstr "用户" msgstr "用户"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1378 #: htdocs/luci-static/resources/view/homeproxy/node.js:1373
msgid "User-Agent" msgid "User-Agent"
msgstr "用户代理" msgstr "用户代理"
@@ -2899,12 +2919,16 @@ msgstr "WAN DNS从接口获取"
msgid "WAN IP Policy" msgid "WAN IP Policy"
msgstr "WAN IP 策略" msgstr "WAN IP 策略"
#: htdocs/luci-static/resources/view/homeproxy/status.js:138
msgid "Warn"
msgstr "警告"
#: htdocs/luci-static/resources/view/homeproxy/node.js:776 #: htdocs/luci-static/resources/view/homeproxy/node.js:776
#: htdocs/luci-static/resources/view/homeproxy/server.js:416 #: htdocs/luci-static/resources/view/homeproxy/server.js:416
msgid "WebSocket" msgid "WebSocket"
msgstr "WebSocket" msgstr "WebSocket"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1369 #: htdocs/luci-static/resources/view/homeproxy/node.js:1364
msgid "Whitelist mode" msgid "Whitelist mode"
msgstr "白名单模式" msgstr "白名单模式"
@@ -2929,7 +2953,7 @@ msgid "Write proxy protocol in the connection header."
msgstr "在连接头中写入代理协议。" msgstr "在连接头中写入代理协议。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:885 #: htdocs/luci-static/resources/view/homeproxy/node.js:885
#: htdocs/luci-static/resources/view/homeproxy/node.js:1391 #: htdocs/luci-static/resources/view/homeproxy/node.js:1386
msgid "Xudp (Xray-core)" msgid "Xudp (Xray-core)"
msgstr "Xudp (Xray-core)" msgstr "Xudp (Xray-core)"
@@ -2942,7 +2966,7 @@ msgid "ZeroSSL"
msgstr "ZeroSSL" msgstr "ZeroSSL"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1086 #: htdocs/luci-static/resources/view/homeproxy/node.js:1086
#: htdocs/luci-static/resources/view/homeproxy/server.js:764 #: htdocs/luci-static/resources/view/homeproxy/server.js:762
msgid "certificate" msgid "certificate"
msgstr "证书" msgstr "证书"
@@ -2986,19 +3010,19 @@ msgstr "gVisor"
#: htdocs/luci-static/resources/view/homeproxy/client.js:504 #: htdocs/luci-static/resources/view/homeproxy/client.js:504
#: htdocs/luci-static/resources/view/homeproxy/client.js:1355 #: htdocs/luci-static/resources/view/homeproxy/client.js:1355
#: htdocs/luci-static/resources/view/homeproxy/node.js:487 #: htdocs/luci-static/resources/view/homeproxy/node.js:487
#: htdocs/luci-static/resources/view/homeproxy/node.js:1133 #: htdocs/luci-static/resources/view/homeproxy/node.js:1127
#: htdocs/luci-static/resources/view/homeproxy/server.js:226 #: htdocs/luci-static/resources/view/homeproxy/server.js:226
msgid "non-empty value" msgid "non-empty value"
msgstr "非空值" msgstr "非空值"
#: htdocs/luci-static/resources/view/homeproxy/node.js:628 #: htdocs/luci-static/resources/view/homeproxy/node.js:628
#: htdocs/luci-static/resources/view/homeproxy/node.js:883 #: htdocs/luci-static/resources/view/homeproxy/node.js:883
#: htdocs/luci-static/resources/view/homeproxy/node.js:1389 #: htdocs/luci-static/resources/view/homeproxy/node.js:1384
msgid "none" msgid "none"
msgstr "无" msgstr "无"
#: htdocs/luci-static/resources/view/homeproxy/node.js:884 #: htdocs/luci-static/resources/view/homeproxy/node.js:884
#: htdocs/luci-static/resources/view/homeproxy/node.js:1390 #: htdocs/luci-static/resources/view/homeproxy/node.js:1385
msgid "packet addr (v2ray-core v5+)" msgid "packet addr (v2ray-core v5+)"
msgstr "packet addr (v2ray-core v5+)" msgstr "packet addr (v2ray-core v5+)"
@@ -3006,7 +3030,7 @@ msgstr "packet addr (v2ray-core v5+)"
msgid "passed" msgid "passed"
msgstr "通过" msgstr "通过"
#: htdocs/luci-static/resources/view/homeproxy/server.js:783 #: htdocs/luci-static/resources/view/homeproxy/server.js:781
msgid "private key" msgid "private key"
msgstr "私钥" msgstr "私钥"
@@ -3014,19 +3038,19 @@ msgstr "私钥"
msgid "quic-go / uquic chrome" msgid "quic-go / uquic chrome"
msgstr "quic-go / uquic chrome" msgstr "quic-go / uquic chrome"
#: htdocs/luci-static/resources/view/homeproxy/status.js:237 #: htdocs/luci-static/resources/view/homeproxy/status.js:283
msgid "sing-box client" msgid "sing-box client"
msgstr "sing-box 客户端" msgstr "sing-box 客户端"
#: htdocs/luci-static/resources/view/homeproxy/status.js:240 #: htdocs/luci-static/resources/view/homeproxy/status.js:286
msgid "sing-box server" msgid "sing-box server"
msgstr "sing-box 服务端" msgstr "sing-box 服务端"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1115 #: htdocs/luci-static/resources/view/homeproxy/node.js:1109
msgid "uTLS fingerprint" msgid "uTLS fingerprint"
msgstr "uTLS 指纹" msgstr "uTLS 指纹"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1116 #: htdocs/luci-static/resources/view/homeproxy/node.js:1110
msgid "" msgid ""
"uTLS is a fork of \"crypto/tls\", which provides ClientHello fingerprinting " "uTLS is a fork of \"crypto/tls\", which provides ClientHello fingerprinting "
"resistance." "resistance."
@@ -3038,7 +3062,7 @@ msgid "unchecked"
msgstr "未检查" msgstr "未检查"
#: htdocs/luci-static/resources/homeproxy.js:237 #: htdocs/luci-static/resources/homeproxy.js:237
#: htdocs/luci-static/resources/view/homeproxy/node.js:1301 #: htdocs/luci-static/resources/view/homeproxy/node.js:1296
msgid "unique UCI identifier" msgid "unique UCI identifier"
msgstr "独立 UCI 标识" msgstr "独立 UCI 标识"
@@ -3048,13 +3072,13 @@ msgstr "独立值"
#: htdocs/luci-static/resources/view/homeproxy/node.js:499 #: htdocs/luci-static/resources/view/homeproxy/node.js:499
#: htdocs/luci-static/resources/view/homeproxy/node.js:642 #: htdocs/luci-static/resources/view/homeproxy/node.js:642
#: htdocs/luci-static/resources/view/homeproxy/node.js:1177 #: htdocs/luci-static/resources/view/homeproxy/node.js:1172
msgid "v1" msgid "v1"
msgstr "v1" msgstr "v1"
#: htdocs/luci-static/resources/view/homeproxy/node.js:500 #: htdocs/luci-static/resources/view/homeproxy/node.js:500
#: htdocs/luci-static/resources/view/homeproxy/node.js:643 #: htdocs/luci-static/resources/view/homeproxy/node.js:643
#: htdocs/luci-static/resources/view/homeproxy/node.js:1178 #: htdocs/luci-static/resources/view/homeproxy/node.js:1173
msgid "v2" msgid "v2"
msgstr "v2" msgstr "v2"
@@ -3073,8 +3097,8 @@ msgstr "有效 DNS 服务器地址"
#: htdocs/luci-static/resources/view/homeproxy/client.js:521 #: htdocs/luci-static/resources/view/homeproxy/client.js:521
#: htdocs/luci-static/resources/view/homeproxy/client.js:1360 #: htdocs/luci-static/resources/view/homeproxy/client.js:1360
#: htdocs/luci-static/resources/view/homeproxy/client.js:1363 #: htdocs/luci-static/resources/view/homeproxy/client.js:1363
#: htdocs/luci-static/resources/view/homeproxy/node.js:1355 #: htdocs/luci-static/resources/view/homeproxy/node.js:1350
#: htdocs/luci-static/resources/view/homeproxy/node.js:1358 #: htdocs/luci-static/resources/view/homeproxy/node.js:1353
msgid "valid URL" msgid "valid URL"
msgstr "有效网址" msgstr "有效网址"

View File

@@ -31,6 +31,7 @@ config homeproxy 'config'
option proxy_mode 'redirect_tproxy' option proxy_mode 'redirect_tproxy'
option ipv6_support '1' option ipv6_support '1'
option github_token '' option github_token ''
option log_level 'warn'
config homeproxy 'control' config homeproxy 'control'
option lan_proxy_mode 'disabled' option lan_proxy_mode 'disabled'
@@ -53,6 +54,7 @@ config homeproxy 'control'
config homeproxy 'routing' config homeproxy 'routing'
option sniff_override '1' option sniff_override '1'
option default_outbound 'direct-out' option default_outbound 'direct-out'
option default_outbound_dns 'default-dns'
config homeproxy 'dns' config homeproxy 'dns'
option dns_strategy 'prefer_ipv4' option dns_strategy 'prefer_ipv4'
@@ -71,11 +73,5 @@ config homeproxy 'subscription'
config homeproxy 'server' config homeproxy 'server'
option enabled '0' option enabled '0'
option log_level 'warn'
config dns_rule 'nodes_domain'
option label 'NodesDomain'
option enabled '1'
option mode 'default'
list outbound 'any-out'
option server 'default-dns'

View File

@@ -13,6 +13,7 @@
1.116.0.0/15 1.116.0.0/15
1.118.2.0/24 1.118.2.0/24
1.118.32.0/22 1.118.32.0/22
1.118.36.0/24
1.119.0.0/17 1.119.0.0/17
1.119.128.0/18 1.119.128.0/18
1.119.192.0/20 1.119.192.0/20
@@ -126,6 +127,7 @@
36.255.128.0/22 36.255.128.0/22
36.255.164.0/24 36.255.164.0/24
36.255.192.0/24 36.255.192.0/24
38.84.220.0/24
38.111.220.0/23 38.111.220.0/23
39.64.0.0/11 39.64.0.0/11
39.96.0.0/13 39.96.0.0/13
@@ -198,7 +200,8 @@
42.240.8.0/22 42.240.8.0/22
42.240.12.0/24 42.240.12.0/24
42.240.16.0/24 42.240.16.0/24
42.240.20.0/24 42.240.20.0/23
42.240.22.0/24
42.240.128.0/17 42.240.128.0/17
42.242.0.0/15 42.242.0.0/15
42.244.0.0/14 42.244.0.0/14
@@ -209,10 +212,13 @@
43.102.152.0/22 43.102.152.0/22
43.136.0.0/13 43.136.0.0/13
43.144.0.0/15 43.144.0.0/15
43.176.0.0/14
43.180.0.0/16
43.192.0.0/16 43.192.0.0/16
43.193.0.0/18 43.193.0.0/18
43.193.64.0/24 43.193.64.0/24
43.194.0.0/20 43.194.0.0/20
43.194.16.0/24
43.195.0.0/20 43.195.0.0/20
43.196.0.0/16 43.196.0.0/16
43.224.12.0/22 43.224.12.0/22
@@ -404,8 +410,7 @@
45.116.152.0/22 45.116.152.0/22
45.116.208.0/22 45.116.208.0/22
45.117.8.0/22 45.117.8.0/22
45.117.68.0/24 45.117.68.0/22
45.117.70.0/23
45.119.60.0/22 45.119.60.0/22
45.119.68.0/22 45.119.68.0/22
45.119.105.0/24 45.119.105.0/24
@@ -444,7 +449,7 @@
45.249.212.0/22 45.249.212.0/22
45.250.32.0/21 45.250.32.0/21
45.250.40.0/22 45.250.40.0/22
45.250.152.0/24 45.250.152.0/23
45.250.180.0/23 45.250.180.0/23
45.250.184.0/22 45.250.184.0/22
45.250.188.0/24 45.250.188.0/24
@@ -513,7 +518,6 @@
49.4.124.0/23 49.4.124.0/23
49.4.126.0/24 49.4.126.0/24
49.4.128.0/22 49.4.128.0/22
49.5.13.0/24
49.7.0.0/16 49.7.0.0/16
49.52.0.0/14 49.52.0.0/14
49.64.0.0/11 49.64.0.0/11
@@ -557,7 +561,7 @@
54.222.48.0/21 54.222.48.0/21
54.222.57.0/24 54.222.57.0/24
54.222.60.0/22 54.222.60.0/22
54.222.64.0/24 54.222.64.0/23
54.222.70.0/23 54.222.70.0/23
54.222.72.0/21 54.222.72.0/21
54.222.80.0/21 54.222.80.0/21
@@ -842,6 +846,7 @@
101.132.0.0/15 101.132.0.0/15
101.197.0.0/16 101.197.0.0/16
101.198.0.0/22 101.198.0.0/22
101.198.4.0/24
101.198.160.0/19 101.198.160.0/19
101.198.192.0/19 101.198.192.0/19
101.199.48.0/20 101.199.48.0/20
@@ -882,6 +887,8 @@
101.251.0.0/22 101.251.0.0/22
101.251.80.0/20 101.251.80.0/20
101.251.128.0/19 101.251.128.0/19
101.251.160.0/20
101.251.176.0/22
101.251.192.0/18 101.251.192.0/18
101.254.0.0/20 101.254.0.0/20
101.254.32.0/19 101.254.32.0/19
@@ -1101,6 +1108,7 @@
103.73.204.0/22 103.73.204.0/22
103.74.24.0/21 103.74.24.0/21
103.74.48.0/22 103.74.48.0/22
103.74.80.0/22
103.75.107.0/24 103.75.107.0/24
103.75.152.0/22 103.75.152.0/22
103.76.60.0/22 103.76.60.0/22
@@ -1116,6 +1124,7 @@
103.79.24.0/22 103.79.24.0/22
103.79.120.0/22 103.79.120.0/22
103.79.200.0/22 103.79.200.0/22
103.79.228.0/24
103.81.4.0/22 103.81.4.0/22
103.81.48.0/22 103.81.48.0/22
103.81.72.0/22 103.81.72.0/22
@@ -1182,7 +1191,6 @@
103.102.214.0/24 103.102.214.0/24
103.103.12.0/24 103.103.12.0/24
103.103.36.0/24 103.103.36.0/24
103.103.200.0/22
103.104.252.0/22 103.104.252.0/22
103.105.0.0/22 103.105.0.0/22
103.105.12.0/22 103.105.12.0/22
@@ -1235,7 +1243,7 @@
103.123.4.0/23 103.123.4.0/23
103.125.236.0/22 103.125.236.0/22
103.126.1.0/24 103.126.1.0/24
103.126.18.0/23 103.126.19.0/24
103.126.101.0/24 103.126.101.0/24
103.126.102.0/23 103.126.102.0/23
103.126.124.0/22 103.126.124.0/22
@@ -1315,7 +1323,6 @@
103.177.44.0/24 103.177.44.0/24
103.179.78.0/23 103.179.78.0/23
103.180.108.0/23 103.180.108.0/23
103.181.164.0/23
103.181.234.0/24 103.181.234.0/24
103.183.66.0/23 103.183.66.0/23
103.183.122.0/23 103.183.122.0/23
@@ -1392,6 +1399,7 @@
103.215.36.0/22 103.215.36.0/22
103.215.44.0/24 103.215.44.0/24
103.215.140.0/22 103.215.140.0/22
103.216.136.0/22
103.216.152.0/22 103.216.152.0/22
103.216.252.0/23 103.216.252.0/23
103.217.184.0/21 103.217.184.0/21
@@ -1425,6 +1433,7 @@
103.227.80.0/22 103.227.80.0/22
103.227.120.0/22 103.227.120.0/22
103.227.136.0/22 103.227.136.0/22
103.227.228.0/22
103.228.12.0/22 103.228.12.0/22
103.228.136.0/22 103.228.136.0/22
103.228.160.0/22 103.228.160.0/22
@@ -1713,7 +1722,6 @@
110.236.0.0/15 110.236.0.0/15
110.240.0.0/12 110.240.0.0/12
111.0.0.0/10 111.0.0.0/10
111.67.192.0/20
111.72.0.0/13 111.72.0.0/13
111.85.0.0/16 111.85.0.0/16
111.112.0.0/14 111.112.0.0/14
@@ -1787,11 +1795,7 @@
113.45.112.0/22 113.45.112.0/22
113.45.120.0/22 113.45.120.0/22
113.45.128.0/17 113.45.128.0/17
113.46.0.0/17 113.46.0.0/16
113.46.128.0/18
113.46.192.0/19
113.46.224.0/20
113.46.240.0/21
113.47.0.0/18 113.47.0.0/18
113.47.64.0/19 113.47.64.0/19
113.47.96.0/21 113.47.96.0/21
@@ -1900,7 +1904,11 @@
114.112.200.0/21 114.112.200.0/21
114.112.208.0/20 114.112.208.0/20
114.113.63.0/24 114.113.63.0/24
114.113.64.0/18 114.113.64.0/20
114.113.80.0/22
114.113.84.0/24
114.113.88.0/21
114.113.96.0/19
114.113.144.0/20 114.113.144.0/20
114.113.196.0/22 114.113.196.0/22
114.113.200.0/24 114.113.200.0/24
@@ -1961,10 +1969,10 @@
115.175.224.0/20 115.175.224.0/20
115.182.0.0/15 115.182.0.0/15
115.190.0.0/17 115.190.0.0/17
115.190.128.0/19 115.190.128.0/18
115.190.192.0/20
115.192.0.0/11 115.192.0.0/11
115.224.0.0/12 115.224.0.0/12
116.0.81.0/24
116.0.89.0/24 116.0.89.0/24
116.1.0.0/16 116.1.0.0/16
116.2.0.0/15 116.2.0.0/15
@@ -2253,8 +2261,8 @@
118.194.128.0/21 118.194.128.0/21
118.194.240.0/21 118.194.240.0/21
118.195.128.0/17 118.195.128.0/17
118.196.0.0/19 118.196.0.0/18
118.196.32.0/20 118.196.64.0/19
118.199.0.0/16 118.199.0.0/16
118.202.0.0/15 118.202.0.0/15
118.212.0.0/15 118.212.0.0/15
@@ -2704,7 +2712,6 @@
124.64.0.0/15 124.64.0.0/15
124.66.0.0/17 124.66.0.0/17
124.67.0.0/16 124.67.0.0/16
124.68.252.0/23
124.70.0.0/16 124.70.0.0/16
124.71.0.0/17 124.71.0.0/17
124.71.128.0/18 124.71.128.0/18
@@ -2773,7 +2780,10 @@
125.112.0.0/12 125.112.0.0/12
125.171.0.0/16 125.171.0.0/16
125.208.0.0/19 125.208.0.0/19
125.208.32.0/20 125.208.32.0/21
125.208.40.0/22
125.208.44.0/23
125.208.46.0/24
125.208.49.0/24 125.208.49.0/24
125.210.0.0/15 125.210.0.0/15
125.213.32.0/20 125.213.32.0/20
@@ -2953,6 +2963,10 @@
154.72.44.0/24 154.72.44.0/24
154.72.47.0/24 154.72.47.0/24
154.89.32.0/20 154.89.32.0/20
154.89.49.0/24
154.89.50.0/23
154.89.52.0/22
154.89.56.0/21
154.91.158.0/23 154.91.158.0/23
154.208.140.0/22 154.208.140.0/22
154.208.144.0/20 154.208.144.0/20
@@ -2972,7 +2986,9 @@
155.102.26.0/23 155.102.26.0/23
155.102.28.0/22 155.102.28.0/22
155.102.32.0/19 155.102.32.0/19
155.102.64.0/23
155.102.72.0/24 155.102.72.0/24
155.102.98.0/23
155.102.111.0/24 155.102.111.0/24
155.102.112.0/21 155.102.112.0/21
155.102.120.0/23 155.102.120.0/23
@@ -2986,8 +3002,12 @@
155.102.164.0/23 155.102.164.0/23
155.102.166.0/24 155.102.166.0/24
155.102.168.0/23 155.102.168.0/23
155.102.171.0/24
155.102.174.0/23
155.102.176.0/23 155.102.176.0/23
155.102.178.0/24
155.102.180.0/22 155.102.180.0/22
155.102.184.0/21
155.102.193.0/24 155.102.193.0/24
155.102.194.0/23 155.102.194.0/23
155.102.196.0/23 155.102.196.0/23
@@ -2995,16 +3015,12 @@
155.102.200.0/23 155.102.200.0/23
155.102.202.0/24 155.102.202.0/24
155.102.204.0/23 155.102.204.0/23
155.102.207.0/24
155.102.208.0/23 155.102.208.0/23
155.102.211.0/24 155.102.211.0/24
155.102.216.0/22 155.102.216.0/22
155.102.220.0/23 155.102.220.0/23
155.102.224.0/20 155.102.224.0/19
155.102.240.0/23
155.102.242.0/24
155.102.247.0/24
155.102.248.0/23
155.102.253.0/24
155.126.176.0/23 155.126.176.0/23
156.59.108.0/24 156.59.108.0/24
156.59.202.0/23 156.59.202.0/23
@@ -3023,7 +3039,6 @@
157.0.0.0/16 157.0.0.0/16
157.10.34.0/24 157.10.34.0/24
157.10.105.0/24 157.10.105.0/24
157.15.74.0/23
157.15.94.0/23 157.15.94.0/23
157.15.104.0/23 157.15.104.0/23
157.18.0.0/16 157.18.0.0/16
@@ -3088,10 +3103,10 @@
163.181.40.0/24 163.181.40.0/24
163.181.42.0/23 163.181.42.0/23
163.181.44.0/22 163.181.44.0/22
163.181.48.0/23 163.181.48.0/22
163.181.50.0/24
163.181.52.0/24 163.181.52.0/24
163.181.56.0/22 163.181.56.0/23
163.181.58.0/24
163.181.60.0/23 163.181.60.0/23
163.181.66.0/23 163.181.66.0/23
163.181.69.0/24 163.181.69.0/24
@@ -3137,9 +3152,9 @@
163.181.192.0/23 163.181.192.0/23
163.181.196.0/22 163.181.196.0/22
163.181.200.0/21 163.181.200.0/21
163.181.209.0/24
163.181.210.0/23 163.181.210.0/23
163.181.212.0/22 163.181.213.0/24
163.181.214.0/23
163.181.216.0/21 163.181.216.0/21
163.181.224.0/23 163.181.224.0/23
163.181.228.0/22 163.181.228.0/22
@@ -3157,10 +3172,6 @@
166.111.0.0/16 166.111.0.0/16
167.139.0.0/16 167.139.0.0/16
167.220.244.0/22 167.220.244.0/22
168.159.144.0/21
168.159.152.0/22
168.159.156.0/23
168.159.158.0/24
168.160.0.0/17 168.160.0.0/17
168.160.152.0/24 168.160.152.0/24
168.160.158.0/23 168.160.158.0/23
@@ -3183,6 +3194,8 @@
175.16.0.0/13 175.16.0.0/13
175.24.0.0/15 175.24.0.0/15
175.27.0.0/16 175.27.0.0/16
175.29.107.0/24
175.29.108.0/22
175.30.0.0/15 175.30.0.0/15
175.42.0.0/15 175.42.0.0/15
175.44.0.0/16 175.44.0.0/16
@@ -3263,6 +3276,7 @@
180.233.0.0/18 180.233.0.0/18
180.235.64.0/21 180.235.64.0/21
180.235.72.0/23 180.235.72.0/23
181.233.128.0/22
182.18.5.0/24 182.18.5.0/24
182.18.32.0/19 182.18.32.0/19
182.18.72.0/21 182.18.72.0/21
@@ -3282,7 +3296,7 @@
182.61.128.0/19 182.61.128.0/19
182.61.192.0/22 182.61.192.0/22
182.61.200.0/21 182.61.200.0/21
182.61.216.0/21 182.61.208.0/20
182.61.224.0/19 182.61.224.0/19
182.80.0.0/13 182.80.0.0/13
182.88.0.0/14 182.88.0.0/14
@@ -3321,8 +3335,6 @@
185.234.212.0/24 185.234.212.0/24
188.131.128.0/17 188.131.128.0/17
192.55.46.0/24 192.55.46.0/24
192.55.68.0/22
192.102.204.0/23
192.140.160.0/19 192.140.160.0/19
192.140.208.0/21 192.140.208.0/21
192.144.128.0/17 192.144.128.0/17
@@ -3335,8 +3347,10 @@
193.119.10.0/23 193.119.10.0/23
193.119.12.0/23 193.119.12.0/23
193.119.15.0/24 193.119.15.0/24
193.119.17.0/24
193.119.19.0/24 193.119.19.0/24
193.119.20.0/23 193.119.20.0/23
193.119.22.0/24
193.119.25.0/24 193.119.25.0/24
193.119.28.0/24 193.119.28.0/24
193.119.30.0/24 193.119.30.0/24
@@ -3347,7 +3361,6 @@
194.138.202.0/23 194.138.202.0/23
194.138.245.0/24 194.138.245.0/24
195.114.203.0/24 195.114.203.0/24
198.175.100.0/22
198.208.17.0/24 198.208.17.0/24
198.208.19.0/24 198.208.19.0/24
198.208.30.0/24 198.208.30.0/24
@@ -3658,6 +3671,7 @@
203.86.60.0/23 203.86.60.0/23
203.86.62.0/24 203.86.62.0/24
203.86.64.0/19 203.86.64.0/19
203.86.112.0/24
203.86.116.0/24 203.86.116.0/24
203.86.254.0/23 203.86.254.0/23
203.88.32.0/19 203.88.32.0/19
@@ -4088,6 +4102,7 @@
211.167.176.0/20 211.167.176.0/20
211.167.224.0/19 211.167.224.0/19
212.64.0.0/17 212.64.0.0/17
212.100.186.0/24
212.129.128.0/17 212.129.128.0/17
218.0.0.0/11 218.0.0.0/11
218.56.0.0/13 218.56.0.0/13
@@ -4342,7 +4357,10 @@
222.126.128.0/22 222.126.128.0/22
222.126.132.0/23 222.126.132.0/23
222.126.140.0/22 222.126.140.0/22
222.126.144.0/20 222.126.144.0/24
222.126.146.0/23
222.126.148.0/22
222.126.152.0/21
222.126.160.0/20 222.126.160.0/20
222.126.176.0/21 222.126.176.0/21
222.126.184.0/22 222.126.184.0/22

View File

@@ -1 +1 @@
20250822033443 20250912032015

View File

@@ -40,6 +40,7 @@
2400:7fc0:2a0::/44 2400:7fc0:2a0::/44
2400:7fc0:2c0::/44 2400:7fc0:2c0::/44
2400:7fc0:4000::/40 2400:7fc0:4000::/40
2400:7fc0:4100::/48
2400:7fc0:6000::/40 2400:7fc0:6000::/40
2400:7fc0:8000::/36 2400:7fc0:8000::/36
2400:7fc0:a000::/36 2400:7fc0:a000::/36
@@ -131,6 +132,7 @@
2401:3480:3000::/36 2401:3480:3000::/36
2401:34a0::/31 2401:34a0::/31
2401:3800::/32 2401:3800::/32
2401:5560:1000::/48
2401:5c20:10::/48 2401:5c20:10::/48
2401:70e0::/32 2401:70e0::/32
2401:71c0::/48 2401:71c0::/48
@@ -171,9 +173,8 @@
2401:f860:86::/47 2401:f860:86::/47
2401:f860:88::/47 2401:f860:88::/47
2401:f860:90::/46 2401:f860:90::/46
2401:f860:94::/48 2401:f860:94::/47
2401:f860:100::/40 2401:f860:100::/40
2401:f860:f100::/40
2401:fa00:40::/43 2401:fa00:40::/43
2402:840:d000::/46 2402:840:d000::/46
2402:840:e000::/46 2402:840:e000::/46
@@ -194,9 +195,7 @@
2402:6f40:2::/48 2402:6f40:2::/48
2402:7d80::/48 2402:7d80::/48
2402:7d80:240::/47 2402:7d80:240::/47
2402:7d80:6666::/48
2402:7d80:8888::/48 2402:7d80:8888::/48
2402:7d80:9999::/48
2402:8bc0::/32 2402:8bc0::/32
2402:8cc0::/40 2402:8cc0::/40
2402:8cc0:200::/40 2402:8cc0:200::/40
@@ -341,6 +340,8 @@
2404:2280:1f0::/45 2404:2280:1f0::/45
2404:2280:1f8::/46 2404:2280:1f8::/46
2404:2280:1fd::/48 2404:2280:1fd::/48
2404:2280:1fe::/48
2404:2280:201::/48
2404:2280:202::/47 2404:2280:202::/47
2404:2280:204::/46 2404:2280:204::/46
2404:2280:208::/46 2404:2280:208::/46
@@ -349,18 +350,22 @@
2404:2280:210::/46 2404:2280:210::/46
2404:2280:214::/48 2404:2280:214::/48
2404:2280:216::/47 2404:2280:216::/47
2404:2280:218::/48 2404:2280:218::/46
2404:2280:21a::/48
2404:2280:21d::/48 2404:2280:21d::/48
2404:2280:221::/48 2404:2280:221::/48
2404:2280:259::/48
2404:2280:25a::/47
2404:2280:25c::/48
2404:2280:265::/48 2404:2280:265::/48
2404:2280:266::/47 2404:2280:266::/47
2404:2280:268::/46 2404:2280:268::/45
2404:2280:26c::/48 2404:2280:270::/47
2404:2280:271::/48
2404:2280:272::/48 2404:2280:272::/48
2404:2280:274::/48
2404:2280:27a::/48 2404:2280:27a::/48
2404:2280:27c::/47 2404:2280:27c::/47
2404:2280:27f::/48
2404:2280:282::/48
2404:3700::/48 2404:3700::/48
2404:4dc0::/32 2404:4dc0::/32
2404:6380::/48 2404:6380::/48
@@ -391,6 +396,7 @@
2404:c2c0:2c0::/44 2404:c2c0:2c0::/44
2404:c2c0:501::/48 2404:c2c0:501::/48
2404:c2c0:4000::/40 2404:c2c0:4000::/40
2404:c2c0:4100::/48
2404:c2c0:6000::/40 2404:c2c0:6000::/40
2404:c2c0:8000::/36 2404:c2c0:8000::/36
2404:c2c0:bb00::/40 2404:c2c0:bb00::/40
@@ -447,9 +453,9 @@
2406:840:e230::/44 2406:840:e230::/44
2406:840:e260::/48 2406:840:e260::/48
2406:840:e2cf::/48 2406:840:e2cf::/48
2406:840:e500::/47
2406:840:e621::/48 2406:840:e621::/48
2406:840:e666::/47 2406:840:e666::/47
2406:840:e720::/44
2406:840:e80f::/48 2406:840:e80f::/48
2406:840:eb00::/46 2406:840:eb00::/46
2406:840:eb04::/47 2406:840:eb04::/47
@@ -595,6 +601,7 @@
2408:8181:a000::/40 2408:8181:a000::/40
2408:8181:a220::/44 2408:8181:a220::/44
2408:8181:e000::/40 2408:8181:e000::/40
2408:8182:6000::/40
2408:8182:c000::/40 2408:8182:c000::/40
2408:8183:4000::/40 2408:8183:4000::/40
2408:8183:8000::/40 2408:8183:8000::/40
@@ -737,7 +744,6 @@
2408:8406:b4c0::/42 2408:8406:b4c0::/42
2408:8406:b500::/41 2408:8406:b500::/41
2408:8406:b580::/42 2408:8406:b580::/42
2408:8407:500::/43
2408:8409::/40 2408:8409::/40
2408:8409:100::/41 2408:8409:100::/41
2408:8409:180::/42 2408:8409:180::/42
@@ -1187,6 +1193,8 @@
240e::/20 240e::/20
2602:2e0:ff::/48 2602:2e0:ff::/48
2602:f7ee:ee::/48 2602:f7ee:ee::/48
2602:f92a:a478::/48
2602:f92a:dead::/48
2602:f92a:e100::/44 2602:f92a:e100::/44
2602:f93b:400::/38 2602:f93b:400::/38
2602:f9ba:a8::/48 2602:f9ba:a8::/48
@@ -1242,28 +1250,25 @@
2a06:3603::/32 2a06:3603::/32
2a06:3604::/30 2a06:3604::/30
2a06:9f81:4600::/43 2a06:9f81:4600::/43
2a06:9f81:4640::/44
2a06:a005:260::/43 2a06:a005:260::/43
2a06:a005:280::/43 2a06:a005:280::/43
2a06:a005:2a0::/44 2a06:a005:2a0::/44
2a06:a005:8d0::/44 2a06:a005:8d0::/44
2a06:a005:9c0::/48
2a06:a005:9e0::/44
2a06:a005:a13::/48 2a06:a005:a13::/48
2a06:a005:e9a::/48
2a06:a005:1c40::/44 2a06:a005:1c40::/44
2a09:b280:ff81::/48 2a09:b280:ff81::/48
2a09:b280:ff83::/48 2a09:b280:ff83::/48
2a09:b280:ff84::/47 2a09:b280:ff84::/47
2a0a:2840::/30 2a0a:2840:20::/43
2a0a:2840:2000::/47
2a0a:2842::/32
2a0a:2845:aab8::/46 2a0a:2845:aab8::/46
2a0a:2845:d647::/48
2a0a:2846::/48 2a0a:2846::/48
2a0a:6040:ec00::/40 2a0a:6040:ec00::/40
2a0a:6044:6600::/40 2a0a:6044:6600::/40
2a0b:b87:ffb5::/48
2a0b:2542::/48 2a0b:2542::/48
2a0b:4340:a6::/48 2a0b:4340:a6::/48
2a0b:4b81:1001::/48
2a0b:4e07:b8::/47 2a0b:4e07:b8::/47
2a0c:9a40:84e0::/48 2a0c:9a40:84e0::/48
2a0c:9a40:9e00::/43 2a0c:9a40:9e00::/43
@@ -1285,10 +1290,9 @@
2a0e:aa07:e044::/48 2a0e:aa07:e044::/48
2a0e:aa07:e151::/48 2a0e:aa07:e151::/48
2a0e:aa07:e155::/48 2a0e:aa07:e155::/48
2a0e:aa07:e160::/47
2a0e:aa07:e162::/48
2a0e:aa07:e16a::/48 2a0e:aa07:e16a::/48
2a0e:aa07:e1a0::/44 2a0e:aa07:e1a0::/44
2a0e:aa07:e1e1::/48
2a0e:aa07:e1e2::/47 2a0e:aa07:e1e2::/47
2a0e:aa07:e1e4::/47 2a0e:aa07:e1e4::/47
2a0e:aa07:e1e6::/48 2a0e:aa07:e1e6::/48
@@ -1324,19 +1328,17 @@
2a0f:7d07::/32 2a0f:7d07::/32
2a0f:85c1:ba5::/48 2a0f:85c1:ba5::/48
2a0f:85c1:ca0::/44 2a0f:85c1:ca0::/44
2a0f:85c1:cf1::/48
2a0f:9400:6110::/48 2a0f:9400:6110::/48
2a0f:9400:7700::/48 2a0f:9400:7700::/48
2a0f:ac00::/29 2a0f:ac00::/29
2a10:2f00:15a::/48 2a10:2f00:15a::/48
2a10:cc40:190::/48
2a10:ccc0:d00::/46 2a10:ccc0:d00::/46
2a10:ccc0:d0a::/47 2a10:ccc0:d0a::/47
2a10:ccc0:d0c::/47 2a10:ccc0:d0c::/47
2a10:ccc6:66c4::/48
2a10:ccc6:66c6::/48 2a10:ccc6:66c6::/48
2a10:ccc6:66c9::/48 2a10:ccc6:66c8::/47
2a10:ccc6:66ca::/48 2a10:ccc6:66ca::/48
2a10:ccc6:66cc::/47
2a12:f8c3::/36 2a12:f8c3::/36
2a13:1800::/48 2a13:1800::/48
2a13:1800:10::/48 2a13:1800:10::/48
@@ -1351,7 +1353,6 @@
2a13:a5c7:2102::/48 2a13:a5c7:2102::/48
2a13:a5c7:2121::/48 2a13:a5c7:2121::/48
2a13:a5c7:2801::/48 2a13:a5c7:2801::/48
2a13:a5c7:2803::/48
2a13:a5c7:3108::/48 2a13:a5c7:3108::/48
2a13:a5c7:31a0::/43 2a13:a5c7:31a0::/43
2a13:aac4:f000::/44 2a13:aac4:f000::/44
@@ -1372,9 +1373,7 @@
2a14:67c1:a040::/47 2a14:67c1:a040::/47
2a14:67c1:a061::/48 2a14:67c1:a061::/48
2a14:67c1:a064::/48 2a14:67c1:a064::/48
2a14:67c1:a090::/46 2a14:67c1:a090::/45
2a14:67c1:a095::/48
2a14:67c1:a096::/48
2a14:67c1:a099::/48 2a14:67c1:a099::/48
2a14:67c1:a100::/43 2a14:67c1:a100::/43
2a14:67c1:b000::/48 2a14:67c1:b000::/48
@@ -1384,15 +1383,15 @@
2a14:67c1:b100::/46 2a14:67c1:b100::/46
2a14:67c1:b105::/48 2a14:67c1:b105::/48
2a14:67c1:b107::/48 2a14:67c1:b107::/48
2a14:67c1:b130::/48 2a14:67c1:b130::/46
2a14:67c1:b132::/47
2a14:67c1:b134::/47 2a14:67c1:b134::/47
2a14:67c1:b140::/48
2a14:67c1:b4a1::/48 2a14:67c1:b4a1::/48
2a14:67c1:b4a2::/48 2a14:67c1:b4a2::/48
2a14:67c1:b4c0::/45 2a14:67c1:b4c0::/45
2a14:67c1:b4d0::/44 2a14:67c1:b4d0::/45
2a14:67c1:b4e0::/43 2a14:67c1:b4e0::/43
2a14:67c1:b500::/48 2a14:67c1:b500::/47
2a14:67c1:b549::/48 2a14:67c1:b549::/48
2a14:67c1:b561::/48 2a14:67c1:b561::/48
2a14:67c1:b563::/48 2a14:67c1:b563::/48
@@ -1401,16 +1400,17 @@
2a14:67c1:b582::/48 2a14:67c1:b582::/48
2a14:67c1:b588::/47 2a14:67c1:b588::/47
2a14:67c1:b590::/48 2a14:67c1:b590::/48
2a14:67c1:b599::/48
2a14:67c5:1900::/40 2a14:67c5:1900::/40
2a14:7580:9200::/40 2a14:7580:9200::/40
2a14:7580:9400::/39 2a14:7580:9400::/39
2a14:7580:d000::/37 2a14:7580:d000::/37
2a14:7580:d800::/39 2a14:7580:d800::/39
2a14:7580:da00::/40
2a14:7580:e200::/40 2a14:7580:e200::/40
2a14:7580:fe00::/40 2a14:7580:fe00::/40
2a14:7581:3100::/40 2a14:7581:3100::/40
2a14:7581:9010::/44 2a14:7583:f4fe::/48
2a14:7583:f500::/48
2c0f:f7a8:8011::/48 2c0f:f7a8:8011::/48
2c0f:f7a8:8050::/48 2c0f:f7a8:8050::/48
2c0f:f7a8:805f::/48 2c0f:f7a8:805f::/48

View File

@@ -1 +1 @@
20250822033443 20250912032015

View File

@@ -1 +1 @@
202508212214 202509112212

View File

@@ -735,6 +735,8 @@ brutaltgp.com
bsky.app bsky.app
bsky.network bsky.network
bsky.social bsky.social
bt4g.org
bt4gprx.com
bt95.com bt95.com
btaia.com btaia.com
btbit.net btbit.net
@@ -1131,6 +1133,7 @@ costco.com
cotweet.com cotweet.com
counter.social counter.social
coursehero.com coursehero.com
covenantswatch.org.tw
coze.com coze.com
cpj.org cpj.org
cpu-monkey.com cpu-monkey.com
@@ -1679,6 +1682,7 @@ fdc64.org
fdc89.jp fdc89.jp
feedburner.com feedburner.com
feeder.co feeder.co
feedly.com
feeds.fileforum.com feeds.fileforum.com
feedx.net feedx.net
feelssh.com feelssh.com
@@ -1851,6 +1855,8 @@ ftvnews.com.tw
ftx.com ftx.com
fucd.com fucd.com
fuchsia.dev fuchsia.dev
fuckccp.com
fuckccp.xyz
fuckgfw.org fuckgfw.org
fulione.com fulione.com
fullerconsideration.com fullerconsideration.com
@@ -2702,6 +2708,7 @@ iphone4hongkong.com
iphonetaiwan.org iphonetaiwan.org
iphonix.fr iphonix.fr
ipicture.ru ipicture.ru
ipify.org
ipjetable.net ipjetable.net
ipobar.com ipobar.com
ipoock.com ipoock.com
@@ -3307,6 +3314,7 @@ mofos.com
mog.com mog.com
mohu.club mohu.club
mohu.rocks mohu.rocks
moj.gov.tw
mojim.com mojim.com
mol.gov.tw mol.gov.tw
molihua.org molihua.org
@@ -4676,6 +4684,7 @@ talkcc.com
talkonly.net talkonly.net
tanc.org tanc.org
tangren.us tangren.us
tanks.gg
taoism.net taoism.net
tapanwap.com tapanwap.com
tapatalk.com tapatalk.com

View File

@@ -1 +1 @@
202508212214 202509112212

View File

@@ -5,7 +5,7 @@
NAME="homeproxy" NAME="homeproxy"
log_max_size="10" #KB log_max_size="50" #KB
main_log_file="/var/run/$NAME/$NAME.log" main_log_file="/var/run/$NAME/$NAME.log"
singc_log_file="/var/run/$NAME/sing-box-c.log" singc_log_file="/var/run/$NAME/sing-box-c.log"
sings_log_file="/var/run/$NAME/sing-box-s.log" sings_log_file="/var/run/$NAME/sing-box-s.log"

View File

@@ -135,6 +135,8 @@ if (match(proxy_mode), /tun/) {
endpoint_independent_nat = uci.get(uciconfig, uciroutingsetting, 'endpoint_independent_nat'); endpoint_independent_nat = uci.get(uciconfig, uciroutingsetting, 'endpoint_independent_nat');
} }
} }
const log_level = uci.get(uciconfig, ucimain, 'log_level') || 'warn';
/* UCI config end */ /* UCI config end */
/* Config helper start */ /* Config helper start */
@@ -399,7 +401,7 @@ const config = {};
/* Log */ /* Log */
config.log = { config.log = {
disabled: false, disabled: false,
level: 'warn', level: log_level,
output: RUN_DIR + '/sing-box-c.log', output: RUN_DIR + '/sing-box-c.log',
timestamp: true timestamp: true
}; };

View File

@@ -23,12 +23,15 @@ uci.load(uciconfig);
const uciserver = 'server'; const uciserver = 'server';
const log_level = uci.get(uciconfig, uciserver, 'log_level') || 'warn';
/* UCI config end */
const config = {}; const config = {};
/* Log */ /* Log */
config.log = { config.log = {
disabled: false, disabled: false,
level: 'warn', level: log_level,
output: RUN_DIR + '/sing-box-s.log', output: RUN_DIR + '/sing-box-s.log',
timestamp: true timestamp: true
}; };

View File

@@ -92,7 +92,7 @@ export function strToInt(str) {
}; };
export function strToTime(str) { export function strToTime(str) {
return str ? (str + 's') : null; return !isEmpty(str) ? (str + 's') : null;
}; };
export function removeBlankAttrs(res) { export function removeBlankAttrs(res) {

View File

@@ -50,8 +50,7 @@ if (github_token) {
} }
/* tun_gso was deprecated in sb 1.11 */ /* tun_gso was deprecated in sb 1.11 */
const tun_gso = uci.get(uciconfig, uciinfra, 'tun_gso'); if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
if (tun_gso || tun_gso === '0')
uci.delete(uciconfig, uciinfra, 'tun_gso'); uci.delete(uciconfig, uciinfra, 'tun_gso');
/* create migration section */ /* create migration section */
@@ -65,6 +64,13 @@ if (!migration_crontab) {
uci.set(uciconfig, ucimigration, 'crontab', '1'); uci.set(uciconfig, ucimigration, 'crontab', '1');
} }
/* log_level was introduced */
if (isEmpty(uci.get(uciconfig, ucimain, 'log_level'))
uci.set(uciconfig, ucimain, 'log_level', 'warn');
if (isEmpty(uci.get(uciconfig, uciserver, 'log_level'))
uci.set(uciconfig, uciserver, 'log_level', 'warn');
/* empty value defaults to all ports now */ /* empty value defaults to all ports now */
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all') if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
uci.delete(uciconfig, ucimain, 'routing_port'); uci.delete(uciconfig, ucimain, 'routing_port');
@@ -204,6 +210,10 @@ uci.foreach(uciconfig, ucinode, (cfg) => {
if (!isEmpty(cfg.tls_ech_tls_disable_drs)) if (!isEmpty(cfg.tls_ech_tls_disable_drs))
uci.delete(uciconfig, cfg['.name'], 'tls_ech_tls_disable_drs'); uci.delete(uciconfig, cfg['.name'], 'tls_ech_tls_disable_drs');
/* tls_ech_enable_pqss is useless and deprecated in sb 1.12 */
if (!isEmpty(cfg.tls_ech_enable_pqss))
uci.delete(uciconfig, cfg['.name'], 'tls_ech_enable_pqss');
/* wireguard_gso was deprecated in sb 1.11 */ /* wireguard_gso was deprecated in sb 1.11 */
if (!isEmpty(cfg.wireguard_gso)) if (!isEmpty(cfg.wireguard_gso))
uci.delete(uciconfig, cfg['.name'], 'wireguard_gso'); uci.delete(uciconfig, cfg['.name'], 'wireguard_gso');
@@ -228,7 +238,7 @@ uci.foreach(uciconfig, uciroutingrule, (cfg) => {
/* server options */ /* server options */
/* auto_firewall was moved into server options */ /* auto_firewall was moved into server options */
const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall'); const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall');
if (auto_firewall || auto_firewall === '0') if (!isEmpty(auto_firewall))
uci.delete(uciconfig, uciserver, 'auto_firewall'); uci.delete(uciconfig, uciserver, 'auto_firewall');
uci.foreach(uciconfig, uciserver, (cfg) => { uci.foreach(uciconfig, uciserver, (cfg) => {

View File

@@ -569,7 +569,7 @@ function main() {
log(sprintf('Removing node: %s.', cfg.label || cfg['name'])); log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
} else { } else {
map(keys(node_cache[cfg.grouphash][cfg['.name']]), (v) => { map(keys(cfg), (v) => {
if (v in node_cache[cfg.grouphash][cfg['.name']]) if (v in node_cache[cfg.grouphash][cfg['.name']])
uci.set(uciconfig, cfg['.name'], v, node_cache[cfg.grouphash][cfg['.name']][v]); uci.set(uciconfig, cfg['.name'], v, node_cache[cfg.grouphash][cfg['.name']][v]);
else else

View File

@@ -55,22 +55,18 @@ local api = require "luci.passwall.api"
"gfwlist_update","chnroute_update","chnroute6_update", "gfwlist_update","chnroute_update","chnroute6_update",
"chnlist_update","geoip_update","geosite_update" "chnlist_update","geoip_update","geosite_update"
]; ];
const targetNode = document.querySelector('form') || document.body; const bindFlags = () => {
const observer = new MutationObserver(() => { let allBound = true;
flags.forEach(flag => { flags.forEach(flag => {
const orig = Array.from(document.querySelectorAll(`input[name$=".${flag}"]`)).find(i => i.type === 'checkbox'); const orig = Array.from(document.querySelectorAll(`input[name$=".${flag}"]`)).find(i => i.type === 'checkbox');
if (!orig) { if (!orig) { allBound = false; return; }
return;
}
// 隐藏最外层 div // 隐藏最外层 div
const wrapper = orig.closest('.cbi-value'); const wrapper = orig.closest('.cbi-value');
if (wrapper && wrapper.style.display !== 'none') { if (wrapper && wrapper.style.display !== 'none') {
wrapper.style.display = 'none'; wrapper.style.display = 'none';
} }
const custom = document.querySelector(`.cbi-input-checkbox[name="${flag.replace('_update','')}"]`); const custom = document.querySelector(`.cbi-input-checkbox[name="${flag.replace('_update','')}"]`);
if (!custom) { if (!custom) { allBound = false; return; }
return;
}
custom.checked = orig.checked; custom.checked = orig.checked;
// 自定义选择框与原生Flag双向绑定 // 自定义选择框与原生Flag双向绑定
if (!custom._binded) { if (!custom._binded) {
@@ -84,8 +80,13 @@ local api = require "luci.passwall.api"
}); });
} }
}); });
}); return allBound;
observer.observe(targetNode, { childList: true, subtree: true }); };
const target = document.querySelector('form') || document.body;
const observer = new MutationObserver(() => bindFlags() ? observer.disconnect() : 0);
observer.observe(target, { childList: true, subtree: true });
const timer = setInterval(() => bindFlags() ? (clearInterval(timer), observer.disconnect()) : 0, 300);
setTimeout(() => { clearInterval(timer); observer.disconnect(); }, 5000);
}); });
function update_rules(btn) { function update_rules(btn) {

View File

@@ -9,6 +9,31 @@ public class JsonUtils
{ {
private static readonly string _tag = "JsonUtils"; private static readonly string _tag = "JsonUtils";
private static readonly JsonSerializerOptions _defaultDeserializeOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
private static readonly JsonSerializerOptions _defaultSerializeOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static readonly JsonSerializerOptions _nullValueSerializeOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static readonly JsonDocumentOptions _defaultDocumentOptions = new()
{
CommentHandling = JsonCommentHandling.Skip
};
/// <summary> /// <summary>
/// DeepCopy /// DeepCopy
/// </summary> /// </summary>
@@ -34,11 +59,7 @@ public class JsonUtils
{ {
return default; return default;
} }
var options = new JsonSerializerOptions return JsonSerializer.Deserialize<T>(strJson, _defaultDeserializeOptions);
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<T>(strJson, options);
} }
catch catch
{ {
@@ -59,7 +80,7 @@ public class JsonUtils
{ {
return null; return null;
} }
return JsonNode.Parse(strJson); return JsonNode.Parse(strJson, nodeOptions: null, _defaultDocumentOptions);
} }
catch catch
{ {
@@ -84,12 +105,7 @@ public class JsonUtils
{ {
return result; return result;
} }
var options = new JsonSerializerOptions var options = nullValue ? _nullValueSerializeOptions : _defaultSerializeOptions;
{
WriteIndented = indented,
DefaultIgnoreCondition = nullValue ? JsonIgnoreCondition.Never : JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
result = JsonSerializer.Serialize(obj, options); result = JsonSerializer.Serialize(obj, options);
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -331,6 +331,32 @@ public class Utils
.ToList(); .ToList();
} }
public static Dictionary<string, List<string>> ParseHostsToDictionary(string hostsContent)
{
var userHostsMap = hostsContent
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
// skip full-line comments
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
// strip inline comments (truncate at '#')
.Select(line =>
{
var index = line.IndexOf('#');
return index >= 0 ? line.Substring(0, index).Trim() : line;
})
// ensure line still contains valid parts
.Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' '))
.Select(line => line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries))
.Where(parts => parts.Length >= 2)
.GroupBy(parts => parts[0])
.ToDictionary(
group => group.Key,
group => group.SelectMany(parts => parts.Skip(1)).ToList()
);
return userHostsMap;
}
#endregion #endregion
#region #region
@@ -857,6 +883,55 @@ public class Utils
return false; return false;
} }
public static bool IsPackagedInstall()
{
try
{
if (IsWindows() || IsOSX())
{
return false;
}
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPIMAGE")))
{
return true;
}
var exePath = GetExePath();
var baseDir = string.IsNullOrEmpty(exePath) ? StartupPath() : Path.GetDirectoryName(exePath) ?? "";
var p = baseDir.Replace('\\', '/');
if (string.IsNullOrEmpty(p))
{
return false;
}
if (p.Contains("/.mount_", StringComparison.Ordinal))
{
return true;
}
if (p.StartsWith("/opt/v2rayN", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (p.StartsWith("/usr/lib/v2rayN", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (p.StartsWith("/usr/share/v2rayN", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
catch
{
}
return false;
}
private static async Task<string?> GetLinuxUserId() private static async Task<string?> GetLinuxUserId()
{ {
var arg = new List<string>() { "-c", "id -u" }; var arg = new List<string>() { "-c", "id -u" };

View File

@@ -4,7 +4,7 @@ public class ClashFmt : BaseFmt
{ {
public static ProfileItem? ResolveFull(string strData, string? subRemarks) public static ProfileItem? ResolveFull(string strData, string? subRemarks)
{ {
if (Contains(strData, "port", "socks-port", "proxies")) if (Contains(strData, "external-controller", "-port", "proxies"))
{ {
var fileName = WriteAllText(strData, "yaml"); var fileName = WriteAllText(strData, "yaml");

View File

@@ -94,17 +94,7 @@ public partial class CoreConfigSingboxService
if (!simpleDNSItem.Hosts.IsNullOrEmpty()) if (!simpleDNSItem.Hosts.IsNullOrEmpty())
{ {
var userHostsMap = simpleDNSItem.Hosts var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
.Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' '))
.Select(line => line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries))
.Where(parts => parts.Length >= 2)
.GroupBy(parts => parts[0])
.ToDictionary(
group => group.Key,
group => group.SelectMany(parts => parts.Skip(1)).ToList()
);
foreach (var kvp in userHostsMap) foreach (var kvp in userHostsMap)
{ {

View File

@@ -71,6 +71,31 @@ public partial class CoreConfigSingboxService
}); });
} }
var hostsDomains = new List<string>();
var systemHostsMap = Utils.GetSystemHosts();
foreach (var kvp in systemHostsMap)
{
hostsDomains.Add(kvp.Key);
}
var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (dnsItem == null || dnsItem.Enabled == false)
{
var simpleDNSItem = _config.SimpleDNSItem;
if (!simpleDNSItem.Hosts.IsNullOrEmpty())
{
var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
foreach (var kvp in userHostsMap)
{
hostsDomains.Add(kvp.Key);
}
}
}
singboxConfig.route.rules.Add(new()
{
action = "resolve",
domain = hostsDomains,
});
singboxConfig.route.rules.Add(new() singboxConfig.route.rules.Add(new()
{ {
outbound = Global.DirectTag, outbound = Global.DirectTag,

View File

@@ -261,17 +261,7 @@ public partial class CoreConfigV2rayService
if (!simpleDNSItem.Hosts.IsNullOrEmpty()) if (!simpleDNSItem.Hosts.IsNullOrEmpty())
{ {
var userHostsMap = simpleDNSItem.Hosts var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
.Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' '))
.Select(line => line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries))
.Where(parts => parts.Length >= 2)
.GroupBy(parts => parts[0])
.ToDictionary(
group => group.Key,
group => group.SelectMany(parts => parts.Skip(1)).ToList()
);
foreach (var kvp in userHostsMap) foreach (var kvp in userHostsMap)
{ {

View File

@@ -63,6 +63,16 @@ public class CheckUpdateViewModel : MyReactiveObject
private CheckUpdateModel GetCheckUpdateModel(string coreType) private CheckUpdateModel GetCheckUpdateModel(string coreType)
{ {
if (coreType == _v2rayN && Utils.IsPackagedInstall())
{
return new()
{
IsSelected = false,
CoreType = coreType,
Remarks = ResUI.menuCheckUpdate + " (Not Support)",
};
}
return new() return new()
{ {
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType) ?? true, IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType) ?? true,
@@ -104,6 +114,11 @@ public class CheckUpdateViewModel : MyReactiveObject
} }
else if (item.CoreType == _v2rayN) else if (item.CoreType == _v2rayN)
{ {
if (Utils.IsPackagedInstall())
{
await UpdateView(_v2rayN, "Not Support");
continue;
}
await CheckUpdateN(EnableCheckPreReleaseUpdate); await CheckUpdateN(EnableCheckPreReleaseUpdate);
} }
else if (item.CoreType == ECoreType.Xray.ToString()) else if (item.CoreType == ECoreType.Xray.ToString())

View File

@@ -400,7 +400,7 @@
Grid.Column="1" Grid.Column="1"
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Watermark="1000:2000,3000:4000" /> Watermark="1000-2000,3000,4000" />
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
Grid.Column="2" Grid.Column="2"

View File

@@ -176,6 +176,7 @@
<DataGrid <DataGrid
x:Name="lstRules" x:Name="lstRules"
AutoGenerateColumns="False" AutoGenerateColumns="False"
Background="Transparent"
BorderThickness="1" BorderThickness="1"
CanUserResizeColumns="True" CanUserResizeColumns="True"
GridLinesVisibility="All" GridLinesVisibility="All"

View File

@@ -92,6 +92,7 @@
<DataGrid <DataGrid
x:Name="lstRoutings" x:Name="lstRoutings"
AutoGenerateColumns="False" AutoGenerateColumns="False"
Background="Transparent"
BorderThickness="1" BorderThickness="1"
CanUserResizeColumns="True" CanUserResizeColumns="True"
GridLinesVisibility="All" GridLinesVisibility="All"

View File

@@ -538,7 +538,7 @@
Width="400" Width="400"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
materialDesign:HintAssist.Hint="1000:2000,3000:4000" materialDesign:HintAssist.Hint="1000-2000,3000,4000"
Style="{StaticResource DefTextBox}" /> Style="{StaticResource DefTextBox}" />
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"

View File

@@ -122,6 +122,8 @@ public partial class RoutingRuleSettingWindow
private void RoutingRuleSettingWindow_PreviewKeyDown(object sender, KeyEventArgs e) private void RoutingRuleSettingWindow_PreviewKeyDown(object sender, KeyEventArgs e)
{ {
if (!lstRules.IsKeyboardFocusWithin)
return;
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{ {
if (e.Key == Key.A) if (e.Key == Key.A)

View File

@@ -424,7 +424,6 @@ from .cpac import (
CPACPlaylistIE, CPACPlaylistIE,
) )
from .cracked import CrackedIE from .cracked import CrackedIE
from .crackle import CrackleIE
from .craftsy import CraftsyIE from .craftsy import CraftsyIE
from .crooksandliars import CrooksAndLiarsIE from .crooksandliars import CrooksAndLiarsIE
from .crowdbunker import ( from .crowdbunker import (
@@ -444,10 +443,6 @@ from .curiositystream import (
CuriosityStreamIE, CuriosityStreamIE,
CuriosityStreamSeriesIE, CuriosityStreamSeriesIE,
) )
from .cwtv import (
CWTVIE,
CWTVMovieIE,
)
from .cybrary import ( from .cybrary import (
CybraryCourseIE, CybraryCourseIE,
CybraryIE, CybraryIE,
@@ -1433,6 +1428,7 @@ from .onet import (
OnetPlIE, OnetPlIE,
) )
from .onionstudios import OnionStudiosIE from .onionstudios import OnionStudiosIE
from .onsen import OnsenIE
from .opencast import ( from .opencast import (
OpencastIE, OpencastIE,
OpencastPlaylistIE, OpencastPlaylistIE,
@@ -1466,10 +1462,6 @@ from .panopto import (
PanoptoListIE, PanoptoListIE,
PanoptoPlaylistIE, PanoptoPlaylistIE,
) )
from .paramountplus import (
ParamountPlusIE,
ParamountPlusSeriesIE,
)
from .parler import ParlerIE from .parler import ParlerIE
from .parlview import ParlviewIE from .parlview import ParlviewIE
from .parti import ( from .parti import (
@@ -1779,7 +1771,6 @@ from .rutube import (
RutubePlaylistIE, RutubePlaylistIE,
RutubeTagsIE, RutubeTagsIE,
) )
from .rutv import RUTVIE
from .ruutu import RuutuIE from .ruutu import RuutuIE
from .ruv import ( from .ruv import (
RuvIE, RuvIE,
@@ -1849,7 +1840,6 @@ from .simplecast import (
SimplecastPodcastIE, SimplecastPodcastIE,
) )
from .sina import SinaIE from .sina import SinaIE
from .sixplay import SixPlayIE
from .skeb import SkebIE from .skeb import SkebIE
from .sky import ( from .sky import (
SkyNewsIE, SkyNewsIE,
@@ -1877,7 +1867,12 @@ from .skynewsau import SkyNewsAUIE
from .slideshare import SlideshareIE from .slideshare import SlideshareIE
from .slideslive import SlidesLiveIE from .slideslive import SlidesLiveIE
from .slutload import SlutloadIE from .slutload import SlutloadIE
from .smotrim import SmotrimIE from .smotrim import (
SmotrimAudioIE,
SmotrimIE,
SmotrimLiveIE,
SmotrimPlaylistIE,
)
from .snapchat import SnapchatSpotlightIE from .snapchat import SnapchatSpotlightIE
from .snotr import SnotrIE from .snotr import SnotrIE
from .softwhiteunderbelly import SoftWhiteUnderbellyIE from .softwhiteunderbelly import SoftWhiteUnderbellyIE
@@ -1925,10 +1920,6 @@ from .spiegel import SpiegelIE
from .sport5 import Sport5IE from .sport5 import Sport5IE
from .sportbox import SportBoxIE from .sportbox import SportBoxIE
from .sportdeutschland import SportDeutschlandIE from .sportdeutschland import SportDeutschlandIE
from .spotify import (
SpotifyIE,
SpotifyShowIE,
)
from .spreaker import ( from .spreaker import (
SpreakerIE, SpreakerIE,
SpreakerShowIE, SpreakerShowIE,
@@ -2149,6 +2140,7 @@ from .tubitv import (
) )
from .tumblr import TumblrIE from .tumblr import TumblrIE
from .tunein import ( from .tunein import (
TuneInEmbedIE,
TuneInPodcastEpisodeIE, TuneInPodcastEpisodeIE,
TuneInPodcastIE, TuneInPodcastIE,
TuneInShortenerIE, TuneInShortenerIE,
@@ -2283,7 +2275,6 @@ from .utreon import UtreonIE
from .varzesh3 import Varzesh3IE from .varzesh3 import Varzesh3IE
from .vbox7 import Vbox7IE from .vbox7 import Vbox7IE
from .veo import VeoIE from .veo import VeoIE
from .vesti import VestiIE
from .vevo import ( from .vevo import (
VevoIE, VevoIE,
VevoPlaylistIE, VevoPlaylistIE,
@@ -2472,7 +2463,6 @@ from .wykop import (
WykopPostCommentIE, WykopPostCommentIE,
WykopPostIE, WykopPostIE,
) )
from .xanimu import XanimuIE
from .xboxclips import XboxClipsIE from .xboxclips import XboxClipsIE
from .xhamster import ( from .xhamster import (
XHamsterEmbedIE, XHamsterEmbedIE,

View File

@@ -5,8 +5,6 @@ import zlib
from .anvato import AnvatoIE from .anvato import AnvatoIE
from .common import InfoExtractor from .common import InfoExtractor
from .paramountplus import ParamountPlusIE
from ..networking import HEADRequest
from ..utils import ( from ..utils import (
ExtractorError, ExtractorError,
UserNotLive, UserNotLive,
@@ -132,13 +130,7 @@ class CBSNewsEmbedIE(CBSNewsBaseIE):
video_id = item['mpxRefId'] video_id = item['mpxRefId']
video_url = self._get_video_url(item) video_url = self._get_video_url(item)
if not video_url: if not video_url:
# Old embeds redirect user to ParamountPlus but most links are 404 raise ExtractorError('This video is no longer available', expected=True)
pplus_url = f'https://www.paramountplus.com/shows/video/{video_id}'
try:
self._request_webpage(HEADRequest(pplus_url), video_id)
return self.url_result(pplus_url, ParamountPlusIE)
except ExtractorError:
self.raise_no_formats('This video is no longer available', True, video_id)
return self._extract_video(item, video_url, video_id) return self._extract_video(item, video_url, video_id)

View File

@@ -1,243 +0,0 @@
import hashlib
import hmac
import re
import time
from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import (
ExtractorError,
determine_ext,
float_or_none,
int_or_none,
orderedSet,
parse_age_limit,
parse_duration,
url_or_none,
)
class CrackleIE(InfoExtractor):
_VALID_URL = r'(?:crackle:|https?://(?:(?:www|m)\.)?(?:sony)?crackle\.com/(?:playlist/\d+/|(?:[^/]+/)+))(?P<id>\d+)'
_TESTS = [{
# Crackle is available in the United States and territories
'url': 'https://www.crackle.com/thanksgiving/2510064',
'info_dict': {
'id': '2510064',
'ext': 'mp4',
'title': 'Touch Football',
'description': 'md5:cfbb513cf5de41e8b56d7ab756cff4df',
'duration': 1398,
'view_count': int,
'average_rating': 0,
'age_limit': 17,
'genre': 'Comedy',
'creator': 'Daniel Powell',
'artist': 'Chris Elliott, Amy Sedaris',
'release_year': 2016,
'series': 'Thanksgiving',
'episode': 'Touch Football',
'season_number': 1,
'episode_number': 1,
},
'params': {
# m3u8 download
'skip_download': True,
},
'expected_warnings': [
'Trying with a list of known countries',
],
}, {
'url': 'https://www.sonycrackle.com/thanksgiving/2510064',
'only_matching': True,
}]
_MEDIA_FILE_SLOTS = {
'360p.mp4': {
'width': 640,
'height': 360,
},
'480p.mp4': {
'width': 768,
'height': 432,
},
'480p_1mbps.mp4': {
'width': 852,
'height': 480,
},
}
def _download_json(self, url, *args, **kwargs):
# Authorization generation algorithm is reverse engineered from:
# https://www.sonycrackle.com/static/js/main.ea93451f.chunk.js
timestamp = time.strftime('%Y%m%d%H%M', time.gmtime())
h = hmac.new(b'IGSLUQCBDFHEOIFM', '|'.join([url, timestamp]).encode(), hashlib.sha1).hexdigest().upper()
headers = {
'Accept': 'application/json',
'Authorization': '|'.join([h, timestamp, '117', '1']),
}
return InfoExtractor._download_json(self, url, *args, headers=headers, **kwargs)
def _real_extract(self, url):
video_id = self._match_id(url)
geo_bypass_country = self.get_param('geo_bypass_country', None)
countries = orderedSet((geo_bypass_country, 'US', 'AU', 'CA', 'AS', 'FM', 'GU', 'MP', 'PR', 'PW', 'MH', 'VI', ''))
num_countries, num = len(countries) - 1, 0
media = {}
for num, country in enumerate(countries):
if num == 1: # start hard-coded list
self.report_warning('%s. Trying with a list of known countries' % (
f'Unable to obtain video formats from {geo_bypass_country} API' if geo_bypass_country
else 'No country code was given using --geo-bypass-country'))
elif num == num_countries: # end of list
geo_info = self._download_json(
'https://web-api-us.crackle.com/Service.svc/geo/country',
video_id, fatal=False, note='Downloading geo-location information from crackle API',
errnote='Unable to fetch geo-location information from crackle') or {}
country = geo_info.get('CountryCode')
if country is None:
continue
self.to_screen(f'{self.IE_NAME} identified country as {country}')
if country in countries:
self.to_screen(f'Downloading from {country} API was already attempted. Skipping...')
continue
if country is None:
continue
try:
media = self._download_json(
f'https://web-api-us.crackle.com/Service.svc/details/media/{video_id}/{country}?disableProtocols=true',
video_id, note=f'Downloading media JSON from {country} API',
errnote='Unable to download media JSON')
except ExtractorError as e:
# 401 means geo restriction, trying next country
if isinstance(e.cause, HTTPError) and e.cause.status == 401:
continue
raise
status = media.get('status')
if status.get('messageCode') != '0':
raise ExtractorError(
'{} said: {} {} - {}'.format(
self.IE_NAME, status.get('messageCodeDescription'), status.get('messageCode'), status.get('message')),
expected=True)
# Found video formats
if isinstance(media.get('MediaURLs'), list):
break
ignore_no_formats = self.get_param('ignore_no_formats_error')
if not media or (not media.get('MediaURLs') and not ignore_no_formats):
raise ExtractorError(
'Unable to access the crackle API. Try passing your country code '
'to --geo-bypass-country. If it still does not work and the '
'video is available in your country')
title = media['Title']
formats, subtitles = [], {}
has_drm = False
for e in media.get('MediaURLs') or []:
if e.get('UseDRM'):
has_drm = True
format_url = url_or_none(e.get('DRMPath'))
else:
format_url = url_or_none(e.get('Path'))
if not format_url:
continue
ext = determine_ext(format_url)
if ext == 'm3u8':
fmts, subs = self._extract_m3u8_formats_and_subtitles(
format_url, video_id, 'mp4', entry_protocol='m3u8_native',
m3u8_id='hls', fatal=False)
formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
elif ext == 'mpd':
fmts, subs = self._extract_mpd_formats_and_subtitles(
format_url, video_id, mpd_id='dash', fatal=False)
formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
elif format_url.endswith('.ism/Manifest'):
fmts, subs = self._extract_ism_formats_and_subtitles(
format_url, video_id, ism_id='mss', fatal=False)
formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
else:
mfs_path = e.get('Type')
mfs_info = self._MEDIA_FILE_SLOTS.get(mfs_path)
if not mfs_info:
continue
formats.append({
'url': format_url,
'format_id': 'http-' + mfs_path.split('.')[0],
'width': mfs_info['width'],
'height': mfs_info['height'],
})
if not formats and has_drm:
self.report_drm(video_id)
description = media.get('Description')
duration = int_or_none(media.get(
'DurationInSeconds')) or parse_duration(media.get('Duration'))
view_count = int_or_none(media.get('CountViews'))
average_rating = float_or_none(media.get('UserRating'))
age_limit = parse_age_limit(media.get('Rating'))
genre = media.get('Genre')
release_year = int_or_none(media.get('ReleaseYear'))
creator = media.get('Directors')
artist = media.get('Cast')
if media.get('MediaTypeDisplayValue') == 'Full Episode':
series = media.get('ShowName')
episode = title
season_number = int_or_none(media.get('Season'))
episode_number = int_or_none(media.get('Episode'))
else:
series = episode = season_number = episode_number = None
cc_files = media.get('ClosedCaptionFiles')
if isinstance(cc_files, list):
for cc_file in cc_files:
if not isinstance(cc_file, dict):
continue
cc_url = url_or_none(cc_file.get('Path'))
if not cc_url:
continue
lang = cc_file.get('Locale') or 'en'
subtitles.setdefault(lang, []).append({'url': cc_url})
thumbnails = []
images = media.get('Images')
if isinstance(images, list):
for image_key, image_url in images.items():
mobj = re.search(r'Img_(\d+)[xX](\d+)', image_key)
if not mobj:
continue
thumbnails.append({
'url': image_url,
'width': int(mobj.group(1)),
'height': int(mobj.group(2)),
})
return {
'id': video_id,
'title': title,
'description': description,
'duration': duration,
'view_count': view_count,
'average_rating': average_rating,
'age_limit': age_limit,
'genre': genre,
'creator': creator,
'artist': artist,
'release_year': release_year,
'series': series,
'episode': episode,
'season_number': season_number,
'episode_number': episode_number,
'thumbnails': thumbnails,
'subtitles': subtitles,
'formats': formats,
}

View File

@@ -1,180 +0,0 @@
import re
from .common import InfoExtractor
from ..utils import (
ExtractorError,
int_or_none,
parse_age_limit,
parse_iso8601,
parse_qs,
smuggle_url,
str_or_none,
update_url_query,
)
from ..utils.traversal import traverse_obj
class CWTVIE(InfoExtractor):
IE_NAME = 'cwtv'
_VALID_URL = r'https?://(?:www\.)?cw(?:tv(?:pr)?|seed)\.com/(?:shows/)?(?:[^/]+/)+[^?]*\?.*\b(?:play|watch|guid)=(?P<id>[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12})'
_TESTS = [{
'url': 'https://www.cwtv.com/shows/continuum/a-stitch-in-time/?play=9149a1e1-4cb2-46d7-81b2-47d35bbd332b',
'info_dict': {
'id': '9149a1e1-4cb2-46d7-81b2-47d35bbd332b',
'ext': 'mp4',
'title': 'A Stitch in Time',
'description': r're:(?s)City Protective Services officer Kiera Cameron is transported from 2077.+',
'thumbnail': r're:https?://.+\.jpe?g',
'duration': 2632,
'timestamp': 1736928000,
'uploader': 'CWTV',
'chapters': 'count:5',
'series': 'Continuum',
'season_number': 1,
'episode_number': 1,
'age_limit': 14,
'upload_date': '20250115',
'season': 'Season 1',
'episode': 'Episode 1',
},
'params': {
# m3u8 download
'skip_download': True,
},
}, {
'url': 'http://cwtv.com/shows/arrow/legends-of-yesterday/?play=6b15e985-9345-4f60-baf8-56e96be57c63',
'info_dict': {
'id': '6b15e985-9345-4f60-baf8-56e96be57c63',
'ext': 'mp4',
'title': 'Legends of Yesterday',
'description': r're:(?s)Oliver and Barry Allen take Kendra Saunders and Carter Hall to a remote.+',
'duration': 2665,
'series': 'Arrow',
'season_number': 4,
'season': '4',
'episode_number': 8,
'upload_date': '20151203',
'timestamp': 1449122100,
},
'params': {
# m3u8 download
'skip_download': True,
},
'skip': 'redirect to http://cwtv.com/shows/arrow/',
}, {
'url': 'http://www.cwseed.com/shows/whose-line-is-it-anyway/jeff-davis-4/?play=24282b12-ead2-42f2-95ad-26770c2c6088',
'info_dict': {
'id': '24282b12-ead2-42f2-95ad-26770c2c6088',
'ext': 'mp4',
'title': 'Jeff Davis 4',
'description': 'Jeff Davis is back to make you laugh.',
'duration': 1263,
'series': 'Whose Line Is It Anyway?',
'season_number': 11,
'episode_number': 20,
'upload_date': '20151006',
'timestamp': 1444107300,
'age_limit': 14,
'uploader': 'CWTV',
'thumbnail': r're:https?://.+\.jpe?g',
'chapters': 'count:4',
'episode': 'Episode 20',
'season': 'Season 11',
},
'params': {
# m3u8 download
'skip_download': True,
},
}, {
'url': 'http://cwtv.com/thecw/chroniclesofcisco/?play=8adebe35-f447-465f-ab52-e863506ff6d6',
'only_matching': True,
}, {
'url': 'http://cwtvpr.com/the-cw/video?watch=9eee3f60-ef4e-440b-b3b2-49428ac9c54e',
'only_matching': True,
}, {
'url': 'http://cwtv.com/shows/arrow/legends-of-yesterday/?watch=6b15e985-9345-4f60-baf8-56e96be57c63',
'only_matching': True,
}, {
'url': 'http://www.cwtv.com/movies/play/?guid=0a8e8b5b-1356-41d5-9a6a-4eda1a6feb6c',
'only_matching': True,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
data = self._download_json(
f'https://images.cwtv.com/feed/app-2/video-meta/apiversion_22/device_android/guid_{video_id}', video_id)
if traverse_obj(data, 'result') != 'ok':
raise ExtractorError(traverse_obj(data, (('error_msg', 'msg'), {str}, any)), expected=True)
video_data = data['video']
title = video_data['title']
mpx_url = update_url_query(
video_data.get('mpx_url') or f'https://link.theplatform.com/s/cwtv/media/guid/2703454149/{video_id}',
{'formats': 'M3U+none'})
season = str_or_none(video_data.get('season'))
episode = str_or_none(video_data.get('episode'))
if episode and season:
episode = episode[len(season):]
return {
'_type': 'url_transparent',
'id': video_id,
'title': title,
'url': smuggle_url(mpx_url, {'force_smil_url': True}),
'description': video_data.get('description_long'),
'duration': int_or_none(video_data.get('duration_secs')),
'series': video_data.get('series_name'),
'season_number': int_or_none(season),
'episode_number': int_or_none(episode),
'timestamp': parse_iso8601(video_data.get('start_time')),
'age_limit': parse_age_limit(video_data.get('rating')),
'ie_key': 'ThePlatform',
'thumbnail': video_data.get('large_thumbnail'),
}
class CWTVMovieIE(InfoExtractor):
IE_NAME = 'cwtv:movie'
_VALID_URL = r'https?://(?:www\.)?cwtv\.com/shows/(?P<id>[\w-]+)/?\?(?:[^#]+&)?viewContext=Movies'
_TESTS = [{
'url': 'https://www.cwtv.com/shows/the-crush/?viewContext=Movies+Swimlane',
'info_dict': {
'id': '0a8e8b5b-1356-41d5-9a6a-4eda1a6feb6c',
'ext': 'mp4',
'title': 'The Crush',
'upload_date': '20241112',
'description': 'md5:1549acd90dff4a8273acd7284458363e',
'chapters': 'count:9',
'timestamp': 1731398400,
'age_limit': 16,
'duration': 5337,
'series': 'The Crush',
'season': 'Season 1',
'uploader': 'CWTV',
'season_number': 1,
'episode': 'Episode 1',
'episode_number': 1,
'thumbnail': r're:https?://.+\.jpe?g',
},
'params': {
# m3u8 download
'skip_download': True,
},
}]
_UUID_RE = r'[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12}'
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
app_url = (
self._html_search_meta('al:ios:url', webpage, default=None)
or self._html_search_meta('al:android:url', webpage, default=None))
video_id = (
traverse_obj(parse_qs(app_url), ('video_id', 0, {lambda x: re.fullmatch(self._UUID_RE, x)}, 0))
or self._search_regex([
rf'CWTV\.Site\.curPlayingGUID\s*=\s*["\']({self._UUID_RE})',
rf'CWTV\.Site\.viewInAppURL\s*=\s*["\']/shows/[\w-]+/watch-in-app/\?play=({self._UUID_RE})',
], webpage, 'video ID'))
return self.url_result(
f'https://www.cwtv.com/shows/{display_id}/{display_id}/?play={video_id}', CWTVIE, video_id)

View File

@@ -37,7 +37,7 @@ class LocoIE(InfoExtractor):
}, },
}, { }, {
'url': 'https://loco.com/stream/c64916eb-10fb-46a9-9a19-8c4b7ed064e7', 'url': 'https://loco.com/stream/c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
'md5': '45ebc8a47ee1c2240178757caf8881b5', 'md5': '8b9bda03eba4d066928ae8d71f19befb',
'info_dict': { 'info_dict': {
'id': 'c64916eb-10fb-46a9-9a19-8c4b7ed064e7', 'id': 'c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
'ext': 'mp4', 'ext': 'mp4',
@@ -55,9 +55,9 @@ class LocoIE(InfoExtractor):
'tags': ['Gameplay'], 'tags': ['Gameplay'],
'series': 'GTA 5', 'series': 'GTA 5',
'timestamp': 1740612872, 'timestamp': 1740612872,
'modified_timestamp': 1740613037, 'modified_timestamp': 1750948439,
'upload_date': '20250226', 'upload_date': '20250226',
'modified_date': '20250226', 'modified_date': '20250626',
}, },
}, { }, {
# Requires video authorization # Requires video authorization
@@ -123,8 +123,8 @@ class LocoIE(InfoExtractor):
def _real_extract(self, url): def _real_extract(self, url):
video_type, video_id = self._match_valid_url(url).group('type', 'id') video_type, video_id = self._match_valid_url(url).group('type', 'id')
webpage = self._download_webpage(url, video_id) webpage = self._download_webpage(url, video_id)
stream = traverse_obj(self._search_nextjs_data(webpage, video_id), ( stream = traverse_obj(self._search_nextjs_v13_data(webpage, video_id), (
'props', 'pageProps', ('liveStreamData', 'stream', 'liveStream'), {dict}, any, {require('stream info')})) ..., (None, 'ssrData'), ('liveStreamData', 'stream', 'liveStream'), {dict}, any, {require('stream info')}))
if access_token := self._get_access_token(video_id): if access_token := self._get_access_token(video_id):
self._request_webpage( self._request_webpage(

View File

@@ -0,0 +1,151 @@
import base64
import json
from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import (
ExtractorError,
clean_html,
int_or_none,
parse_qs,
str_or_none,
strftime_or_none,
update_url,
update_url_query,
url_or_none,
)
from ..utils.traversal import traverse_obj
class OnsenIE(InfoExtractor):
IE_NAME = 'onsen'
IE_DESC = 'インターネットラジオステーション<音泉>'
_BASE_URL = 'https://www.onsen.ag'
_HEADERS = {'Referer': f'{_BASE_URL}/'}
_NETRC_MACHINE = 'onsen'
_VALID_URL = r'https?://(?:(?:share|www)\.)onsen\.ag/program/(?P<id>[^/?#]+)'
_TESTS = [{
'url': 'https://share.onsen.ag/program/onsenking?p=90&c=MTA0NjI',
'info_dict': {
'id': '10462',
'ext': 'm4a',
'title': '第SP回',
'cast': 'count:3',
'description': 'md5:de62c80a41c4c8d84da53a1ee681ad18',
'display_id': 'MTA0NjI=',
'media_type': 'sound',
'section_start': 0,
'series': '音泉キング「下野紘」のラジオ きみはもちろん、<音泉>ファミリーだよね?',
'series_id': 'onsenking',
'tags': 'count:2',
'thumbnail': r're:https?://d3bzklg4lms4gh\.cloudfront\.net/program_info/image/default/production/.+',
'upload_date': '20220627',
'webpage_url': 'https://www.onsen.ag/program/onsenking?c=MTA0NjI=',
},
}, {
'url': 'https://share.onsen.ag/program/girls-band-cry-radio?p=370&c=MTgwMDE',
'info_dict': {
'id': '18001',
'ext': 'mp4',
'title': '第4回',
'cast': 'count:5',
'description': 'md5:bbca8a389d99c90cbbce8f383c85fedd',
'display_id': 'MTgwMDE=',
'media_type': 'movie',
'section_start': 0,
'series': 'TVアニメ『ガールズバンドクライ』WEBラジオ「ガールズバンドクライラジオにも全部ぶち込め。',
'series_id': 'girls-band-cry-radio',
'tags': 'count:3',
'thumbnail': r're:https?://d3bzklg4lms4gh\.cloudfront\.net/program_info/image/default/production/.+',
'upload_date': '20240425',
'webpage_url': 'https://www.onsen.ag/program/girls-band-cry-radio?c=MTgwMDE=',
},
'skip': 'Only available for premium supporters',
}, {
'url': 'https://www.onsen.ag/program/uma',
'info_dict': {
'id': 'uma',
'title': 'UMA YELL RADIO',
},
'playlist_mincount': 35,
}]
@staticmethod
def _get_encoded_id(program):
return base64.urlsafe_b64encode(str(program['id']).encode()).decode()
def _perform_login(self, username, password):
sign_in = self._download_json(
f'{self._BASE_URL}/web_api/signin', None, 'Logging in', headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
}, data=json.dumps({
'session': {
'email': username,
'password': password,
},
}).encode(), expected_status=401)
if sign_in.get('error'):
raise ExtractorError('Invalid username or password', expected=True)
def _real_extract(self, url):
program_id = self._match_id(url)
try:
programs = self._download_json(
f'{self._BASE_URL}/web_api/programs/{program_id}', program_id)
except ExtractorError as e:
if isinstance(e.cause, HTTPError) and e.cause.status == 404:
raise ExtractorError('Invalid URL', expected=True)
raise
query = {k: v[-1] for k, v in parse_qs(url).items() if v}
if 'c' not in query:
entries = [
self.url_result(update_url_query(url, {'c': self._get_encoded_id(program)}), OnsenIE)
for program in traverse_obj(programs, ('contents', lambda _, v: v['id']))
]
return self.playlist_result(
entries, program_id, traverse_obj(programs, ('program_info', 'title', {clean_html})))
raw_id = base64.urlsafe_b64decode(f'{query["c"]}===').decode()
p_keys = ('contents', lambda _, v: v['id'] == int(raw_id))
program = traverse_obj(programs, (*p_keys, any))
if not program:
raise ExtractorError(
'This program is no longer available', expected=True)
m3u8_url = traverse_obj(program, ('streaming_url', {url_or_none}))
if not m3u8_url:
self.raise_login_required(
'This program is only available for premium supporters')
display_id = self._get_encoded_id(program)
date_str = self._search_regex(
rf'{program_id}0?(\d{{6}})', m3u8_url, 'date string', default=None)
return {
'display_id': display_id,
'formats': self._extract_m3u8_formats(m3u8_url, raw_id, headers=self._HEADERS),
'http_headers': self._HEADERS,
'section_start': int_or_none(query.get('t', 0)),
'upload_date': strftime_or_none(f'20{date_str}'),
'webpage_url': f'{self._BASE_URL}/program/{program_id}?c={display_id}',
**traverse_obj(program, {
'id': ('id', {int}, {str_or_none}),
'title': ('title', {clean_html}),
'media_type': ('media_type', {str}),
'thumbnail': ('poster_image_url', {url_or_none}, {update_url(query=None)}),
}),
**traverse_obj(programs, {
'cast': (('performers', (*p_keys, 'guests')), ..., 'name', {str}, filter),
'series_id': ('directory_name', {str}),
}),
**traverse_obj(programs, ('program_info', {
'description': ('description', {clean_html}, filter),
'series': ('title', {clean_html}),
'tags': ('hashtag_list', ..., {str}, filter),
})),
}

View File

@@ -1,201 +0,0 @@
import itertools
from .cbs import CBSBaseIE
from .common import InfoExtractor
from ..utils import (
ExtractorError,
int_or_none,
url_or_none,
)
class ParamountPlusIE(CBSBaseIE):
_VALID_URL = r'''(?x)
(?:
paramountplus:|
https?://(?:www\.)?(?:
paramountplus\.com/(?:shows|movies)/(?:video|[^/]+/video|[^/]+)/
)(?P<id>[\w-]+))'''
# All tests are blocked outside US
_TESTS = [{
'url': 'https://www.paramountplus.com/shows/video/Oe44g5_NrlgiZE3aQVONleD6vXc8kP0k/',
'info_dict': {
'id': 'Oe44g5_NrlgiZE3aQVONleD6vXc8kP0k',
'ext': 'mp4',
'title': 'CatDog - Climb Every CatDog/The Canine Mutiny',
'description': 'md5:7ac835000645a69933df226940e3c859',
'duration': 1426,
'timestamp': 920264400,
'upload_date': '19990301',
'uploader': 'CBSI-NEW',
'episode_number': 5,
'thumbnail': r're:https?://.+\.jpg$',
'season': 'Season 2',
'chapters': 'count:3',
'episode': 'Episode 5',
'season_number': 2,
'series': 'CatDog',
},
'params': {
'skip_download': 'm3u8',
},
}, {
'url': 'https://www.paramountplus.com/shows/video/6hSWYWRrR9EUTz7IEe5fJKBhYvSUfexd/',
'info_dict': {
'id': '6hSWYWRrR9EUTz7IEe5fJKBhYvSUfexd',
'ext': 'mp4',
'title': '7/23/21 WEEK IN REVIEW (Rep. Jahana Hayes/Howard Fineman/Sen. Michael Bennet/Sheera Frenkel & Cecilia Kang)',
'description': 'md5:f4adcea3e8b106192022e121f1565bae',
'duration': 2506,
'timestamp': 1627063200,
'upload_date': '20210723',
'uploader': 'CBSI-NEW',
'episode_number': 81,
'thumbnail': r're:https?://.+\.jpg$',
'season': 'Season 2',
'chapters': 'count:4',
'episode': 'Episode 81',
'season_number': 2,
'series': 'Tooning Out The News',
},
'params': {
'skip_download': 'm3u8',
},
}, {
'url': 'https://www.paramountplus.com/movies/video/vM2vm0kE6vsS2U41VhMRKTOVHyQAr6pC/',
'info_dict': {
'id': 'vM2vm0kE6vsS2U41VhMRKTOVHyQAr6pC',
'ext': 'mp4',
'title': 'Daddy\'s Home',
'upload_date': '20151225',
'description': 'md5:9a6300c504d5e12000e8707f20c54745',
'uploader': 'CBSI-NEW',
'timestamp': 1451030400,
'thumbnail': r're:https?://.+\.jpg$',
'chapters': 'count:0',
'duration': 5761,
'series': 'Paramount+ Movies',
},
'params': {
'skip_download': 'm3u8',
},
'skip': 'DRM',
}, {
'url': 'https://www.paramountplus.com/movies/video/5EKDXPOzdVf9voUqW6oRuocyAEeJGbEc/',
'info_dict': {
'id': '5EKDXPOzdVf9voUqW6oRuocyAEeJGbEc',
'ext': 'mp4',
'uploader': 'CBSI-NEW',
'description': 'md5:bc7b6fea84ba631ef77a9bda9f2ff911',
'timestamp': 1577865600,
'title': 'Sonic the Hedgehog',
'upload_date': '20200101',
'thumbnail': r're:https?://.+\.jpg$',
'chapters': 'count:0',
'duration': 5932,
'series': 'Paramount+ Movies',
},
'params': {
'skip_download': 'm3u8',
},
'skip': 'DRM',
}, {
'url': 'https://www.paramountplus.com/shows/the-real-world/video/mOVeHeL9ub9yWdyzSZFYz8Uj4ZBkVzQg/the-real-world-reunion/',
'only_matching': True,
}, {
'url': 'https://www.paramountplus.com/shows/video/mOVeHeL9ub9yWdyzSZFYz8Uj4ZBkVzQg/',
'only_matching': True,
}, {
'url': 'https://www.paramountplus.com/movies/video/W0VyStQqUnqKzJkrpSAIARuCc9YuYGNy/',
'only_matching': True,
}, {
'url': 'https://www.paramountplus.com/movies/paw-patrol-the-movie/W0VyStQqUnqKzJkrpSAIARuCc9YuYGNy/',
'only_matching': True,
}]
def _extract_video_info(self, content_id, mpx_acc=2198311517):
items_data = self._download_json(
f'https://www.paramountplus.com/apps-api/v2.0/androidtv/video/cid/{content_id}.json',
content_id, query={
'locale': 'en-us',
'at': 'ABCXgPuoStiPipsK0OHVXIVh68zNys+G4f7nW9R6qH68GDOcneW6Kg89cJXGfiQCsj0=',
}, headers=self.geo_verification_headers())
asset_types = {
item.get('assetType'): {
'format': 'SMIL',
'formats': 'M3U+none,MPEG4', # '+none' specifies ProtectionScheme (no DRM)
} for item in items_data['itemList']
}
item = items_data['itemList'][-1]
info, error = {}, None
metadata = {
'title': item.get('title'),
'series': item.get('seriesTitle'),
'season_number': int_or_none(item.get('seasonNum')),
'episode_number': int_or_none(item.get('episodeNum')),
'duration': int_or_none(item.get('duration')),
'thumbnail': url_or_none(item.get('thumbnail')),
}
try:
info = self._extract_common_video_info(content_id, asset_types, mpx_acc, extra_info=metadata)
except ExtractorError as e:
error = e
# Check for DRM formats to give appropriate error
if not info.get('formats'):
for query in asset_types.values():
query['formats'] = 'MPEG-DASH,M3U,MPEG4' # allows DRM formats
try:
drm_info = self._extract_common_video_info(content_id, asset_types, mpx_acc, extra_info=metadata)
except ExtractorError:
if error:
raise error from None
raise
if drm_info['formats']:
self.report_drm(content_id)
elif error:
raise error
return info
class ParamountPlusSeriesIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?paramountplus\.com/shows/(?P<id>[a-zA-Z0-9-_]+)/?(?:[#?]|$)'
_TESTS = [{
'url': 'https://www.paramountplus.com/shows/drake-josh',
'playlist_mincount': 50,
'info_dict': {
'id': 'drake-josh',
},
}, {
'url': 'https://www.paramountplus.com/shows/hawaii_five_0/',
'playlist_mincount': 240,
'info_dict': {
'id': 'hawaii_five_0',
},
}, {
'url': 'https://www.paramountplus.com/shows/spongebob-squarepants/',
'playlist_mincount': 248,
'info_dict': {
'id': 'spongebob-squarepants',
},
}]
def _entries(self, show_name):
for page in itertools.count():
show_json = self._download_json(
f'https://www.paramountplus.com/shows/{show_name}/xhr/episodes/page/{page}/size/50/xs/0/season/0', show_name)
if not show_json.get('success'):
return
for episode in show_json['result']['data']:
yield self.url_result(
'https://www.paramountplus.com{}'.format(episode['url']),
ie=ParamountPlusIE.ie_key(), video_id=episode['content_id'])
def _real_extract(self, url):
show_name = self._match_id(url)
return self.playlist_result(self._entries(show_name), playlist_id=show_name)

View File

@@ -1,191 +0,0 @@
import re
from .common import InfoExtractor
from ..utils import ExtractorError, int_or_none, str_to_int
class RUTVIE(InfoExtractor):
IE_DESC = 'RUTV.RU'
_VALID_URL = r'''(?x)
https?://
(?:test)?player\.(?:rutv\.ru|vgtrk\.com)/
(?P<path>
flash\d+v/container\.swf\?id=|
iframe/(?P<type>swf|video|live)/id/|
index/iframe/cast_id/
)
(?P<id>\d+)
'''
_EMBED_REGEX = [
r'<iframe[^>]+?src=(["\'])(?P<url>https?://(?:test)?player\.(?:rutv\.ru|vgtrk\.com)/(?:iframe/(?:swf|video|live)/id|index/iframe/cast_id)/.+?)\1',
r'<meta[^>]+?property=(["\'])og:video\1[^>]+?content=(["\'])(?P<url>https?://(?:test)?player\.(?:rutv\.ru|vgtrk\.com)/flash\d+v/container\.swf\?id=.+?\2)',
]
_TESTS = [{
'url': 'http://player.rutv.ru/flash2v/container.swf?id=774471&sid=kultura&fbv=true&isPlay=true&ssl=false&i=560&acc_video_id=episode_id/972347/video_id/978186/brand_id/31724',
'info_dict': {
'id': '774471',
'ext': 'mp4',
'title': 'Монологи на все времена. Концерт',
'description': 'md5:18d8b5e6a41fb1faa53819471852d5d5',
'duration': 2906,
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'https://player.vgtrk.com/flash2v/container.swf?id=774016&sid=russiatv&fbv=true&isPlay=true&ssl=false&i=560&acc_video_id=episode_id/972098/video_id/977760/brand_id/57638',
'info_dict': {
'id': '774016',
'ext': 'mp4',
'title': 'Чужой в семье Сталина',
'description': '',
'duration': 2539,
},
'skip': 'Invalid URL',
}, {
'url': 'http://player.rutv.ru/iframe/swf/id/766888/sid/hitech/?acc_video_id=4000',
'info_dict': {
'id': '766888',
'ext': 'mp4',
'title': 'Вести.net: интернет-гиганты начали перетягивание программных "одеял"',
'description': 'md5:65ddd47f9830c4f42ed6475f8730c995',
'duration': 279,
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'http://player.rutv.ru/iframe/video/id/771852/start_zoom/true/showZoomBtn/false/sid/russiatv/?acc_video_id=episode_id/970443/video_id/975648/brand_id/5169',
'info_dict': {
'id': '771852',
'ext': 'mp4',
'title': 'Прямой эфир. Жертвы загадочной болезни: смерть от старости в 17 лет',
'description': 'md5:b81c8c55247a4bd996b43ce17395b2d8',
'duration': 3096,
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
},
'params': {'skip_download': 'm3u8'},
}, {
'url': 'http://player.rutv.ru/iframe/live/id/51499/showZoomBtn/false/isPlay/true/sid/sochi2014',
'info_dict': {
'id': '51499',
'ext': 'flv',
'title': 'Сочи-2014. Биатлон. Индивидуальная гонка. Мужчины ',
'description': 'md5:9e0ed5c9d2fa1efbfdfed90c9a6d179c',
},
'skip': 'Invalid URL',
}, {
'url': 'http://player.rutv.ru/iframe/live/id/21/showZoomBtn/false/isPlay/true/',
'info_dict': {
'id': '21',
'ext': 'mp4',
'title': str,
'is_live': True,
},
'skip': 'Invalid URL',
}, {
'url': 'https://testplayer.vgtrk.com/iframe/live/id/19201/showZoomBtn/false/isPlay/true/',
'only_matching': True,
}]
_WEBPAGE_TESTS = [{
'url': 'http://istoriya-teatra.ru/news/item/f00/s05/n0000545/index.shtml',
'info_dict': {
'id': '1952012',
'ext': 'mp4',
'title': 'Новости культуры. Эфир от 10.10.2019 (23:30). Театр Сатиры отмечает день рождения премьерой',
'description': 'md5:fced27112ff01ff8fc4a452fc088bad6',
'duration': 191,
'thumbnail': r're:https?://cdn-st2\.smotrim\.ru/.+\.jpg',
},
'params': {'skip_download': 'm3u8'},
}]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
video_id = mobj.group('id')
video_path = mobj.group('path')
if re.match(r'flash\d+v', video_path):
video_type = 'video'
elif video_path.startswith('iframe'):
video_type = mobj.group('type')
if video_type == 'swf':
video_type = 'video'
elif video_path.startswith('index/iframe/cast_id'):
video_type = 'live'
is_live = video_type == 'live'
json_data = self._download_json(
'http://player.vgtrk.com/iframe/data{}/id/{}'.format('live' if is_live else 'video', video_id),
video_id, 'Downloading JSON')
if json_data['errors']:
raise ExtractorError('{} said: {}'.format(self.IE_NAME, json_data['errors']), expected=True)
playlist = json_data['data']['playlist']
medialist = playlist['medialist']
media = medialist[0]
if media['errors']:
raise ExtractorError('{} said: {}'.format(self.IE_NAME, media['errors']), expected=True)
view_count = int_or_none(playlist.get('count_views'))
priority_transport = playlist['priority_transport']
thumbnail = media['picture']
width = int_or_none(media['width'])
height = int_or_none(media['height'])
description = media['anons']
title = media['title']
duration = int_or_none(media.get('duration'))
formats = []
subtitles = {}
for transport, links in media['sources'].items():
for quality, url in links.items():
preference = -1 if priority_transport == transport else -2
if transport == 'rtmp':
mobj = re.search(r'^(?P<url>rtmp://[^/]+/(?P<app>.+))/(?P<playpath>.+)$', url)
if not mobj:
continue
fmt = {
'url': mobj.group('url'),
'play_path': mobj.group('playpath'),
'app': mobj.group('app'),
'page_url': 'http://player.rutv.ru',
'player_url': 'http://player.rutv.ru/flash3v/osmf.swf?i=22',
'rtmp_live': True,
'ext': 'flv',
'vbr': str_to_int(quality),
}
elif transport == 'm3u8':
fmt, subs = self._extract_m3u8_formats_and_subtitles(
url, video_id, 'mp4', quality=preference, m3u8_id='hls')
formats.extend(fmt)
self._merge_subtitles(subs, target=subtitles)
continue
else:
fmt = {
'url': url,
}
fmt.update({
'width': int_or_none(quality, default=height, invscale=width, scale=height),
'height': int_or_none(quality, default=height),
'format_id': f'{transport}-{quality}',
'source_preference': preference,
})
formats.append(fmt)
return {
'id': video_id,
'title': title,
'description': description,
'thumbnail': thumbnail,
'view_count': view_count,
'duration': duration,
'formats': formats,
'subtitles': subtitles,
'is_live': is_live,
'_format_sort_fields': ('source', ),
}

View File

@@ -1,119 +0,0 @@
from .common import InfoExtractor
from ..utils import (
determine_ext,
int_or_none,
parse_qs,
qualities,
try_get,
)
class SixPlayIE(InfoExtractor):
IE_NAME = '6play'
_VALID_URL = r'(?:6play:|https?://(?:www\.)?(?P<domain>6play\.fr|rtlplay\.be|play\.rtl\.hr|rtlmost\.hu)/.+?-c_)(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://www.6play.fr/minute-par-minute-p_9533/le-but-qui-a-marque-lhistoire-du-football-francais-c_12041051',
'md5': '31fcd112637baa0c2ab92c4fcd8baf27',
'info_dict': {
'id': '12041051',
'ext': 'mp4',
'title': 'Le but qui a marqué l\'histoire du football français !',
'description': 'md5:b59e7e841d646ef1eb42a7868eb6a851',
},
}, {
'url': 'https://www.rtlplay.be/rtl-info-13h-p_8551/les-titres-du-rtlinfo-13h-c_12045869',
'only_matching': True,
}, {
'url': 'https://play.rtl.hr/pj-masks-p_9455/epizoda-34-sezona-1-catboyevo-cudo-na-dva-kotaca-c_11984989',
'only_matching': True,
}, {
'url': 'https://www.rtlmost.hu/megtorve-p_14167/megtorve-6-resz-c_12397787',
'only_matching': True,
}]
def _real_extract(self, url):
domain, video_id = self._match_valid_url(url).groups()
service, consumer_name = {
'6play.fr': ('6play', 'm6web'),
'rtlplay.be': ('rtlbe_rtl_play', 'rtlbe'),
'play.rtl.hr': ('rtlhr_rtl_play', 'rtlhr'),
'rtlmost.hu': ('rtlhu_rtl_most', 'rtlhu'),
}.get(domain, ('6play', 'm6web'))
data = self._download_json(
f'https://pc.middleware.6play.fr/6play/v2/platforms/m6group_web/services/{service}/videos/clip_{video_id}',
video_id, headers={
'x-customer-name': consumer_name,
}, query={
'csa': 5,
'with': 'clips',
})
clip_data = data['clips'][0]
title = clip_data['title']
urls = []
quality_key = qualities(['lq', 'sd', 'hq', 'hd'])
formats = []
subtitles = {}
assets = clip_data.get('assets') or []
for asset in assets:
asset_url = asset.get('full_physical_path')
protocol = asset.get('protocol')
if not asset_url or ((protocol == 'primetime' or asset.get('type') == 'usp_hlsfp_h264') and not ('_drmnp.ism/' in asset_url or '_unpnp.ism/' in asset_url)) or asset_url in urls:
continue
urls.append(asset_url)
container = asset.get('video_container')
ext = determine_ext(asset_url)
if protocol == 'http_subtitle' or ext == 'vtt':
subtitles.setdefault('fr', []).append({'url': asset_url})
continue
if container == 'm3u8' or ext == 'm3u8':
if protocol == 'usp':
if parse_qs(asset_url).get('token', [None])[0]:
urlh = self._request_webpage(
asset_url, video_id, fatal=False,
headers=self.geo_verification_headers())
if not urlh:
continue
asset_url = urlh.url
asset_url = asset_url.replace('_drmnp.ism/', '_unpnp.ism/')
for i in range(3, 0, -1):
asset_url = asset_url.replace('_sd1/', f'_sd{i}/')
m3u8_formats = self._extract_m3u8_formats(
asset_url, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False)
formats.extend(m3u8_formats)
formats.extend(self._extract_mpd_formats(
asset_url.replace('.m3u8', '.mpd'),
video_id, mpd_id='dash', fatal=False))
if m3u8_formats:
break
else:
formats.extend(self._extract_m3u8_formats(
asset_url, video_id, 'mp4', 'm3u8_native',
m3u8_id='hls', fatal=False))
elif container == 'mp4' or ext == 'mp4':
quality = asset.get('video_quality')
formats.append({
'url': asset_url,
'format_id': quality,
'quality': quality_key(quality),
'ext': ext,
})
def get(getter):
for src in (data, clip_data):
v = try_get(src, getter, str)
if v:
return v
return {
'id': video_id,
'title': title,
'description': get(lambda x: x['description']),
'duration': int_or_none(clip_data.get('duration')),
'series': get(lambda x: x['program']['title']),
'formats': formats,
'subtitles': subtitles,
}

View File

@@ -1,65 +1,403 @@
import functools
import json
import re
import urllib.parse
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ExtractorError from ..utils import (
OnDemandPagedList,
clean_html,
determine_ext,
extract_attributes,
int_or_none,
parse_iso8601,
str_or_none,
unescapeHTML,
url_or_none,
urljoin,
)
from ..utils.traversal import (
find_element,
find_elements,
require,
traverse_obj,
)
class SmotrimIE(InfoExtractor): class SmotrimBaseIE(InfoExtractor):
_VALID_URL = r'https?://smotrim\.ru/(?P<type>brand|video|article|live)/(?P<id>[0-9]+)' _BASE_URL = 'https://smotrim.ru'
_TESTS = [{ # video _GEO_BYPASS = False
_GEO_COUNTRIES = ['RU']
def _extract_from_smotrim_api(self, typ, item_id):
path = f'data{typ.replace("-", "")}/{"uid" if typ == "live" else "id"}'
data = self._download_json(
f'https://player.smotrim.ru/iframe/{path}/{item_id}/sid/smotrim', item_id)
media = traverse_obj(data, ('data', 'playlist', 'medialist', -1, {dict}))
if traverse_obj(media, ('locked', {bool})):
self.raise_login_required()
if error_msg := traverse_obj(media, ('errors', {clean_html})):
self.raise_geo_restricted(error_msg, countries=self._GEO_COUNTRIES)
webpage_url = traverse_obj(data, ('data', 'template', 'share_url', {url_or_none}))
webpage = self._download_webpage(webpage_url, item_id)
common = {
'thumbnail': self._html_search_meta(['og:image', 'twitter:image'], webpage, default=None),
**traverse_obj(media, {
'id': ('id', {str_or_none}),
'title': (('episodeTitle', 'title'), {clean_html}, filter, any),
'channel_id': ('channelId', {str_or_none}),
'description': ('anons', {clean_html}, filter),
'season': ('season', {clean_html}, filter),
'series': (('brand_title', 'brandTitle'), {clean_html}, filter, any),
'series_id': ('brand_id', {str_or_none}),
}),
}
if typ == 'audio':
bookmark = self._search_json(
r'class="bookmark"[^>]+value\s*=\s*"', webpage,
'bookmark', item_id, default={}, transform_source=unescapeHTML)
metadata = {
'vcodec': 'none',
**common,
**traverse_obj(media, {
'ext': ('audio_url', {determine_ext(default_ext='mp3')}),
'duration': ('duration', {int_or_none}),
'url': ('audio_url', {url_or_none}),
}),
**traverse_obj(bookmark, {
'title': ('subtitle', {clean_html}),
'timestamp': ('published', {parse_iso8601}),
}),
}
elif typ == 'audio-live':
metadata = {
'ext': 'mp3',
'url': traverse_obj(media, ('source', 'auto', {url_or_none})),
'vcodec': 'none',
**common,
}
else:
formats, subtitles = [], {}
for m3u8_url in traverse_obj(media, (
'sources', 'm3u8', {dict.values}, ..., {url_or_none},
)):
fmts, subs = self._extract_m3u8_formats_and_subtitles(
m3u8_url, item_id, 'mp4', m3u8_id='hls', fatal=False)
formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles)
metadata = {
'formats': formats,
'subtitles': subtitles,
**self._search_json_ld(webpage, item_id),
**common,
}
return {
'age_limit': traverse_obj(data, ('data', 'age_restrictions', {int_or_none})),
'is_live': typ in ('audio-live', 'live'),
'tags': traverse_obj(webpage, (
{find_elements(cls='tags-list__link')}, ..., {clean_html}, filter, all, filter)),
'webpage_url': webpage_url,
**metadata,
}
class SmotrimIE(SmotrimBaseIE):
IE_NAME = 'smotrim'
_VALID_URL = r'(?:https?:)?//(?:(?:player|www)\.)?smotrim\.ru(?:/iframe)?/video(?:/id)?/(?P<id>\d+)'
_EMBED_REGEX = [fr'<iframe\b[^>]+\bsrc=["\'](?P<url>{_VALID_URL})']
_TESTS = [{
'url': 'https://smotrim.ru/video/1539617', 'url': 'https://smotrim.ru/video/1539617',
'md5': 'b1923a533c8cab09679789d720d0b1c5',
'info_dict': { 'info_dict': {
'id': '1539617', 'id': '1539617',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Полиглот. Китайский с нуля за 16 часов! Урок №16', 'title': 'Урок №16',
'description': '', 'duration': 2631,
'series': 'Полиглот. Китайский с нуля за 16 часов!',
'series_id': '60562',
'tags': 'mincount:6',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1466771100,
'upload_date': '20160624',
'view_count': int,
}, },
'add_ie': ['RUTV'], }, {
}, { # article (geo-restricted? plays fine from the US and JP) 'url': 'https://player.smotrim.ru/iframe/video/id/2988590',
'info_dict': {
'id': '2988590',
'ext': 'mp4',
'title': 'Трейлер',
'age_limit': 16,
'description': 'md5:6af7e68ecf4ed7b8ff6720d20c4da47b',
'duration': 30,
'series': 'Мы в разводе',
'series_id': '71624',
'tags': 'mincount:5',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1750670040,
'upload_date': '20250623',
'view_count': int,
'webpage_url': 'https://smotrim.ru/video/2988590',
},
}]
_WEBPAGE_TESTS = [{
'url': 'https://smotrim.ru/article/2813445', 'url': 'https://smotrim.ru/article/2813445',
'md5': 'e0ac453952afbc6a2742e850b4dc8e77',
'info_dict': { 'info_dict': {
'id': '2431846', 'id': '2431846',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Новости культуры. Съёмки первой программы "Большие и маленькие"', 'title': 'Съёмки первой программы "Большие и маленькие"',
'description': 'md5:94a4a22472da4252bf5587a4ee441b99', 'description': 'md5:446c9a5d334b995152a813946353f447',
'duration': 240,
'series': 'Новости культуры',
'series_id': '19725',
'tags': 'mincount:6',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1656054443,
'upload_date': '20220624',
'view_count': int,
'webpage_url': 'https://smotrim.ru/video/2431846',
}, },
'add_ie': ['RUTV'], }, {
}, { # brand, redirect 'url': 'https://www.vesti.ru/article/4642878',
'url': 'https://smotrim.ru/brand/64356',
'md5': '740472999ccff81d7f6df79cecd91c18',
'info_dict': { 'info_dict': {
'id': '2354523', 'id': '3007209',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Большие и маленькие. Лучшее. 4-й выпуск', 'title': 'Иностранные мессенджеры используют не только мошенники, но и вербовщики',
'description': 'md5:84089e834429008371ea41ea3507b989', 'description': 'md5:74ab625a0a89b87b2e0ed98d6391b182',
'duration': 265,
'series': 'Вести. Дежурная часть',
'series_id': '5204',
'tags': 'mincount:6',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1754756280,
'upload_date': '20250809',
'view_count': int,
'webpage_url': 'https://smotrim.ru/video/3007209',
}, },
'add_ie': ['RUTV'],
}, { # live
'url': 'https://smotrim.ru/live/19201',
'info_dict': {
'id': '19201',
'ext': 'mp4',
# this looks like a TV channel name
'title': 'Россия Культура. Прямой эфир',
'description': '',
},
'add_ie': ['RUTV'],
}] }]
def _real_extract(self, url): def _real_extract(self, url):
video_id, typ = self._match_valid_url(url).group('id', 'type') video_id = self._match_id(url)
rutv_type = 'video'
if typ not in ('video', 'live'):
webpage = self._download_webpage(url, video_id, f'Resolving {typ} link')
# there are two cases matching regex:
# 1. "embedUrl" in JSON LD (/brand/)
# 2. "src" attribute from iframe (/article/)
video_id = self._search_regex(
r'"https://player.smotrim.ru/iframe/video/id/(?P<video_id>\d+)/',
webpage, 'video_id', default=None)
if not video_id:
raise ExtractorError('There are no video in this page.', expected=True)
elif typ == 'live':
rutv_type = 'live'
return self.url_result(f'https://player.vgtrk.com/iframe/{rutv_type}/id/{video_id}') return self._extract_from_smotrim_api('video', video_id)
class SmotrimAudioIE(SmotrimBaseIE):
IE_NAME = 'smotrim:audio'
_VALID_URL = r'https?://(?:(?:player|www)\.)?smotrim\.ru(?:/iframe)?/audio(?:/id)?/(?P<id>\d+)'
_TESTS = [{
'url': 'https://smotrim.ru/audio/2573986',
'md5': 'e28d94c20da524e242b2d00caef41a8e',
'info_dict': {
'id': '2573986',
'ext': 'mp3',
'title': 'Радиоспектакль',
'description': 'md5:4bcaaf7d532bc78f76e478fad944e388',
'duration': 3072,
'series': 'Морис Леблан. Арсен Люпен, джентльмен-грабитель',
'series_id': '66461',
'tags': 'mincount:7',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1624884358,
'upload_date': '20210628',
},
}, {
'url': 'https://player.smotrim.ru/iframe/audio/id/2860468',
'md5': '5a6bc1fa24c7142958be1ad9cfae58a8',
'info_dict': {
'id': '2860468',
'ext': 'mp3',
'title': 'Колобок и музыкальная игра "Терем-теремок"',
'duration': 1501,
'series': 'Веселый колобок',
'series_id': '68880',
'tags': 'mincount:4',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': 1755925800,
'upload_date': '20250823',
'webpage_url': 'https://smotrim.ru/audio/2860468',
},
}]
def _real_extract(self, url):
audio_id = self._match_id(url)
return self._extract_from_smotrim_api('audio', audio_id)
class SmotrimLiveIE(SmotrimBaseIE):
IE_NAME = 'smotrim:live'
_VALID_URL = r'''(?x:
(?:https?:)?//
(?:(?:(?:test)?player|www)\.)?
(?:
smotrim\.ru|
vgtrk\.com
)
(?:/iframe)?/
(?P<type>
channel|
(?:audio-)?live
)
(?:/u?id)?/(?P<id>[\da-f-]+)
)'''
_EMBED_REGEX = [fr'<iframe\b[^>]+\bsrc=["\'](?P<url>{_VALID_URL})']
_TESTS = [{
'url': 'https://smotrim.ru/channel/76',
'info_dict': {
'id': '1661',
'ext': 'mp4',
'title': str,
'channel_id': '76',
'description': 'Смотрим прямой эфир «Москва 24»',
'display_id': '76',
'live_status': 'is_live',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': int,
'upload_date': str,
},
'params': {'skip_download': 'Livestream'},
}, {
# Radio
'url': 'https://smotrim.ru/channel/81',
'info_dict': {
'id': '81',
'ext': 'mp3',
'title': str,
'channel_id': '81',
'live_status': 'is_live',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
},
'params': {'skip_download': 'Livestream'},
}, {
# Sometimes geo-restricted to Russia
'url': 'https://player.smotrim.ru/iframe/live/uid/381308c7-a066-4c4f-9656-83e2e792a7b4',
'info_dict': {
'id': '19201',
'ext': 'mp4',
'title': str,
'channel_id': '4',
'description': 'Смотрим прямой эфир «Россия К»',
'display_id': '381308c7-a066-4c4f-9656-83e2e792a7b4',
'live_status': 'is_live',
'thumbnail': r're:https?://cdn-st\d+\.smotrim\.ru/.+\.(?:jpg|png)',
'timestamp': int,
'upload_date': str,
'webpage_url': 'https://smotrim.ru/channel/4',
},
'params': {'skip_download': 'Livestream'},
}, {
'url': 'https://smotrim.ru/live/19201',
'only_matching': True,
}, {
'url': 'https://player.smotrim.ru/iframe/audio-live/id/81',
'only_matching': True,
}, {
'url': 'https://testplayer.vgtrk.com/iframe/live/id/19201',
'only_matching': True,
}]
def _real_extract(self, url):
typ, display_id = self._match_valid_url(url).group('type', 'id')
if typ == 'live' and re.fullmatch(r'[0-9]+', display_id):
url = self._request_webpage(url, display_id).url
typ = self._match_valid_url(url).group('type')
if typ == 'channel':
webpage = self._download_webpage(url, display_id)
src_url = traverse_obj(webpage, ((
({find_element(cls='main-player__frame', html=True)}, {extract_attributes}, 'src'),
({find_element(cls='audio-play-button', html=True)},
{extract_attributes}, 'value', {urllib.parse.unquote}, {json.loads}, 'source'),
), any, {self._proto_relative_url}, {url_or_none}, {require('src URL')}))
typ, video_id = self._match_valid_url(src_url).group('type', 'id')
else:
video_id = display_id
return {
'display_id': display_id,
**self._extract_from_smotrim_api(typ, video_id),
}
class SmotrimPlaylistIE(SmotrimBaseIE):
IE_NAME = 'smotrim:playlist'
_PAGE_SIZE = 15
_VALID_URL = r'https?://smotrim\.ru/(?P<type>brand|podcast)/(?P<id>\d+)/?(?P<season>[\w-]+)?'
_TESTS = [{
# Video
'url': 'https://smotrim.ru/brand/64356',
'info_dict': {
'id': '64356',
'title': 'Большие и маленькие',
},
'playlist_mincount': 55,
}, {
# Video, season
'url': 'https://smotrim.ru/brand/65293/3-sezon',
'info_dict': {
'id': '65293',
'title': 'Спасская',
'season': '3 сезон',
},
'playlist_count': 16,
}, {
# Audio
'url': 'https://smotrim.ru/brand/68880',
'info_dict': {
'id': '68880',
'title': 'Веселый колобок',
},
'playlist_mincount': 156,
}, {
# Podcast
'url': 'https://smotrim.ru/podcast/8021',
'info_dict': {
'id': '8021',
'title': 'Сила звука',
},
'playlist_mincount': 27,
}]
def _fetch_page(self, endpoint, key, playlist_id, page):
page += 1
items = self._download_json(
f'{self._BASE_URL}/api/{endpoint}', playlist_id,
f'Downloading page {page}', query={
key: playlist_id,
'limit': self._PAGE_SIZE,
'page': page,
},
)
for link in traverse_obj(items, ('contents', -1, 'list', ..., 'link', {str})):
yield self.url_result(urljoin(self._BASE_URL, link))
def _real_extract(self, url):
playlist_type, playlist_id, season = self._match_valid_url(url).group('type', 'id', 'season')
key = 'rubricId' if playlist_type == 'podcast' else 'brandId'
webpage = self._download_webpage(url, playlist_id)
playlist_title = self._html_search_meta(['og:title', 'twitter:title'], webpage, default=None)
if season:
return self.playlist_from_matches(traverse_obj(webpage, (
{find_elements(tag='a', attr='href', value=r'/video/\d+', html=True, regex=True)},
..., {extract_attributes}, 'href', {str},
)), playlist_id, playlist_title, season=traverse_obj(webpage, (
{find_element(cls='seasons__item seasons__item--selected')}, {clean_html},
)), ie=SmotrimIE, getter=urljoin(self._BASE_URL))
if traverse_obj(webpage, (
{find_element(cls='brand-main-item__videos')}, {clean_html}, filter,
)):
endpoint = 'videos'
else:
endpoint = 'audios'
return self.playlist_result(OnDemandPagedList(
functools.partial(self._fetch_page, endpoint, key, playlist_id), self._PAGE_SIZE), playlist_id, playlist_title)

View File

@@ -1,167 +0,0 @@
import functools
import json
import re
from .common import InfoExtractor
from ..utils import (
OnDemandPagedList,
clean_podcast_url,
float_or_none,
int_or_none,
strip_or_none,
traverse_obj,
try_get,
unified_strdate,
)
class SpotifyBaseIE(InfoExtractor):
_WORKING = False
_ACCESS_TOKEN = None
_OPERATION_HASHES = {
'Episode': '8276d4423d709ae9b68ec1b74cc047ba0f7479059a37820be730f125189ac2bf',
'MinimalShow': '13ee079672fad3f858ea45a55eb109553b4fb0969ed793185b2e34cbb6ee7cc0',
'ShowEpisodes': 'e0e5ce27bd7748d2c59b4d44ba245a8992a05be75d6fabc3b20753fc8857444d',
}
_VALID_URL_TEMPL = r'https?://open\.spotify\.com/(?:embed-podcast/|embed/|)%s/(?P<id>[^/?&#]+)'
_EMBED_REGEX = [r'<iframe[^>]+src="(?P<url>https?://open\.spotify.com/embed/[^"]+)"']
def _real_initialize(self):
self._ACCESS_TOKEN = self._download_json(
'https://open.spotify.com/get_access_token', None)['accessToken']
def _call_api(self, operation, video_id, variables, **kwargs):
return self._download_json(
'https://api-partner.spotify.com/pathfinder/v1/query', video_id, query={
'operationName': 'query' + operation,
'variables': json.dumps(variables),
'extensions': json.dumps({
'persistedQuery': {
'sha256Hash': self._OPERATION_HASHES[operation],
},
}),
}, headers={'authorization': 'Bearer ' + self._ACCESS_TOKEN},
**kwargs)['data']
def _extract_episode(self, episode, series):
episode_id = episode['id']
title = episode['name'].strip()
formats = []
audio_preview = episode.get('audioPreview') or {}
audio_preview_url = audio_preview.get('url')
if audio_preview_url:
f = {
'url': audio_preview_url.replace('://p.scdn.co/mp3-preview/', '://anon-podcast.scdn.co/'),
'vcodec': 'none',
}
audio_preview_format = audio_preview.get('format')
if audio_preview_format:
f['format_id'] = audio_preview_format
mobj = re.match(r'([0-9A-Z]{3})_(?:[A-Z]+_)?(\d+)', audio_preview_format)
if mobj:
f.update({
'abr': int(mobj.group(2)),
'ext': mobj.group(1).lower(),
})
formats.append(f)
for item in (try_get(episode, lambda x: x['audio']['items']) or []):
item_url = item.get('url')
if not (item_url and item.get('externallyHosted')):
continue
formats.append({
'url': clean_podcast_url(item_url),
'vcodec': 'none',
})
thumbnails = []
for source in (try_get(episode, lambda x: x['coverArt']['sources']) or []):
source_url = source.get('url')
if not source_url:
continue
thumbnails.append({
'url': source_url,
'width': int_or_none(source.get('width')),
'height': int_or_none(source.get('height')),
})
return {
'id': episode_id,
'title': title,
'formats': formats,
'thumbnails': thumbnails,
'description': strip_or_none(episode.get('description')),
'duration': float_or_none(try_get(
episode, lambda x: x['duration']['totalMilliseconds']), 1000),
'release_date': unified_strdate(try_get(
episode, lambda x: x['releaseDate']['isoString'])),
'series': series,
}
class SpotifyIE(SpotifyBaseIE):
IE_NAME = 'spotify'
IE_DESC = 'Spotify episodes'
_VALID_URL = SpotifyBaseIE._VALID_URL_TEMPL % 'episode'
_TESTS = [{
'url': 'https://open.spotify.com/episode/4Z7GAJ50bgctf6uclHlWKo',
'md5': '74010a1e3fa4d9e1ab3aa7ad14e42d3b',
'info_dict': {
'id': '4Z7GAJ50bgctf6uclHlWKo',
'ext': 'mp3',
'title': 'From the archive: Why time management is ruining our lives',
'description': 'md5:b120d9c4ff4135b42aa9b6d9cde86935',
'duration': 2083.605,
'release_date': '20201217',
'series': "The Guardian's Audio Long Reads",
},
}, {
'url': 'https://open.spotify.com/embed/episode/4TvCsKKs2thXmarHigWvXE?si=7eatS8AbQb6RxqO2raIuWA',
'only_matching': True,
}]
def _real_extract(self, url):
episode_id = self._match_id(url)
episode = self._call_api('Episode', episode_id, {
'uri': 'spotify:episode:' + episode_id,
})['episode']
return self._extract_episode(
episode, try_get(episode, lambda x: x['podcast']['name']))
class SpotifyShowIE(SpotifyBaseIE):
IE_NAME = 'spotify:show'
IE_DESC = 'Spotify shows'
_VALID_URL = SpotifyBaseIE._VALID_URL_TEMPL % 'show'
_TEST = {
'url': 'https://open.spotify.com/show/4PM9Ke6l66IRNpottHKV9M',
'info_dict': {
'id': '4PM9Ke6l66IRNpottHKV9M',
'title': 'The Story from the Guardian',
'description': 'The Story podcast is dedicated to our finest audio documentaries, investigations and long form stories',
},
'playlist_mincount': 36,
}
_PER_PAGE = 100
def _fetch_page(self, show_id, page=0):
return self._call_api('ShowEpisodes', show_id, {
'limit': 100,
'offset': page * self._PER_PAGE,
'uri': f'spotify:show:{show_id}',
}, note=f'Downloading page {page + 1} JSON metadata')['podcast']
def _real_extract(self, url):
show_id = self._match_id(url)
first_page = self._fetch_page(show_id)
def _entries(page):
podcast = self._fetch_page(show_id, page) if page else first_page
yield from map(
functools.partial(self._extract_episode, series=podcast.get('name')),
traverse_obj(podcast, ('episodes', 'items', ..., 'episode')))
return self.playlist_result(
OnDemandPagedList(_entries, self._PER_PAGE),
show_id, first_page.get('name'), first_page.get('description'))

View File

@@ -1,244 +1,335 @@
import functools
import urllib.parse import urllib.parse
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
OnDemandPagedList, OnDemandPagedList,
determine_ext, UnsupportedError,
clean_html,
int_or_none,
join_nonempty,
parse_iso8601, parse_iso8601,
traverse_obj, update_url_query,
url_or_none,
) )
from ..utils.traversal import traverse_obj
class TuneInBaseIE(InfoExtractor): class TuneInBaseIE(InfoExtractor):
_VALID_URL_BASE = r'https?://(?:www\.)?tunein\.com' def _call_api(self, item_id, endpoint=None, note='Downloading JSON metadata', fatal=False, query=None):
return self._download_json(
def _extract_metadata(self, webpage, content_id): join_nonempty('https://api.tunein.com/profiles', item_id, endpoint, delim='/'),
return self._search_json(r'window.INITIAL_STATE=', webpage, 'hydration', content_id, fatal=False) item_id, note=note, fatal=fatal, query=query) or {}
def _extract_formats_and_subtitles(self, content_id): def _extract_formats_and_subtitles(self, content_id):
streams = self._download_json( streams = self._download_json(
f'https://opml.radiotime.com/Tune.ashx?render=json&formats=mp3,aac,ogg,flash,hls&id={content_id}', 'https://opml.radiotime.com/Tune.ashx', content_id, query={
content_id)['body'] 'formats': 'mp3,aac,ogg,flash,hls',
'id': content_id,
'render': 'json',
})
formats, subtitles = [], {} formats, subtitles = [], {}
for stream in streams: for stream in traverse_obj(streams, ('body', lambda _, v: url_or_none(v['url']))):
if stream.get('media_type') == 'hls': if stream.get('media_type') == 'hls':
fmts, subs = self._extract_m3u8_formats_and_subtitles(stream['url'], content_id, fatal=False) fmts, subs = self._extract_m3u8_formats_and_subtitles(stream['url'], content_id, fatal=False)
formats.extend(fmts) formats.extend(fmts)
self._merge_subtitles(subs, target=subtitles) self._merge_subtitles(subs, target=subtitles)
elif determine_ext(stream['url']) == 'pls':
playlist_content = self._download_webpage(stream['url'], content_id)
formats.append({
'url': self._search_regex(r'File1=(.*)', playlist_content, 'url', fatal=False),
'abr': stream.get('bitrate'),
'ext': stream.get('media_type'),
})
else: else:
formats.append({ formats.append(traverse_obj(stream, {
'url': stream['url'], 'abr': ('bitrate', {int_or_none}),
'abr': stream.get('bitrate'), 'ext': ('media_type', {str}),
'ext': stream.get('media_type'), 'url': ('url', {self._proto_relative_url}),
}) }))
return formats, subtitles return formats, subtitles
class TuneInStationIE(TuneInBaseIE): class TuneInStationIE(TuneInBaseIE):
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'(?:/radio/[^?#]+-|/embed/player/)(?P<id>s\d+)' IE_NAME = 'tunein:station'
_EMBED_REGEX = [r'<iframe[^>]+src=["\'](?P<url>(?:https?://)?tunein\.com/embed/player/s\d+)'] _VALID_URL = r'https?://tunein\.com/radio/[^/?#]+(?P<id>s\d+)'
_TESTS = [{ _TESTS = [{
'url': 'https://tunein.com/radio/Jazz24-885-s34682/', 'url': 'https://tunein.com/radio/Jazz24-885-s34682/',
'info_dict': { 'info_dict': {
'id': 's34682', 'id': 's34682',
'title': str,
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
'location': 'Seattle-Tacoma, US',
'ext': 'mp3', 'ext': 'mp3',
'title': str,
'alt_title': 'World Class Jazz',
'channel_follower_count': int,
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
'location': r're:Seattle-Tacoma, (?:US|WA)',
'live_status': 'is_live', 'live_status': 'is_live',
'thumbnail': r're:https?://.+',
}, },
'params': { 'params': {'skip_download': 'Livestream'},
'skip_download': True,
},
}, {
'url': 'https://tunein.com/embed/player/s6404/',
'only_matching': True,
}, { }, {
'url': 'https://tunein.com/radio/BBC-Radio-1-988-s24939/', 'url': 'https://tunein.com/radio/BBC-Radio-1-988-s24939/',
'info_dict': { 'info_dict': {
'id': 's24939', 'id': 's24939',
'title': str,
'description': 'md5:ee2c56794844610d045f8caf5ff34d0c',
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
'location': 'London, UK',
'ext': 'm4a', 'ext': 'm4a',
'title': str,
'alt_title': 'The biggest new pop and all-day vibes',
'channel_follower_count': int,
'description': 'md5:ee2c56794844610d045f8caf5ff34d0c',
'location': 'London, UK',
'live_status': 'is_live', 'live_status': 'is_live',
'thumbnail': r're:https?://.+',
}, },
'params': { 'params': {'skip_download': 'Livestream'},
'skip_download': True, }]
def _real_extract(self, url):
station_id = self._match_id(url)
formats, subtitles = self._extract_formats_and_subtitles(station_id)
return {
'id': station_id,
'formats': formats,
'subtitles': subtitles,
**traverse_obj(self._call_api(station_id), ('Item', {
'title': ('Title', {clean_html}),
'alt_title': ('Subtitle', {clean_html}, filter),
'channel_follower_count': ('Actions', 'Follow', 'FollowerCount', {int_or_none}),
'description': ('Description', {clean_html}, filter),
'is_live': ('Actions', 'Play', 'IsLive', {bool}),
'location': ('Properties', 'Location', 'DisplayName', {str}),
'thumbnail': ('Image', {url_or_none}),
})),
}
class TuneInPodcastIE(TuneInBaseIE):
IE_NAME = 'tunein:podcast:program'
_PAGE_SIZE = 20
_VALID_URL = r'https?://tunein\.com/podcasts(?:/[^/?#]+){1,2}(?P<id>p\d+)'
_TESTS = [{
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019/',
'info_dict': {
'id': 'p1153019',
'title': 'Lex Fridman Podcast',
}, },
'playlist_mincount': 200,
}, {
'url': 'https://tunein.com/podcasts/World-News/BBC-News-p14/',
'info_dict': {
'id': 'p14',
'title': 'BBC News',
},
'playlist_mincount': 35,
}]
@classmethod
def suitable(cls, url):
return False if TuneInPodcastEpisodeIE.suitable(url) else super().suitable(url)
def _fetch_page(self, url, podcast_id, page=0):
items = self._call_api(
podcast_id, 'contents', f'Downloading page {page + 1}', query={
'filter': 't:free',
'limit': self._PAGE_SIZE,
'offset': page * self._PAGE_SIZE,
},
)['Items']
for item in traverse_obj(items, (..., 'GuideId', {str}, filter)):
yield self.url_result(update_url_query(url, {'topicId': item[1:]}))
def _real_extract(self, url):
podcast_id = self._match_id(url)
return self.playlist_result(OnDemandPagedList(
functools.partial(self._fetch_page, url, podcast_id), self._PAGE_SIZE),
podcast_id, traverse_obj(self._call_api(podcast_id), ('Item', 'Title', {str})))
class TuneInPodcastEpisodeIE(TuneInBaseIE):
IE_NAME = 'tunein:podcast'
_VALID_URL = r'https?://tunein\.com/podcasts(?:/[^/?#]+){1,2}(?P<series_id>p\d+)/?\?(?:[^#]+&)?(?i:topicid)=(?P<id>\d+)'
_TESTS = [{
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019/?topicId=236404354',
'info_dict': {
'id': 't236404354',
'ext': 'mp3',
'title': '#351 MrBeast: Future of YouTube, Twitter, TikTok, and Instagram',
'alt_title': 'Technology Podcasts >',
'cast': 'count:1',
'description': 'md5:1029895354ef073ff00f20b82eb6eb71',
'display_id': '236404354',
'duration': 8330,
'thumbnail': r're:https?://.+',
'timestamp': 1673458571,
'upload_date': '20230111',
'series': 'Lex Fridman Podcast',
'series_id': 'p1153019',
},
}, {
'url': 'https://tunein.com/podcasts/The-BOB--TOM-Show-Free-Podcast-p20069/?topicId=174556405',
'info_dict': {
'id': 't174556405',
'ext': 'mp3',
'title': 'B&T Extra: Ohhh Yeah, It\'s Sexy Time',
'alt_title': 'Westwood One >',
'cast': 'count:2',
'description': 'md5:6828234f410ab88c85655495c5fcfa88',
'display_id': '174556405',
'duration': 1203,
'series': 'The BOB & TOM Show Free Podcast',
'series_id': 'p20069',
'thumbnail': r're:https?://.+',
'timestamp': 1661799600,
'upload_date': '20220829',
},
}]
def _real_extract(self, url):
series_id, display_id = self._match_valid_url(url).group('series_id', 'id')
episode_id = f't{display_id}'
formats, subtitles = self._extract_formats_and_subtitles(episode_id)
return {
'id': episode_id,
'display_id': display_id,
'formats': formats,
'series': traverse_obj(self._call_api(series_id), ('Item', 'Title', {clean_html})),
'series_id': series_id,
'subtitles': subtitles,
**traverse_obj(self._call_api(episode_id), ('Item', {
'title': ('Title', {clean_html}),
'alt_title': ('Subtitle', {clean_html}, filter),
'cast': (
'Properties', 'ParentProgram', 'Hosts', {clean_html},
{lambda x: x.split(';')}, ..., {str.strip}, filter, all, filter),
'description': ('Description', {clean_html}, filter),
'duration': ('Actions', 'Play', 'Duration', {int_or_none}),
'thumbnail': ('Image', {url_or_none}),
'timestamp': ('Actions', 'Play', 'PublishTime', {parse_iso8601}),
})),
}
class TuneInEmbedIE(TuneInBaseIE):
IE_NAME = 'tunein:embed'
_VALID_URL = r'https?://tunein\.com/embed/player/(?P<id>[^/?#]+)'
_EMBED_REGEX = [r'<iframe\b[^>]+\bsrc=["\'](?P<url>(?:https?:)?//tunein\.com/embed/player/[^/?#"\']+)']
_TESTS = [{
'url': 'https://tunein.com/embed/player/s6404/',
'info_dict': {
'id': 's6404',
'ext': 'mp3',
'title': str,
'alt_title': 'South Africa\'s News and Information Leader',
'channel_follower_count': int,
'live_status': 'is_live',
'location': 'Johannesburg, South Africa',
'thumbnail': r're:https?://.+',
},
'params': {'skip_download': 'Livestream'},
}, {
'url': 'https://tunein.com/embed/player/t236404354/',
'info_dict': {
'id': 't236404354',
'ext': 'mp3',
'title': '#351 MrBeast: Future of YouTube, Twitter, TikTok, and Instagram',
'alt_title': 'Technology Podcasts >',
'cast': 'count:1',
'description': 'md5:1029895354ef073ff00f20b82eb6eb71',
'display_id': '236404354',
'duration': 8330,
'series': 'Lex Fridman Podcast',
'series_id': 'p1153019',
'thumbnail': r're:https?://.+',
'timestamp': 1673458571,
'upload_date': '20230111',
},
}, {
'url': 'https://tunein.com/embed/player/p191660/',
'info_dict': {
'id': 'p191660',
'title': 'SBS Tamil',
},
'playlist_mincount': 195,
}] }]
_WEBPAGE_TESTS = [{ _WEBPAGE_TESTS = [{
'url': 'https://www.martiniinthemorning.com/', 'url': 'https://www.martiniinthemorning.com/',
'info_dict': { 'info_dict': {
'id': 's55412', 'id': 's55412',
'ext': 'mp3', 'ext': 'mp3',
'title': 'TuneInStation video #s55412', 'title': str,
'alt_title': 'Now that\'s music!',
'channel_follower_count': int,
'description': 'md5:41588a3e2cf34b3eafc6c33522fa611a',
'live_status': 'is_live',
'location': 'US',
'thumbnail': r're:https?://.+',
}, },
'expected_warnings': ['unable to extract hydration', 'Extractor failed to obtain "title"'], 'params': {'skip_download': 'Livestream'},
}] }]
def _real_extract(self, url): def _real_extract(self, url):
station_id = self._match_id(url) embed_id = self._match_id(url)
kind = {
'p': 'program',
's': 'station',
't': 'topic',
}.get(embed_id[:1])
webpage = self._download_webpage(url, station_id) return self.url_result(
metadata = self._extract_metadata(webpage, station_id) f'https://tunein.com/{kind}/?{kind}id={embed_id[1:]}')
formats, subtitles = self._extract_formats_and_subtitles(station_id)
return {
'id': station_id,
'title': traverse_obj(metadata, ('profiles', station_id, 'title')),
'description': traverse_obj(metadata, ('profiles', station_id, 'description')),
'thumbnail': traverse_obj(metadata, ('profiles', station_id, 'image')),
'timestamp': parse_iso8601(
traverse_obj(metadata, ('profiles', station_id, 'actions', 'play', 'publishTime'))),
'location': traverse_obj(
metadata, ('profiles', station_id, 'metadata', 'properties', 'location', 'displayName'),
('profiles', station_id, 'properties', 'location', 'displayName')),
'formats': formats,
'subtitles': subtitles,
'is_live': traverse_obj(metadata, ('profiles', station_id, 'actions', 'play', 'isLive')),
}
class TuneInPodcastIE(TuneInBaseIE):
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'/(?:podcasts/[^?#]+-|embed/player/)(?P<id>p\d+)/?(?:#|$)'
_EMBED_REGEX = [r'<iframe[^>]+src=["\'](?P<url>(?:https?://)?tunein\.com/embed/player/p\d+)']
_TESTS = [{
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019',
'info_dict': {
'id': 'p1153019',
'title': 'Lex Fridman Podcast',
'description': 'md5:bedc4e5f1c94f7dec6e4317b5654b00d',
},
'playlist_mincount': 200,
}, {
'url': 'https://tunein.com/embed/player/p191660/',
'only_matching': True,
}, {
'url': 'https://tunein.com/podcasts/World-News/BBC-News-p14/',
'info_dict': {
'id': 'p14',
'title': 'BBC News',
'description': 'md5:30b9622bcc4bd101d4acd6f38f284aed',
},
'playlist_mincount': 36,
}]
_PAGE_SIZE = 30
def _real_extract(self, url):
podcast_id = self._match_id(url)
webpage = self._download_webpage(url, podcast_id, fatal=False)
metadata = self._extract_metadata(webpage, podcast_id)
def page_func(page_num):
api_response = self._download_json(
f'https://api.tunein.com/profiles/{podcast_id}/contents', podcast_id,
note=f'Downloading page {page_num + 1}', query={
'filter': 't:free',
'offset': page_num * self._PAGE_SIZE,
'limit': self._PAGE_SIZE,
})
return [
self.url_result(
f'https://tunein.com/podcasts/{podcast_id}?topicId={episode["GuideId"][1:]}',
TuneInPodcastEpisodeIE, title=episode.get('Title'))
for episode in api_response['Items']]
entries = OnDemandPagedList(page_func, self._PAGE_SIZE)
return self.playlist_result(
entries, playlist_id=podcast_id, title=traverse_obj(metadata, ('profiles', podcast_id, 'title')),
description=traverse_obj(metadata, ('profiles', podcast_id, 'description')))
class TuneInPodcastEpisodeIE(TuneInBaseIE):
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'/podcasts/(?:[^?&]+-)?(?P<podcast_id>p\d+)/?\?topicId=(?P<id>\w\d+)'
_TESTS = [{
'url': 'https://tunein.com/podcasts/Technology-Podcasts/Artificial-Intelligence-p1153019/?topicId=236404354',
'info_dict': {
'id': 't236404354',
'title': '#351 MrBeast: Future of YouTube, Twitter, TikTok, and Instagram',
'description': 'md5:2784533b98f8ac45c0820b1e4a8d8bb2',
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
'timestamp': 1673458571,
'upload_date': '20230111',
'series_id': 'p1153019',
'series': 'Lex Fridman Podcast',
'ext': 'mp3',
},
}]
def _real_extract(self, url):
podcast_id, episode_id = self._match_valid_url(url).group('podcast_id', 'id')
episode_id = f't{episode_id}'
webpage = self._download_webpage(url, episode_id)
metadata = self._extract_metadata(webpage, episode_id)
formats, subtitles = self._extract_formats_and_subtitles(episode_id)
return {
'id': episode_id,
'title': traverse_obj(metadata, ('profiles', episode_id, 'title')),
'description': traverse_obj(metadata, ('profiles', episode_id, 'description')),
'thumbnail': traverse_obj(metadata, ('profiles', episode_id, 'image')),
'timestamp': parse_iso8601(
traverse_obj(metadata, ('profiles', episode_id, 'actions', 'play', 'publishTime'))),
'series_id': podcast_id,
'series': traverse_obj(metadata, ('profiles', podcast_id, 'title')),
'formats': formats,
'subtitles': subtitles,
}
class TuneInShortenerIE(InfoExtractor): class TuneInShortenerIE(InfoExtractor):
_WORKING = False
IE_NAME = 'tunein:shortener' IE_NAME = 'tunein:shortener'
IE_DESC = False # Do not list IE_DESC = False # Do not list
_VALID_URL = r'https?://tun\.in/(?P<id>[A-Za-z0-9]+)' _VALID_URL = r'https?://tun\.in/(?P<id>[^/?#]+)'
_TESTS = [{ _TESTS = [{
# test redirection
'url': 'http://tun.in/ser7s', 'url': 'http://tun.in/ser7s',
'info_dict': { 'info_dict': {
'id': 's34682', 'id': 's34682',
'title': str, 'title': str,
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
'location': 'Seattle-Tacoma, US',
'ext': 'mp3', 'ext': 'mp3',
'alt_title': 'World Class Jazz',
'channel_follower_count': int,
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
'location': r're:Seattle-Tacoma, (?:US|WA)',
'live_status': 'is_live', 'live_status': 'is_live',
'thumbnail': r're:https?://.+',
}, },
'params': { 'params': {'skip_download': 'Livestream'},
'skip_download': True, # live stream }, {
'url': 'http://tun.in/tqeeFw',
'info_dict': {
'id': 't236404354',
'title': str,
'ext': 'mp3',
'alt_title': 'Technology Podcasts >',
'cast': 'count:1',
'description': 'md5:1029895354ef073ff00f20b82eb6eb71',
'display_id': '236404354',
'duration': 8330,
'series': 'Lex Fridman Podcast',
'series_id': 'p1153019',
'thumbnail': r're:https?://.+',
'timestamp': 1673458571,
'upload_date': '20230111',
}, },
'params': {'skip_download': 'Livestream'},
}, {
'url': 'http://tun.in/pei6i',
'info_dict': {
'id': 'p14',
'title': 'BBC News',
},
'playlist_mincount': 35,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
redirect_id = self._match_id(url) redirect_id = self._match_id(url)
# The server doesn't support HEAD requests # The server doesn't support HEAD requests
urlh = self._request_webpage( urlh = self._request_webpage(url, redirect_id, 'Downloading redirect page')
url, redirect_id, note='Downloading redirect page') # Need to strip port from URL
parsed = urllib.parse.urlparse(urlh.url)
url = urlh.url new_url = parsed._replace(netloc=parsed.hostname).geturl()
url_parsed = urllib.parse.urlparse(url) # Prevent infinite loop in case redirect fails
if url_parsed.port == 443: if self.suitable(new_url):
url = url_parsed._replace(netloc=url_parsed.hostname).url raise UnsupportedError(new_url)
return self.url_result(new_url)
self.to_screen(f'Following redirect: {url}')
return self.url_result(url)

View File

@@ -30,13 +30,13 @@ class KnownDRMIE(UnsupportedInfoExtractor):
r'play\.hbomax\.com', r'play\.hbomax\.com',
r'channel(?:4|5)\.com', r'channel(?:4|5)\.com',
r'peacocktv\.com', r'peacocktv\.com',
r'(?:[\w\.]+\.)?disneyplus\.com', r'(?:[\w.]+\.)?disneyplus\.com',
r'open\.spotify\.com/(?:track|playlist|album|artist)', r'open\.spotify\.com',
r'tvnz\.co\.nz', r'tvnz\.co\.nz',
r'oneplus\.ch', r'oneplus\.ch',
r'artstation\.com/learning/courses', r'artstation\.com/learning/courses',
r'philo\.com', r'philo\.com',
r'(?:[\w\.]+\.)?mech-plus\.com', r'(?:[\w.]+\.)?mech-plus\.com',
r'aha\.video', r'aha\.video',
r'mubi\.com', r'mubi\.com',
r'vootkids\.com', r'vootkids\.com',
@@ -57,6 +57,14 @@ class KnownDRMIE(UnsupportedInfoExtractor):
r'ctv\.ca', r'ctv\.ca',
r'noovo\.ca', r'noovo\.ca',
r'tsn\.ca', r'tsn\.ca',
r'paramountplus\.com',
r'(?:m\.)?(?:sony)?crackle\.com',
r'cw(?:tv(?:pr)?|seed)\.com',
r'6play\.fr',
r'rtlplay\.be',
r'play\.rtl\.hr',
r'rtlmost\.hu',
r'plus\.rtl\.de(?!/podcast/)',
) )
_TESTS = [{ _TESTS = [{
@@ -78,10 +86,7 @@ class KnownDRMIE(UnsupportedInfoExtractor):
'url': r'https://www.disneyplus.com', 'url': r'https://www.disneyplus.com',
'only_matching': True, 'only_matching': True,
}, { }, {
'url': 'https://open.spotify.com/artist/', 'url': 'https://open.spotify.com',
'only_matching': True,
}, {
'url': 'https://open.spotify.com/track/',
'only_matching': True, 'only_matching': True,
}, { }, {
# https://github.com/yt-dlp/yt-dlp/issues/4122 # https://github.com/yt-dlp/yt-dlp/issues/4122
@@ -184,6 +189,39 @@ class KnownDRMIE(UnsupportedInfoExtractor):
}, { }, {
'url': 'https://www.tsn.ca/video/relaxed-oilers-look-to-put-emotional-game-2-loss-in-the-rearview%7E3148747', 'url': 'https://www.tsn.ca/video/relaxed-oilers-look-to-put-emotional-game-2-loss-in-the-rearview%7E3148747',
'only_matching': True, 'only_matching': True,
}, {
'url': 'https://www.paramountplus.com',
'only_matching': True,
}, {
'url': 'https://www.crackle.com',
'only_matching': True,
}, {
'url': 'https://m.sonycrackle.com',
'only_matching': True,
}, {
'url': 'https://www.cwtv.com',
'only_matching': True,
}, {
'url': 'https://www.cwseed.com',
'only_matching': True,
}, {
'url': 'https://cwtvpr.com',
'only_matching': True,
}, {
'url': 'https://www.6play.fr',
'only_matching': True,
}, {
'url': 'https://www.rtlplay.be',
'only_matching': True,
}, {
'url': 'https://play.rtl.hr',
'only_matching': True,
}, {
'url': 'https://www.rtlmost.hu',
'only_matching': True,
}, {
'url': 'https://plus.rtl.de/video-tv/',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
@@ -222,6 +260,7 @@ class KnownPiracyIE(UnsupportedInfoExtractor):
r'91porn\.com', r'91porn\.com',
r'einthusan\.(?:tv|com|ca)', r'einthusan\.(?:tv|com|ca)',
r'yourupload\.com', r'yourupload\.com',
r'xanimu\.com',
) )
_TESTS = [{ _TESTS = [{

View File

@@ -1,119 +0,0 @@
import re
from .common import InfoExtractor
from .rutv import RUTVIE
from ..utils import ExtractorError
class VestiIE(InfoExtractor):
_WORKING = False
IE_DESC = 'Вести.Ru'
_VALID_URL = r'https?://(?:.+?\.)?vesti\.ru/(?P<id>.+)'
_TESTS = [
{
'url': 'http://www.vesti.ru/videos?vid=575582&cid=1',
'info_dict': {
'id': '765035',
'ext': 'mp4',
'title': 'Вести.net: биткоины в России не являются законными',
'description': 'md5:d4bb3859dc1177b28a94c5014c35a36b',
'duration': 302,
},
'params': {
# m3u8 download
'skip_download': True,
},
},
{
'url': 'http://www.vesti.ru/doc.html?id=1349233',
'info_dict': {
'id': '773865',
'ext': 'mp4',
'title': 'Участники митинга штурмуют Донецкую областную администрацию',
'description': 'md5:1a160e98b3195379b4c849f2f4958009',
'duration': 210,
},
'params': {
# m3u8 download
'skip_download': True,
},
},
{
'url': 'http://www.vesti.ru/only_video.html?vid=576180',
'info_dict': {
'id': '766048',
'ext': 'mp4',
'title': 'США заморозило, Британию затопило',
'description': 'md5:f0ed0695ec05aed27c56a70a58dc4cc1',
'duration': 87,
},
'params': {
# m3u8 download
'skip_download': True,
},
},
{
'url': 'http://hitech.vesti.ru/news/view/id/4000',
'info_dict': {
'id': '766888',
'ext': 'mp4',
'title': 'Вести.net: интернет-гиганты начали перетягивание программных "одеял"',
'description': 'md5:65ddd47f9830c4f42ed6475f8730c995',
'duration': 279,
},
'params': {
# m3u8 download
'skip_download': True,
},
},
{
'url': 'http://sochi2014.vesti.ru/video/index/video_id/766403',
'info_dict': {
'id': '766403',
'ext': 'mp4',
'title': 'XXII зимние Олимпийские игры. Российские хоккеисты стартовали на Олимпиаде с победы',
'description': 'md5:55805dfd35763a890ff50fa9e35e31b3',
'duration': 271,
},
'params': {
# m3u8 download
'skip_download': True,
},
'skip': 'Blocked outside Russia',
},
{
'url': 'http://sochi2014.vesti.ru/live/play/live_id/301',
'info_dict': {
'id': '51499',
'ext': 'flv',
'title': 'Сочи-2014. Биатлон. Индивидуальная гонка. Мужчины ',
'description': 'md5:9e0ed5c9d2fa1efbfdfed90c9a6d179c',
},
'params': {
# rtmp download
'skip_download': True,
},
'skip': 'Translation has finished',
},
]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
video_id = mobj.group('id')
page = self._download_webpage(url, video_id, 'Downloading page')
mobj = re.search(
r'<meta[^>]+?property="og:video"[^>]+?content="http://www\.vesti\.ru/i/flvplayer_videoHost\.swf\?vid=(?P<id>\d+)',
page)
if mobj:
video_id = mobj.group('id')
page = self._download_webpage(f'http://www.vesti.ru/only_video.html?vid={video_id}', video_id,
'Downloading video page')
rutv_url = RUTVIE._extract_url(page)
if rutv_url:
return self.url_result(rutv_url, 'RUTV')
raise ExtractorError('No video found', expected=True)

View File

@@ -1,52 +0,0 @@
import re
from .common import InfoExtractor
from ..utils import int_or_none
class XanimuIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?xanimu\.com/(?P<id>[^/]+)/?'
_TESTS = [{
'url': 'https://xanimu.com/51944-the-princess-the-frog-hentai/',
'md5': '899b88091d753d92dad4cb63bbf357a7',
'info_dict': {
'id': '51944-the-princess-the-frog-hentai',
'ext': 'mp4',
'title': 'The Princess + The Frog Hentai',
'thumbnail': 'https://xanimu.com/storage/2020/09/the-princess-and-the-frog-hentai.jpg',
'description': r're:^Enjoy The Princess \+ The Frog Hentai',
'duration': 207.0,
'age_limit': 18,
},
}, {
'url': 'https://xanimu.com/huge-expansion/',
'only_matching': True,
}]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
formats = []
for format_id in ['videoHigh', 'videoLow']:
format_url = self._search_json(
rf'var\s+{re.escape(format_id)}\s*=', webpage, format_id,
video_id, default=None, contains_pattern=r'[\'"]([^\'"]+)[\'"]')
if format_url:
formats.append({
'url': format_url,
'format_id': format_id,
'quality': -2 if format_id.endswith('Low') else None,
})
return {
'id': video_id,
'formats': formats,
'title': self._search_regex(r'[\'"]headline[\'"]:\s*[\'"]([^"]+)[\'"]', webpage,
'title', default=None) or self._html_extract_title(webpage),
'thumbnail': self._html_search_meta('thumbnailUrl', webpage, default=None),
'description': self._html_search_meta('description', webpage, default=None),
'duration': int_or_none(self._search_regex(r'duration:\s*[\'"]([^\'"]+?)[\'"]',
webpage, 'duration', fatal=False)),
'age_limit': 18,
}