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 Wed Sep 10 20:42:57 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/proxydialer"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/ntp"
gost "github.com/metacubex/mihomo/transport/gost-plugin"
"github.com/metacubex/mihomo/transport/restls"
obfs "github.com/metacubex/mihomo/transport/simple-obfs"
@@ -251,8 +252,9 @@ func (ss *ShadowSocks) SupportUOT() bool {
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
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,
TimeFunc: ntp.Now,
})
if err != nil {
return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err)

View File

@@ -331,15 +331,22 @@ func (cp *CompatibleProvider) Close() error {
}
func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) {
excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None)
if err != nil {
return nil, fmt.Errorf("invalid excludeFilter regex: %w", err)
}
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)
if err != nil {
return nil, fmt.Errorf("invalid excludeFilter regex: %w", err)
}
excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg)
}
}
var filterRegs []*regexp2.Regexp
for _, filter := range strings.Split(filter, "`") {
filterReg, err := regexp2.Compile(filter, regexp2.None)
@@ -367,8 +374,9 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
proxies := []C.Proxy{}
proxiesSet := map[string]struct{}{}
for _, filterReg := range filterRegs {
LOOP1:
for idx, mapping := range schema.Proxies {
if nil != excludeTypeArray && len(excludeTypeArray) > 0 {
if len(excludeTypeArray) > 0 {
mType, ok := mapping["type"]
if !ok {
continue
@@ -377,18 +385,11 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
if !ok {
continue
}
flag := false
for i := range excludeTypeArray {
if strings.EqualFold(pType, excludeTypeArray[i]) {
flag = true
break
for _, excludeType := range excludeTypeArray {
if strings.EqualFold(pType, excludeType) {
continue LOOP1
}
}
if flag {
continue
}
}
mName, ok := mapping["name"]
if !ok {
@@ -398,9 +399,11 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
if !ok {
continue
}
if len(excludeFilter) > 0 {
if mat, _ := excludeFilterReg.MatchString(name); mat {
continue
if len(excludeFilterRegs) > 0 {
for _, excludeFilterReg := range excludeFilterRegs {
if mat, _ := excludeFilterReg.MatchString(name); mat {
continue LOOP1
}
}
}
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/randv2 v0.2.0
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-quic v0.0.0-20250909002258-06122df8f231
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-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-wireguard v0.0.0-20250503063753-2dc62acc626f
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/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.6-0.20250904143031-f1a62fab1489 h1:jKOFzhHTbxqhCluh5ONxjDe6CJMNHvgniXAf1RWuzlE=
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 h1:ArXEdw7JvbL3dLc3D7kBGTDmuBBI/sNIyR3O4MlfPH8=
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/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/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA=
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-shadowsocks2 v0.2.6 h1:ZR1kYT0f0Vi64iQSS09OdhFfppiNkh7kjgRdMm0SB98=
github.com/metacubex/sing-shadowsocks2 v0.2.6/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=
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/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.20250910070000-df2c1a4be299/go.mod h1:e4AyoGUrhiKQjRio3npn87E4TmIk7X5LmeiRwZettUA=
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.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/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM=
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.WriteToSystem,
)
} else {
ntp.ReCreateNTPService("", 0, "", false)
}
}

View File

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

View File

@@ -2,7 +2,7 @@
"manifest_version": 1,
"latest": {
"mihomo": "v1.19.13",
"mihomo_alpha": "alpha-7061c5a",
"mihomo_alpha": "alpha-909729c",
"clash_rs": "v0.9.0",
"clash_premium": "2023-09-05-gdcc8d87",
"clash_rs_alpha": "0.9.0-alpha+sha.50f295d"
@@ -69,5 +69,5 @@
"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 {
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 {
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) {
uconn, err = net.ListenUDP("udp", nil)
if err != nil {
return
return uconn, err
}
err = o.applyToUDPConn(uconn.(*net.UDPConn))
if err != nil {
uconn.Close()
uconn = nil
return
return uconn, err
}
return
return uconn, err
}
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) {
rconn, err := c.SyscallConn()
if err != nil {
return
return err
}
cerr := rconn.Control(func(fd uintptr) {
err = cb(int(fd))
})
if err != nil {
return
return err
}
if cerr != nil {
err = fmt.Errorf("failed to control fd: %w", cerr)
return
return err
}
return
return err
}
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) {
rcvbuf, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF)
if err != nil {
return
return err
}
// The test server called setsockopt(fd, SOL_SOCKET, SO_RCVBUF, 2500),
// and kernel will double this value for getsockopt().
assert.Equal(t, 5000, rcvbuf)
return
return err
})
assert.NoError(t, err)
}

View File

@@ -1173,7 +1173,7 @@ func splitHostPort(hostPort string) (host, port string) {
host = host[1 : len(host)-1]
}
return
return host, port
}
// Marshaling interface implementations.
@@ -1263,8 +1263,8 @@ func stringContainsCTLByte(s string) bool {
func JoinPath(base string, elem ...string) (result string, err error) {
url, err := Parse(base)
if err != nil {
return
return result, err
}
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)
if err != nil {
err = fmt.Errorf("failed to stat certificate file: %w", err)
return
return certModTime, keyModTime, err
}
certModTime = fi.ModTime()
fi, err = os.Stat(l.KeyFile)
if err != nil {
err = fmt.Errorf("failed to stat key file: %w", err)
return
return certModTime, keyModTime, err
}
keyModTime = fi.ModTime()
return
return certModTime, keyModTime, err
}
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()
if err != nil {
return
return cache, err
}
cert, err := tls.LoadX509KeyPair(l.CertFile, l.KeyFile)
if err != nil {
return
return cache, err
}
c.certificate = &cert
if c.certificate.Leaf == nil {
// certificate.Leaf was left nil by tls.LoadX509KeyPair before Go 1.23
c.certificate.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return
return cache, err
}
}
cache = c
return
return cache, err
}
func (l *LocalCertificateLoader) getCertificateWithCache() (*tls.Certificate, error) {

View File

@@ -60,7 +60,7 @@ func newUDPSessionEntry(
ExitFunc: exitFunc,
}
return
return e
}
// 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) {
timeoutEntry := make([]*udpSessionEntry, 0, len(m.m))
// We use RLock here as we are only scanning the map, not deleting from it.
m.mutex.RLock()
timeoutEntry := make([]*udpSessionEntry, 0, len(m.m))
now := time.Now()
for _, entry := range m.m {
if !idleOnly || now.Sub(entry.Last.Get()) > m.idleTimeout {
@@ -289,14 +288,14 @@ func (m *udpSessionManager) feed(msg *protocol.UDPMessage) {
// Call the hook
err = m.io.Hook(firstMsgData, &addr)
if err != nil {
return
return conn, actualAddr, err
}
actualAddr = addr
// Log the event
m.eventLogger.New(msg.SessionID, addr)
// Dial target
conn, err = m.io.UDP(addr)
return
return conn, actualAddr, err
}
exitFunc := func(err error) {
// 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)
if n <= 0 {
c.readMutex.Unlock()
return
return n, addr, err
}
n = c.Obfs.Deobfuscate(c.readBuf[:n], p)
c.readMutex.Unlock()
if n > 0 || err != nil {
return
return n, addr, err
}
// Invalid packet, try again
}
@@ -83,7 +83,7 @@ func (c *obfsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
if err == nil {
n = len(p)
}
return
return n, err
}
func (c *obfsPacketConn) Close() error {

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=r8125
PKG_VERSION:=9.016.00
PKG_VERSION:=9.016.01
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.bz2
PKG_SOURCE_URL:=https://github.com/openwrt/rtl8125/releases/download/$(PKG_VERSION)
PKG_HASH:=cd1955dd07d2f5a6faaa210ffc4e8af992421295a32ab6ddcfa759bed9eba922
PKG_HASH:=5434b26500538a62541c55cd09eea099177f59bd9cc48d16969089a9bcdbbd41
PKG_BUILD_PARALLEL:=1
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/etherdevice.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
}
@@ -76,7 +76,7 @@ Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
static void
_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) {
rtl8125_link_on_patch(dev);

View File

@@ -8,7 +8,7 @@
#include <linux/if_vlan.h>
#include <linux/crc32.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
@@ -31,7 +31,7 @@
rtl8125_init_software_variable(struct net_device *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)
tp->rtl8125_rx_config &= ~EnableRxDescV4_1;

View File

@@ -1,12 +1,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=r8126
PKG_VERSION:=10.015.00
PKG_VERSION:=10.016.00
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.bz2
PKG_SOURCE_URL:=https://github.com/openwrt/rtl8126/releases/download/$(PKG_VERSION)
PKG_HASH:=fac513aa925264a95b053e7532fcda56022d29db288f6625fafee2759a8a6124
PKG_HASH:=50c8d3d49592d2e8f372bd7ece8e7df9b50a71b055c077d42eacc42302914440
PKG_BUILD_PARALLEL:=1
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
+++ b/src/r8126.h
@@ -1756,6 +1756,10 @@ enum RTL8126_register_content {
@@ -1752,6 +1752,10 @@ enum RTL8126_register_content {
LinkStatus = 0x02,
FullDup = 0x01,
@@ -38,8 +38,8 @@ Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/delay.h>
@@ -4661,6 +4662,40 @@ rtl8126_link_down_patch(struct net_devic
#endif
@@ -4410,6 +4411,40 @@ rtl8126_link_down_patch(struct net_devic
//rtl8126_set_speed(dev, tp->autoneg, tp->speed, tp->duplex, tp->advertising);
}
+static unsigned int rtl8126_phy_duplex(u32 status)
@@ -79,7 +79,7 @@ Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
static void
_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) {
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/proxydialer"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/ntp"
gost "github.com/metacubex/mihomo/transport/gost-plugin"
"github.com/metacubex/mihomo/transport/restls"
obfs "github.com/metacubex/mihomo/transport/simple-obfs"
@@ -251,8 +252,9 @@ func (ss *ShadowSocks) SupportUOT() bool {
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
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,
TimeFunc: ntp.Now,
})
if err != nil {
return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err)

View File

@@ -331,15 +331,22 @@ func (cp *CompatibleProvider) Close() error {
}
func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) {
excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None)
if err != nil {
return nil, fmt.Errorf("invalid excludeFilter regex: %w", err)
}
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)
if err != nil {
return nil, fmt.Errorf("invalid excludeFilter regex: %w", err)
}
excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg)
}
}
var filterRegs []*regexp2.Regexp
for _, filter := range strings.Split(filter, "`") {
filterReg, err := regexp2.Compile(filter, regexp2.None)
@@ -367,8 +374,9 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
proxies := []C.Proxy{}
proxiesSet := map[string]struct{}{}
for _, filterReg := range filterRegs {
LOOP1:
for idx, mapping := range schema.Proxies {
if nil != excludeTypeArray && len(excludeTypeArray) > 0 {
if len(excludeTypeArray) > 0 {
mType, ok := mapping["type"]
if !ok {
continue
@@ -377,18 +385,11 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
if !ok {
continue
}
flag := false
for i := range excludeTypeArray {
if strings.EqualFold(pType, excludeTypeArray[i]) {
flag = true
break
for _, excludeType := range excludeTypeArray {
if strings.EqualFold(pType, excludeType) {
continue LOOP1
}
}
if flag {
continue
}
}
mName, ok := mapping["name"]
if !ok {
@@ -398,9 +399,11 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d
if !ok {
continue
}
if len(excludeFilter) > 0 {
if mat, _ := excludeFilterReg.MatchString(name); mat {
continue
if len(excludeFilterRegs) > 0 {
for _, excludeFilterReg := range excludeFilterRegs {
if mat, _ := excludeFilterReg.MatchString(name); mat {
continue LOOP1
}
}
}
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/randv2 v0.2.0
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-quic v0.0.0-20250909002258-06122df8f231
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-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-wireguard v0.0.0-20250503063753-2dc62acc626f
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/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.6-0.20250904143031-f1a62fab1489 h1:jKOFzhHTbxqhCluh5ONxjDe6CJMNHvgniXAf1RWuzlE=
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 h1:ArXEdw7JvbL3dLc3D7kBGTDmuBBI/sNIyR3O4MlfPH8=
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/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/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA=
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-shadowsocks2 v0.2.6 h1:ZR1kYT0f0Vi64iQSS09OdhFfppiNkh7kjgRdMm0SB98=
github.com/metacubex/sing-shadowsocks2 v0.2.6/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A=
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/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.20250910070000-df2c1a4be299/go.mod h1:e4AyoGUrhiKQjRio3npn87E4TmIk7X5LmeiRwZettUA=
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.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/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM=
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.WriteToSystem,
)
} else {
ntp.ReCreateNTPService("", 0, "", false)
}
}

View File

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

View File

@@ -10,11 +10,11 @@ include $(TOPDIR)/rules.mk
PKG_ARCH_quickstart:=$(ARCH)
PKG_NAME:=quickstart
PKG_VERSION:=0.11.6
PKG_VERSION:=0.11.7
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
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)

View File

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

View File

@@ -432,7 +432,8 @@ jobs:
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
build_apple:
name: Build Apple clients
runs-on: macos-15
runs-on: macos-26
if: false
needs:
- calculate_version
strategy:
@@ -479,14 +480,6 @@ jobs:
uses: actions/setup-go@v5
with:
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
if: matrix.if
run: |-

View File

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

View File

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

View File

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

View File

@@ -69,11 +69,7 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
} else {
return E.New("missing ECH keys")
}
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return E.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
echKeys, err := parseECHKeys(echKey)
if err != nil {
return E.Cause(err, "parse ECH keys")
}
@@ -85,21 +81,29 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
return nil
}
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
echKey, err := os.ReadFile(echKeyPath)
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
echKeys, err := parseECHKeys(echKey)
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)
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)
if err != nil {
return E.Cause(err, "parse ECH keys")
return nil, E.Cause(err, "parse ECH keys")
}
tlsConfig.EncryptedClientHelloKeys = echKeys
return nil
return echKeys, nil
}
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.")
}
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
return E.New("ECH requires go1.24, please recompile your binary.")
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
panic("unreachable")
}

View File

@@ -6,6 +6,7 @@ import (
"net"
"os"
"strings"
"sync"
"time"
"github.com/sagernet/fswatch"
@@ -21,6 +22,7 @@ import (
var errInsecureUnused = E.New("tls: insecure unused")
type STDServerConfig struct {
access sync.RWMutex
config *tls.Config
logger log.Logger
acmeService adapter.SimpleLifecycle
@@ -33,14 +35,22 @@ type STDServerConfig struct {
}
func (c *STDServerConfig) ServerName() string {
c.access.RLock()
defer c.access.RUnlock()
return c.config.ServerName
}
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 {
c.access.RLock()
defer c.access.RUnlock()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
return c.config.NextProtos[1:]
} else {
@@ -49,11 +59,15 @@ func (c *STDServerConfig) NextProtos() []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 {
c.config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
} else {
c.config.NextProtos = nextProto
config.NextProtos = nextProto
}
c.config = config
}
func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
@@ -78,9 +92,6 @@ func (c *STDServerConfig) Start() error {
if c.acmeService != nil {
return c.acmeService.Start()
} else {
if c.certificatePath == "" && c.keyPath == "" {
return nil
}
err := c.startWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
@@ -100,6 +111,9 @@ func (c *STDServerConfig) startWatcher() error {
if c.echKeyPath != "" {
watchPath = append(watchPath, c.echKeyPath)
}
if len(watchPath) == 0 {
return nil
}
watcher, err := fswatch.NewWatcher(fswatch.Options{
Path: watchPath,
Callback: func(path string) {
@@ -139,10 +153,18 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
if err != nil {
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")
} 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 {
return err
}
@@ -263,7 +285,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
return nil, err
}
}
var config ServerConfig = &STDServerConfig{
serverConfig := &STDServerConfig{
config: tlsConfig,
logger: logger,
acmeService: acmeService,
@@ -273,6 +295,12 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
keyPath: options.KeyPath,
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 !C.IsLinux {
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) {
s.access.Lock()
delete(s.delayHistory, tag)
s.access.Unlock()
s.notifyUpdated()
s.access.Unlock()
}
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
s.access.Lock()
s.delayHistory[tag] = history
s.access.Unlock()
s.notifyUpdated()
s.access.Unlock()
}
func (s *HistoryStorage) notifyUpdated() {
@@ -68,6 +68,8 @@ func (s *HistoryStorage) notifyUpdated() {
}
func (s *HistoryStorage) Close() error {
s.access.Lock()
defer s.access.Unlock()
s.updateHook = 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)
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) {

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"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/batch"
E "github.com/sagernet/sing/common/exceptions"

View File

@@ -27,16 +27,16 @@ import (
var _ adapter.RuleSet = (*LocalRuleSet)(nil)
type LocalRuleSet struct {
ctx context.Context
logger logger.Logger
tag string
rules []adapter.HeadlessRule
metadata adapter.RuleSetMetadata
fileFormat string
watcher *fswatch.Watcher
callbackAccess sync.Mutex
callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32
ctx context.Context
logger logger.Logger
tag string
access sync.RWMutex
rules []adapter.HeadlessRule
metadata adapter.RuleSetMetadata
fileFormat string
watcher *fswatch.Watcher
callbacks list.List[adapter.RuleSetUpdateCallback]
refs atomic.Int32
}
func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) {
@@ -141,11 +141,11 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule)
metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule)
metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
s.access.Lock()
s.rules = rules
s.metadata = metadata
s.callbackAccess.Lock()
callbacks := s.callbacks.Array()
s.callbackAccess.Unlock()
s.access.Unlock()
for _, callback := range callbacks {
callback(s)
}
@@ -157,10 +157,14 @@ func (s *LocalRuleSet) PostStart() error {
}
func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
s.access.RLock()
defer s.access.RUnlock()
return s.metadata
}
func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet {
s.access.RLock()
defer s.access.RUnlock()
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] {
s.callbackAccess.Lock()
defer s.callbackAccess.Unlock()
s.access.Lock()
defer s.access.Unlock()
return s.callbacks.PushBack(callback)
}
func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
s.callbackAccess.Lock()
defer s.callbackAccess.Unlock()
s.access.Lock()
defer s.access.Unlock()
s.callbacks.Remove(element)
}

View File

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

View File

@@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=hysteria
PKG_VERSION:=2.6.2
PKG_VERSION:=2.6.3
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
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_LICENSE:=MIT

View File

@@ -711,39 +711,37 @@ return view.extend({
o.modalonly = true;
}
if (features.with_reality_server) {
o = s.option(form.Flag, 'tls_reality', _('REALITY'));
o.depends({'tls': '1', 'tls_acme': '0', 'type': 'vless'});
o.depends({'tls': '1', 'tls_acme': null, 'type': 'vless'});
o.modalonly = true;
o = s.option(form.Flag, 'tls_reality', _('REALITY'));
o.depends({'tls': '1', 'tls_acme': '0', 'type': /^(anytls|vless)$/});
o.depends({'tls': '1', 'tls_acme': null, 'type': /^(anytls|vless)$/});
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_private_key', _('REALITY private key'));
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_private_key', _('REALITY private key'));
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.DynamicList, 'tls_reality_short_id', _('REALITY short ID'));
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.DynamicList, 'tls_reality_short_id', _('REALITY short ID'));
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_max_time_difference', _('Max time difference'),
_('The maximum time difference between the server and the client.'));
o.depends('tls_reality', '1');
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_max_time_difference', _('Max time difference'),
_('The maximum time difference between the server and the client.'));
o.depends('tls_reality', '1');
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_server_addr', _('Handshake server address'));
o.datatype = 'hostname';
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_server_addr', _('Handshake server address'));
o.datatype = 'hostname';
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_server_port', _('Handshake server port'));
o.datatype = 'port';
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
}
o = s.option(form.Value, 'tls_reality_server_port', _('Handshake server port'));
o.datatype = 'port';
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_cert_path', _('Certificate path'),
_('The server public key, in PEM format.'));

View File

@@ -31,7 +31,7 @@ const css = ' \
const hp_dir = '/var/run/homeproxy';
function getConnStat(self, site) {
function getConnStat(o, site) {
const callConnStat = rpc.declare({
object: 'luci.homeproxy',
method: 'connection_check',
@@ -39,12 +39,12 @@ function getConnStat(self, site) {
expect: { '': {} }
});
self.default = E('div', { 'style': 'cbi-value-field' }, [
o.default = E('div', { 'style': 'cbi-value-field' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, function() {
return L.resolveDefault(callConnStat(site), {}).then((ret) => {
let ele = self.default.firstElementChild.nextElementSibling;
let ele = o.default.firstElementChild.nextElementSibling;
if (ret.result) {
ele.style.setProperty('color', 'green');
ele.innerHTML = _('passed');
@@ -60,7 +60,7 @@ function getConnStat(self, site) {
]);
}
function getResVersion(self, type) {
function getResVersion(o, type) {
const callResVersion = rpc.declare({
object: 'luci.homeproxy',
method: 'resources_get_version',
@@ -83,23 +83,23 @@ function getResVersion(self, type) {
return L.resolveDefault(callResUpdate(type), {}).then((res) => {
switch (res.status) {
case 0:
self.description = _('Successfully updated.');
o.description = _('Successfully updated.');
break;
case 1:
self.description = _('Update failed.');
o.description = _('Update failed.');
break;
case 2:
self.description = _('Already in updating.');
o.description = _('Already in updating.');
break;
case 3:
self.description = _('Already at the latest version.');
o.description = _('Already at the latest version.');
break;
default:
self.description = _('Unknown error.');
o.description = _('Unknown error.');
break;
}
return self.map.reset();
return o.map.reset();
});
})
}, [ _('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({
object: 'luci.homeproxy',
method: 'log_clean',
@@ -121,7 +167,7 @@ function getRuntimeLog(name, filename) {
expect: { '': {} }
});
let log_textarea = E('div', { 'id': 'log_textarea' },
const log_textarea = E('div', { 'id': 'log_textarea' },
E('img', {
'src': L.resource('icons/loading.svg'),
'alt': _('Loading'),
@@ -155,11 +201,12 @@ function getRuntimeLog(name, filename) {
return E([
E('style', [ css ]),
E('div', {'class': 'cbi-map'}, [
E('h3', {'name': 'content'}, [
E('h3', {'name': 'content', 'style': 'align-items: center; display: flex;'}, [
_('%s log').format(name),
' ',
log_level_el || '',
E('button', {
'class': 'btn cbi-button cbi-button-action',
'style': 'margin-left: 4px;',
'click': ui.createHandlerFn(this, function() {
return L.resolveDefault(callLogClean(filename), {});
})
@@ -185,29 +232,28 @@ return view.extend({
s.anonymous = true;
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.cfgvalue = function() { return getConnStat(this, 'google') };
o.cfgvalue = L.bind(getConnStat, this, o, 'google');
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
s.anonymous = true;
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 = 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 = 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 = 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 = s.option(form.Value, 'github_token', _('GitHub token'));
@@ -231,13 +277,13 @@ return view.extend({
s.anonymous = true;
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.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.render = L.bind(getRuntimeLog, this, _('sing-box server'), 'sing-box-s');
o.render = L.bind(getRuntimeLog, this, o, _('sing-box server'));
return m.render();
},

View File

@@ -1,11 +1,11 @@
msgid ""
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"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1454
#: htdocs/luci-static/resources/view/homeproxy/node.js:1449
msgid "%s nodes removed"
msgstr ""
@@ -25,9 +25,9 @@ msgid ""
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1082
#: htdocs/luci-static/resources/view/homeproxy/node.js:1106
#: htdocs/luci-static/resources/view/homeproxy/server.js:760
#: htdocs/luci-static/resources/view/homeproxy/server.js:779
#: htdocs/luci-static/resources/view/homeproxy/node.js:1101
#: htdocs/luci-static/resources/view/homeproxy/server.js:758
#: htdocs/luci-static/resources/view/homeproxy/server.js:777
msgid "<strong>Save your configuration before uploading files!</strong>"
msgstr ""
@@ -123,7 +123,7 @@ msgid "Allow access from the Internet."
msgstr ""
#: 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"
msgstr ""
@@ -131,7 +131,7 @@ msgstr ""
msgid "Allow insecure connection at TLS client."
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."
msgstr ""
@@ -161,7 +161,7 @@ msgstr ""
msgid "Alternative TLS port"
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"
msgstr ""
@@ -222,11 +222,11 @@ msgstr ""
msgid "Authentication type"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1334
#: htdocs/luci-static/resources/view/homeproxy/node.js:1329
msgid "Auto update"
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."
msgstr ""
@@ -234,7 +234,7 @@ msgstr ""
msgid "BBR"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:187
#: htdocs/luci-static/resources/view/homeproxy/status.js:234
msgid "BaiDu"
msgstr ""
@@ -253,7 +253,7 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:453
#: 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"
msgstr ""
@@ -267,7 +267,7 @@ msgstr ""
msgid "BitTorrent"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1368
#: htdocs/luci-static/resources/view/homeproxy/node.js:1363
msgid "Blacklist mode"
msgstr ""
@@ -279,7 +279,7 @@ msgstr ""
#: 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:1100
#: htdocs/luci-static/resources/view/homeproxy/server.js:859
#: htdocs/luci-static/resources/view/homeproxy/server.js:857
msgid "Both"
msgstr ""
@@ -307,12 +307,12 @@ msgstr ""
msgid "CUBIC"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1238
#: htdocs/luci-static/resources/view/homeproxy/node.js:1233
msgid "Cancel"
msgstr ""
#: 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"
msgstr ""
@@ -328,15 +328,15 @@ msgstr ""
msgid "China DNS server"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:197
#: htdocs/luci-static/resources/view/homeproxy/status.js:243
msgid "China IPv4 list version"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:201
#: htdocs/luci-static/resources/view/homeproxy/status.js:247
msgid "China IPv6 list version"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:205
#: htdocs/luci-static/resources/view/homeproxy/status.js:251
msgid "China list version"
msgstr ""
@@ -353,7 +353,7 @@ msgstr ""
msgid "Cisco Public DNS (208.67.222.222)"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:166
#: htdocs/luci-static/resources/view/homeproxy/status.js:213
msgid "Clean log"
msgstr ""
@@ -379,7 +379,7 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:114
#: 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..."
msgstr ""
@@ -392,7 +392,7 @@ msgstr ""
msgid "Congestion control algorithm"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:184
#: htdocs/luci-static/resources/view/homeproxy/status.js:231
msgid "Connection check"
msgstr ""
@@ -439,6 +439,10 @@ msgstr ""
msgid "DTLS"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:136
msgid "Debug"
msgstr ""
#: htdocs/luci-static/resources/homeproxy.js:17
#: htdocs/luci-static/resources/view/homeproxy/client.js:433
#: 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."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1388
#: htdocs/luci-static/resources/view/homeproxy/node.js:1383
msgid "Default packet encoding"
msgstr ""
@@ -523,8 +527,8 @@ msgstr ""
#: 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:566
#: htdocs/luci-static/resources/view/homeproxy/node.js:1117
#: htdocs/luci-static/resources/view/homeproxy/node.js:1367
#: htdocs/luci-static/resources/view/homeproxy/node.js:1111
#: 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:279
msgid "Disable"
@@ -645,14 +649,14 @@ msgstr ""
msgid "Drop requests"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1374
#: htdocs/luci-static/resources/view/homeproxy/node.js:1369
msgid ""
"Drop/keep nodes that contain the specific keywords. <a target=\"_blank\" "
"href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/"
"Regular_Expressions\">Regex</a> is supported."
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."
msgstr ""
@@ -664,22 +668,22 @@ msgid ""
"a non-ACME system, such as a CA customer database."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1091
#: htdocs/luci-static/resources/view/homeproxy/node.js:1090
msgid ""
"ECH (Encrypted Client Hello) is a TLS extension that allows a client to "
"encrypt the first part of its ClientHello message."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1110
#: htdocs/luci-static/resources/view/homeproxy/server.js:825
#: htdocs/luci-static/resources/view/homeproxy/node.js:1105
#: htdocs/luci-static/resources/view/homeproxy/server.js:823
msgid "ECH config"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1099
#: htdocs/luci-static/resources/view/homeproxy/node.js:1094
msgid "ECH config path"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:786
#: htdocs/luci-static/resources/view/homeproxy/server.js:784
msgid "ECH key"
msgstr ""
@@ -703,7 +707,7 @@ msgstr ""
msgid "Early data is sent in path instead of header by default."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1210
#: htdocs/luci-static/resources/view/homeproxy/node.js:1205
msgid "Edit nodes"
msgstr ""
@@ -738,14 +742,10 @@ msgstr ""
msgid "Enable ACME"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1090
#: htdocs/luci-static/resources/view/homeproxy/node.js:1089
msgid "Enable ECH"
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/server.js:522
msgid "Enable TCP Brutal"
@@ -756,8 +756,8 @@ msgstr ""
msgid "Enable TCP Brutal congestion control algorithm"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1167
#: htdocs/luci-static/resources/view/homeproxy/server.js:845
#: htdocs/luci-static/resources/view/homeproxy/node.js:1162
#: htdocs/luci-static/resources/view/homeproxy/server.js:843
msgid "Enable UDP fragmentation."
msgstr ""
@@ -770,11 +770,11 @@ msgstr ""
msgid "Enable padding"
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."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1171
#: htdocs/luci-static/resources/view/homeproxy/node.js:1166
msgid ""
"Enable the SUoT protocol, requires server support. Conflict with multiplex."
msgstr ""
@@ -785,6 +785,10 @@ msgstr ""
msgid "Encrypt method"
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:271
#: 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:1537
#: 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:1301
#: htdocs/luci-static/resources/view/homeproxy/node.js:1355
#: htdocs/luci-static/resources/view/homeproxy/node.js:1358
#: htdocs/luci-static/resources/view/homeproxy/node.js:1127
#: htdocs/luci-static/resources/view/homeproxy/node.js:1296
#: htdocs/luci-static/resources/view/homeproxy/node.js:1350
#: 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:628
#: htdocs/luci-static/resources/view/homeproxy/server.js:630
@@ -843,11 +847,15 @@ msgstr ""
msgid "Failed to upload %s, error: %s."
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"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1365
#: htdocs/luci-static/resources/view/homeproxy/node.js:1360
msgid "Filter nodes"
msgstr ""
@@ -889,7 +897,7 @@ msgstr ""
msgid "GET"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:209
#: htdocs/luci-static/resources/view/homeproxy/status.js:255
msgid "GFW list version"
msgstr ""
@@ -915,11 +923,11 @@ msgstr ""
#: 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:357
#: htdocs/luci-static/resources/view/homeproxy/server.js:817
#: htdocs/luci-static/resources/view/homeproxy/server.js:815
msgid "Generate"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:213
#: htdocs/luci-static/resources/view/homeproxy/status.js:259
msgid "GitHub token"
msgstr ""
@@ -947,7 +955,7 @@ msgstr ""
msgid "Global settings"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:190
#: htdocs/luci-static/resources/view/homeproxy/status.js:237
msgid "Google"
msgstr ""
@@ -987,11 +995,11 @@ msgstr ""
msgid "HTTPUpgrade"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:735
#: htdocs/luci-static/resources/view/homeproxy/server.js:734
msgid "Handshake server address"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:741
#: htdocs/luci-static/resources/view/homeproxy/server.js:740
msgid "Handshake server port"
msgstr ""
@@ -1007,7 +1015,7 @@ msgstr ""
#: 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: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
msgid "HomeProxy"
msgstr ""
@@ -1150,18 +1158,18 @@ msgstr ""
msgid "Ignore client bandwidth"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1284
#: htdocs/luci-static/resources/view/homeproxy/node.js:1279
msgid "Import"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1231
#: htdocs/luci-static/resources/view/homeproxy/node.js:1310
#: htdocs/luci-static/resources/view/homeproxy/node.js:1312
#: htdocs/luci-static/resources/view/homeproxy/node.js:1226
#: htdocs/luci-static/resources/view/homeproxy/node.js:1305
#: htdocs/luci-static/resources/view/homeproxy/node.js:1307
msgid "Import share links"
msgstr ""
#: 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."
msgstr ""
@@ -1184,6 +1192,10 @@ msgstr ""
msgid "Independent cache per server"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:137
msgid "Info"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:1403
msgid "Interface Control"
msgstr ""
@@ -1217,7 +1229,7 @@ msgstr ""
msgid "Invert match result."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:767
#: htdocs/luci-static/resources/view/homeproxy/server.js:765
msgid "Key path"
msgstr ""
@@ -1290,7 +1302,7 @@ msgstr ""
msgid "Listen port"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:127
#: htdocs/luci-static/resources/view/homeproxy/status.js:173
msgid "Loading"
msgstr ""
@@ -1302,11 +1314,11 @@ msgstr ""
msgid "Local address"
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."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:137
#: htdocs/luci-static/resources/view/homeproxy/status.js:183
msgid "Log is empty."
msgstr ""
@@ -1451,7 +1463,7 @@ msgstr ""
msgid "Max download speed in Mbps."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:730
#: htdocs/luci-static/resources/view/homeproxy/server.js:729
msgid "Max time difference"
msgstr ""
@@ -1527,8 +1539,8 @@ msgstr ""
msgid "Mode"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1163
#: htdocs/luci-static/resources/view/homeproxy/server.js:840
#: htdocs/luci-static/resources/view/homeproxy/node.js:1158
#: htdocs/luci-static/resources/view/homeproxy/server.js:838
msgid "MultiPath TCP"
msgstr ""
@@ -1546,7 +1558,7 @@ msgstr ""
msgid "NOT RUNNING"
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."
msgstr ""
@@ -1564,7 +1576,7 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:637
#: 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"
msgstr ""
@@ -1584,15 +1596,15 @@ msgstr ""
msgid "No additional encryption support: It's basically duplicate encryption."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1410
#: htdocs/luci-static/resources/view/homeproxy/node.js:1405
msgid "No subscription available"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1435
#: htdocs/luci-static/resources/view/homeproxy/node.js:1430
msgid "No subscription node"
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."
msgstr ""
@@ -1605,7 +1617,7 @@ msgstr ""
msgid "Node Settings"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1216
#: htdocs/luci-static/resources/view/homeproxy/node.js:1211
msgid "Nodes"
msgstr ""
@@ -1687,6 +1699,10 @@ msgstr ""
msgid "Padding scheme"
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/server.js:190
msgid "Password"
@@ -1899,21 +1915,21 @@ msgstr ""
msgid "RDRC timeout"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1144
#: htdocs/luci-static/resources/view/homeproxy/server.js:715
#: htdocs/luci-static/resources/view/homeproxy/node.js:1138
#: htdocs/luci-static/resources/view/homeproxy/server.js:714
msgid "REALITY"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:720
#: htdocs/luci-static/resources/view/homeproxy/server.js:719
msgid "REALITY private key"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1148
#: htdocs/luci-static/resources/view/homeproxy/node.js:1143
msgid "REALITY public key"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1153
#: htdocs/luci-static/resources/view/homeproxy/server.js:725
#: htdocs/luci-static/resources/view/homeproxy/node.js:1148
#: htdocs/luci-static/resources/view/homeproxy/server.js:724
msgid "REALITY short ID"
msgstr ""
@@ -1946,7 +1962,7 @@ msgstr ""
msgid "Redirect TCP + Tun UDP"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:171
#: htdocs/luci-static/resources/view/homeproxy/status.js:218
msgid "Refresh every %s seconds."
msgstr ""
@@ -1963,11 +1979,11 @@ msgstr ""
msgid "Remote"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1432
#: htdocs/luci-static/resources/view/homeproxy/node.js:1427
msgid "Remove %s nodes"
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"
msgstr ""
@@ -1991,15 +2007,15 @@ msgstr ""
msgid "Resolve strategy"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:194
#: htdocs/luci-static/resources/view/homeproxy/status.js:240
msgid "Resources management"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:870
#: htdocs/luci-static/resources/view/homeproxy/server.js:868
msgid "Reuse address"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:871
#: htdocs/luci-static/resources/view/homeproxy/server.js:869
msgid "Reuse listener address."
msgstr ""
@@ -2081,7 +2097,7 @@ msgstr ""
msgid "STUN"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1176
#: htdocs/luci-static/resources/view/homeproxy/node.js:1171
msgid "SUoT version"
msgstr ""
@@ -2098,16 +2114,16 @@ msgstr ""
msgid "Same as main node"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:220
#: htdocs/luci-static/resources/view/homeproxy/status.js:225
#: htdocs/luci-static/resources/view/homeproxy/status.js:266
#: htdocs/luci-static/resources/view/homeproxy/status.js:271
msgid "Save"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1396
#: htdocs/luci-static/resources/view/homeproxy/node.js:1391
msgid "Save current settings"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1393
#: htdocs/luci-static/resources/view/homeproxy/node.js:1388
msgid "Save subscriptions settings"
msgstr ""
@@ -2254,19 +2270,19 @@ msgstr ""
msgid "String"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1321
#: htdocs/luci-static/resources/view/homeproxy/node.js:1316
msgid "Sub (%s)"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1348
#: htdocs/luci-static/resources/view/homeproxy/node.js:1343
msgid "Subscription URL-s"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1332
#: htdocs/luci-static/resources/view/homeproxy/node.js:1327
msgid "Subscriptions"
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."
msgstr ""
@@ -2274,8 +2290,8 @@ msgstr ""
msgid "Successfully updated."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1232
#: htdocs/luci-static/resources/view/homeproxy/node.js:1349
#: htdocs/luci-static/resources/view/homeproxy/node.js:1227
#: htdocs/luci-static/resources/view/homeproxy/node.js:1344
msgid ""
"Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) "
"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:949
#: 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"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1160
#: htdocs/luci-static/resources/view/homeproxy/server.js:835
#: htdocs/luci-static/resources/view/homeproxy/node.js:1155
#: htdocs/luci-static/resources/view/homeproxy/server.js:833
msgid "TCP fast open"
msgstr ""
@@ -2507,7 +2523,7 @@ msgid ""
"allowed to open."
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."
msgstr ""
@@ -2522,7 +2538,7 @@ msgid "The modern ImmortalWrt proxy platform for ARM64/AMD64."
msgstr ""
#: 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."
msgstr ""
@@ -2530,7 +2546,7 @@ msgstr ""
msgid "The path of the DNS server."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1100
#: htdocs/luci-static/resources/view/homeproxy/node.js:1095
msgid ""
"The path to the ECH config, in PEM format. If empty, load from DNS will be "
"attempted."
@@ -2552,11 +2568,11 @@ msgstr ""
msgid "The response code."
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."
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."
msgstr ""
@@ -2587,7 +2603,7 @@ msgid ""
msgstr ""
#: 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 ""
"This is <strong>DANGEROUS</strong>, your traffic is almost like "
"<strong>PLAIN TEXT</strong>! Use at your own risk!"
@@ -2628,6 +2644,10 @@ msgid ""
"<code>kmod-tun</code>"
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/server.js:409
msgid "Transport"
@@ -2657,21 +2677,21 @@ msgstr ""
#: 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:1099
#: htdocs/luci-static/resources/view/homeproxy/server.js:858
#: htdocs/luci-static/resources/view/homeproxy/server.js:856
msgid "UDP"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1166
#: htdocs/luci-static/resources/view/homeproxy/server.js:844
#: htdocs/luci-static/resources/view/homeproxy/node.js:1161
#: htdocs/luci-static/resources/view/homeproxy/server.js:842
msgid "UDP Fragment"
msgstr ""
#: 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"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1170
#: htdocs/luci-static/resources/view/homeproxy/node.js:1165
msgid "UDP over TCP"
msgstr ""
@@ -2712,15 +2732,15 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:148
#: htdocs/luci-static/resources/view/homeproxy/status.js:194
msgid "Unknown error: %s"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1137
#: htdocs/luci-static/resources/view/homeproxy/node.js:1131
msgid "Unsupported fingerprint!"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1407
#: htdocs/luci-static/resources/view/homeproxy/node.js:1402
msgid "Update %s subscriptions"
msgstr ""
@@ -2736,23 +2756,23 @@ msgstr ""
msgid "Update interval of rule set."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1402
#: htdocs/luci-static/resources/view/homeproxy/node.js:1397
msgid "Update nodes from subscriptions"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1345
#: htdocs/luci-static/resources/view/homeproxy/node.js:1340
msgid "Update subscriptions via proxy."
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1338
#: htdocs/luci-static/resources/view/homeproxy/node.js:1333
msgid "Update time"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1344
#: htdocs/luci-static/resources/view/homeproxy/node.js:1339
msgid "Update via proxy"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1105
#: htdocs/luci-static/resources/view/homeproxy/node.js:1100
msgid "Upload ECH config"
msgstr ""
@@ -2767,18 +2787,18 @@ msgid "Upload bandwidth in Mbps."
msgstr ""
#: 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"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:778
#: htdocs/luci-static/resources/view/homeproxy/server.js:776
msgid "Upload key"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1084
#: htdocs/luci-static/resources/view/homeproxy/node.js:1108
#: htdocs/luci-static/resources/view/homeproxy/server.js:762
#: htdocs/luci-static/resources/view/homeproxy/server.js:781
#: htdocs/luci-static/resources/view/homeproxy/node.js:1103
#: htdocs/luci-static/resources/view/homeproxy/server.js:760
#: htdocs/luci-static/resources/view/homeproxy/server.js:779
msgid "Upload..."
msgstr ""
@@ -2802,7 +2822,7 @@ msgstr ""
msgid "User"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1378
#: htdocs/luci-static/resources/view/homeproxy/node.js:1373
msgid "User-Agent"
msgstr ""
@@ -2830,12 +2850,16 @@ msgstr ""
msgid "WAN IP Policy"
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/server.js:416
msgid "WebSocket"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1369
#: htdocs/luci-static/resources/view/homeproxy/node.js:1364
msgid "Whitelist mode"
msgstr ""
@@ -2860,7 +2884,7 @@ msgid "Write proxy protocol in the connection header."
msgstr ""
#: 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)"
msgstr ""
@@ -2873,7 +2897,7 @@ msgid "ZeroSSL"
msgstr ""
#: 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"
msgstr ""
@@ -2917,19 +2941,19 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:504
#: 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:1133
#: htdocs/luci-static/resources/view/homeproxy/node.js:1127
#: htdocs/luci-static/resources/view/homeproxy/server.js:226
msgid "non-empty value"
msgstr ""
#: 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:1389
#: htdocs/luci-static/resources/view/homeproxy/node.js:1384
msgid "none"
msgstr ""
#: 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+)"
msgstr ""
@@ -2937,7 +2961,7 @@ msgstr ""
msgid "passed"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/server.js:783
#: htdocs/luci-static/resources/view/homeproxy/server.js:781
msgid "private key"
msgstr ""
@@ -2945,19 +2969,19 @@ msgstr ""
msgid "quic-go / uquic chrome"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:237
#: htdocs/luci-static/resources/view/homeproxy/status.js:283
msgid "sing-box client"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:240
#: htdocs/luci-static/resources/view/homeproxy/status.js:286
msgid "sing-box server"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1115
#: htdocs/luci-static/resources/view/homeproxy/node.js:1109
msgid "uTLS fingerprint"
msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/node.js:1116
#: htdocs/luci-static/resources/view/homeproxy/node.js:1110
msgid ""
"uTLS is a fork of \"crypto/tls\", which provides ClientHello fingerprinting "
"resistance."
@@ -2968,7 +2992,7 @@ msgid "unchecked"
msgstr ""
#: 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"
msgstr ""
@@ -2978,13 +3002,13 @@ msgstr ""
#: 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:1177
#: htdocs/luci-static/resources/view/homeproxy/node.js:1172
msgid "v1"
msgstr ""
#: 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:1178
#: htdocs/luci-static/resources/view/homeproxy/node.js:1173
msgid "v2"
msgstr ""
@@ -3003,8 +3027,8 @@ msgstr ""
#: 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:1363
#: htdocs/luci-static/resources/view/homeproxy/node.js:1355
#: htdocs/luci-static/resources/view/homeproxy/node.js:1358
#: htdocs/luci-static/resources/view/homeproxy/node.js:1350
#: htdocs/luci-static/resources/view/homeproxy/node.js:1353
msgid "valid URL"
msgstr ""

View File

@@ -8,11 +8,11 @@ msgstr ""
"MIME-Version: 1.0\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"
msgstr "%s 日志"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1454
#: htdocs/luci-static/resources/view/homeproxy/node.js:1449
msgid "%s nodes removed"
msgstr "移除了 %s 个节点"
@@ -34,9 +34,9 @@ msgstr ""
"<code>%s</code>。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1082
#: htdocs/luci-static/resources/view/homeproxy/node.js:1106
#: htdocs/luci-static/resources/view/homeproxy/server.js:760
#: htdocs/luci-static/resources/view/homeproxy/server.js:779
#: htdocs/luci-static/resources/view/homeproxy/node.js:1101
#: htdocs/luci-static/resources/view/homeproxy/server.js:758
#: htdocs/luci-static/resources/view/homeproxy/server.js:777
msgid "<strong>Save your configuration before uploading files!</strong>"
msgstr "<strong>上传文件前请先保存配置!</strong>"
@@ -132,7 +132,7 @@ msgid "Allow access from the Internet."
msgstr "允许来自互联网的访问。"
#: 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"
msgstr "允许不安全连接"
@@ -140,7 +140,7 @@ msgstr "允许不安全连接"
msgid "Allow insecure connection at TLS client."
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."
msgstr "从订阅获取节点时,默认允许不安全连接。"
@@ -170,7 +170,7 @@ msgstr "替代 HTTP 端口"
msgid "Alternative TLS port"
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"
msgstr "更新订阅时发生错误:%s"
@@ -233,11 +233,11 @@ msgstr "认证载荷"
msgid "Authentication type"
msgstr "认证类型"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1334
#: htdocs/luci-static/resources/view/homeproxy/node.js:1329
msgid "Auto update"
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."
msgstr "自动更新订阅和地理数据。"
@@ -245,7 +245,7 @@ msgstr "自动更新订阅和地理数据。"
msgid "BBR"
msgstr "BBR"
#: htdocs/luci-static/resources/view/homeproxy/status.js:187
#: htdocs/luci-static/resources/view/homeproxy/status.js:234
msgid "BaiDu"
msgstr "百度"
@@ -264,7 +264,7 @@ msgstr "二进制文件"
#: htdocs/luci-static/resources/view/homeproxy/client.js:453
#: 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"
msgstr "绑定接口"
@@ -278,7 +278,7 @@ msgstr "绑定出站流量至指定端口。留空自动检测。"
msgid "BitTorrent"
msgstr "BitTorrent"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1368
#: htdocs/luci-static/resources/view/homeproxy/node.js:1363
msgid "Blacklist mode"
msgstr "黑名单模式"
@@ -290,7 +290,7 @@ msgstr "封锁"
#: 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:1100
#: htdocs/luci-static/resources/view/homeproxy/server.js:859
#: htdocs/luci-static/resources/view/homeproxy/server.js:857
msgid "Both"
msgstr "全部"
@@ -318,12 +318,12 @@ msgstr "CNNIC 公共 DNS210.2.4.8"
msgid "CUBIC"
msgstr "CUBIC"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1238
#: htdocs/luci-static/resources/view/homeproxy/node.js:1233
msgid "Cancel"
msgstr "取消"
#: 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"
msgstr "证书路径"
@@ -339,15 +339,15 @@ msgstr "检查更新"
msgid "China DNS server"
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"
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"
msgstr "国内 IPv6 库版本"
#: htdocs/luci-static/resources/view/homeproxy/status.js:205
#: htdocs/luci-static/resources/view/homeproxy/status.js:251
msgid "China list version"
msgstr "国内域名列表版本"
@@ -364,7 +364,7 @@ msgstr "密码套件"
msgid "Cisco Public DNS (208.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"
msgstr "清空日志"
@@ -390,7 +390,7 @@ msgstr "Cloudflare"
#: htdocs/luci-static/resources/view/homeproxy/client.js:114
#: 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..."
msgstr "正在收集数据中..."
@@ -403,7 +403,7 @@ msgstr "仅常用端口(绕过 P2P 流量)"
msgid "Congestion control algorithm"
msgstr "拥塞控制算法"
#: htdocs/luci-static/resources/view/homeproxy/status.js:184
#: htdocs/luci-static/resources/view/homeproxy/status.js:231
msgid "Connection check"
msgstr "连接检查"
@@ -450,6 +450,10 @@ msgstr "DNS01 验证"
msgid "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/view/homeproxy/client.js:433
#: 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."
msgstr "用于未被任何路由规则匹配的连接的默认出站。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1388
#: htdocs/luci-static/resources/view/homeproxy/node.js:1383
msgid "Default packet encoding"
msgstr "默认包封装格式"
@@ -534,8 +538,8 @@ msgstr "直连 MAC 地址"
#: 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:566
#: htdocs/luci-static/resources/view/homeproxy/node.js:1117
#: htdocs/luci-static/resources/view/homeproxy/node.js:1367
#: htdocs/luci-static/resources/view/homeproxy/node.js:1111
#: 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:279
msgid "Disable"
@@ -658,7 +662,7 @@ msgstr "丢弃数据包"
msgid "Drop requests"
msgstr "丢弃请求"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1374
#: htdocs/luci-static/resources/view/homeproxy/node.js:1369
msgid ""
"Drop/keep nodes that contain the specific keywords. <a target=\"_blank\" "
"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\">"
"正则表达式</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."
msgstr "从订阅中 丢弃/保留 指定节点"
@@ -683,7 +687,7 @@ msgstr ""
"<br/>外部帐户绑定“用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA "
"客户数据库。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1091
#: htdocs/luci-static/resources/view/homeproxy/node.js:1090
msgid ""
"ECH (Encrypted Client Hello) is a TLS extension that allows a client to "
"encrypt the first part of its ClientHello message."
@@ -691,16 +695,16 @@ msgstr ""
"ECHEncrypted Client Hello是一个 TLS 扩展,它允许客户端加密其 ClientHello "
"信息的第一部分。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1110
#: htdocs/luci-static/resources/view/homeproxy/server.js:825
#: htdocs/luci-static/resources/view/homeproxy/node.js:1105
#: htdocs/luci-static/resources/view/homeproxy/server.js:823
msgid "ECH config"
msgstr "ECH 配置"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1099
#: htdocs/luci-static/resources/view/homeproxy/node.js:1094
msgid "ECH config path"
msgstr "ECH 配置路径"
#: htdocs/luci-static/resources/view/homeproxy/server.js:786
#: htdocs/luci-static/resources/view/homeproxy/server.js:784
msgid "ECH key"
msgstr "ECH 密钥"
@@ -724,7 +728,7 @@ msgstr "前置数据标头"
msgid "Early data is sent in path instead of header by default."
msgstr "前置数据默认发送在路径而不是标头中。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1210
#: htdocs/luci-static/resources/view/homeproxy/node.js:1205
msgid "Edit nodes"
msgstr "修改节点"
@@ -761,14 +765,10 @@ msgstr "启用 0-RTT 握手"
msgid "Enable ACME"
msgstr "启用 ACME"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1090
#: htdocs/luci-static/resources/view/homeproxy/node.js:1089
msgid "Enable 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/server.js:522
msgid "Enable TCP Brutal"
@@ -779,8 +779,8 @@ msgstr "启用 TCP Brutal"
msgid "Enable TCP Brutal congestion control algorithm"
msgstr "启用 TCP Brutal 拥塞控制算法。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1167
#: htdocs/luci-static/resources/view/homeproxy/server.js:845
#: htdocs/luci-static/resources/view/homeproxy/node.js:1162
#: htdocs/luci-static/resources/view/homeproxy/server.js:843
msgid "Enable UDP fragmentation."
msgstr "启用 UDP 分片。"
@@ -793,11 +793,11 @@ msgstr "启用端点独立 NAT"
msgid "Enable padding"
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."
msgstr "为监听器启用 TCP 快速打开。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1171
#: htdocs/luci-static/resources/view/homeproxy/node.js:1166
msgid ""
"Enable the SUoT protocol, requires server support. Conflict with multiplex."
msgstr "启用 SUoT 协议,需要服务端支持。与多路复用冲突。"
@@ -808,6 +808,10 @@ msgstr "启用 SUoT 协议,需要服务端支持。与多路复用冲突。"
msgid "Encrypt method"
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:271
#: 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:1537
#: 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:1301
#: htdocs/luci-static/resources/view/homeproxy/node.js:1355
#: htdocs/luci-static/resources/view/homeproxy/node.js:1358
#: htdocs/luci-static/resources/view/homeproxy/node.js:1127
#: htdocs/luci-static/resources/view/homeproxy/node.js:1296
#: htdocs/luci-static/resources/view/homeproxy/node.js:1350
#: 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:628
#: htdocs/luci-static/resources/view/homeproxy/server.js:630
@@ -866,11 +870,15 @@ msgstr "生成 %s 失败,错误:%s。"
msgid "Failed to upload %s, error: %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"
msgstr "过滤关键词"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1365
#: htdocs/luci-static/resources/view/homeproxy/node.js:1360
msgid "Filter nodes"
msgstr "过滤节点"
@@ -912,7 +920,7 @@ msgstr "分片回退延迟"
msgid "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"
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:355
#: 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"
msgstr "生成"
#: htdocs/luci-static/resources/view/homeproxy/status.js:213
#: htdocs/luci-static/resources/view/homeproxy/status.js:259
msgid "GitHub token"
msgstr "GitHub 令牌"
@@ -970,7 +978,7 @@ msgstr "全局代理 MAC 地址"
msgid "Global settings"
msgstr "全局设置"
#: htdocs/luci-static/resources/view/homeproxy/status.js:190
#: htdocs/luci-static/resources/view/homeproxy/status.js:237
msgid "Google"
msgstr "谷歌"
@@ -1010,11 +1018,11 @@ msgstr "HTTPS"
msgid "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"
msgstr "握手服务器地址"
#: htdocs/luci-static/resources/view/homeproxy/server.js:741
#: htdocs/luci-static/resources/view/homeproxy/server.js:740
msgid "Handshake server port"
msgstr "握手服务器端口"
@@ -1030,7 +1038,7 @@ msgstr "心跳间隔"
#: 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: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
msgid "HomeProxy"
msgstr "HomeProxy"
@@ -1177,18 +1185,18 @@ msgstr "如果你拥有根证书,使用此选项而不是允许不安全连接
msgid "Ignore client bandwidth"
msgstr "忽略客户端带宽"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1284
#: htdocs/luci-static/resources/view/homeproxy/node.js:1279
msgid "Import"
msgstr "导入"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1231
#: htdocs/luci-static/resources/view/homeproxy/node.js:1310
#: htdocs/luci-static/resources/view/homeproxy/node.js:1312
#: htdocs/luci-static/resources/view/homeproxy/node.js:1226
#: htdocs/luci-static/resources/view/homeproxy/node.js:1305
#: htdocs/luci-static/resources/view/homeproxy/node.js:1307
msgid "Import share links"
msgstr "导入分享链接"
#: 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."
msgstr "单位:秒。"
@@ -1211,6 +1219,10 @@ msgstr "在检查中,关闭空闲时间超过此值的会话(单位:秒)
msgid "Independent cache per server"
msgstr "独立缓存"
#: htdocs/luci-static/resources/view/homeproxy/status.js:137
msgid "Info"
msgstr "信息"
#: htdocs/luci-static/resources/view/homeproxy/client.js:1403
msgid "Interface Control"
msgstr "接口控制"
@@ -1244,7 +1256,7 @@ msgstr "反转"
msgid "Invert match result."
msgstr "反转匹配结果"
#: htdocs/luci-static/resources/view/homeproxy/server.js:767
#: htdocs/luci-static/resources/view/homeproxy/server.js:765
msgid "Key path"
msgstr "证书路径"
@@ -1319,7 +1331,7 @@ msgstr "监听接口"
msgid "Listen port"
msgstr "监听端口"
#: htdocs/luci-static/resources/view/homeproxy/status.js:127
#: htdocs/luci-static/resources/view/homeproxy/status.js:173
msgid "Loading"
msgstr "加载中"
@@ -1331,11 +1343,11 @@ msgstr "本地"
msgid "Local address"
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."
msgstr "日志文件不存在。"
#: htdocs/luci-static/resources/view/homeproxy/status.js:137
#: htdocs/luci-static/resources/view/homeproxy/status.js:183
msgid "Log is empty."
msgstr "日志为空。"
@@ -1480,7 +1492,7 @@ msgstr "最大下载速度"
msgid "Max download speed in 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"
msgstr "最大时间差"
@@ -1558,8 +1570,8 @@ msgstr "混合<code>系统</code> TCP 栈和 <code>gVisor</code> UDP 栈。"
msgid "Mode"
msgstr "模式"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1163
#: htdocs/luci-static/resources/view/homeproxy/server.js:840
#: htdocs/luci-static/resources/view/homeproxy/node.js:1158
#: htdocs/luci-static/resources/view/homeproxy/server.js:838
msgid "MultiPath TCP"
msgstr "多路径 TCPMPTCP"
@@ -1577,7 +1589,7 @@ msgstr "多路复用协议。"
msgid "NOT RUNNING"
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."
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:1097
#: htdocs/luci-static/resources/view/homeproxy/server.js:856
#: htdocs/luci-static/resources/view/homeproxy/server.js:854
msgid "Network"
msgstr "网络"
@@ -1615,15 +1627,15 @@ msgstr "无 TCP 传输层, 纯 HTTP 已合并到 HTTP 传输层。"
msgid "No additional encryption support: It's basically duplicate encryption."
msgstr "无额外加密支持:它基本上是重复加密。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1410
#: htdocs/luci-static/resources/view/homeproxy/node.js:1405
msgid "No subscription available"
msgstr "无可用订阅"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1435
#: htdocs/luci-static/resources/view/homeproxy/node.js:1430
msgid "No subscription node"
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."
msgstr "找不到有效分享链接。"
@@ -1636,7 +1648,7 @@ msgstr "节点"
msgid "Node Settings"
msgstr "节点设置"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1216
#: htdocs/luci-static/resources/view/homeproxy/node.js:1211
msgid "Nodes"
msgstr "节点"
@@ -1718,6 +1730,10 @@ msgstr "数据包编码"
msgid "Padding scheme"
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/server.js:190
msgid "Password"
@@ -1930,21 +1946,21 @@ msgstr "RDP"
msgid "RDRC timeout"
msgstr "RDRC 超时"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1144
#: htdocs/luci-static/resources/view/homeproxy/server.js:715
#: htdocs/luci-static/resources/view/homeproxy/node.js:1138
#: htdocs/luci-static/resources/view/homeproxy/server.js:714
msgid "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"
msgstr "REALITY 私钥"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1148
#: htdocs/luci-static/resources/view/homeproxy/node.js:1143
msgid "REALITY public key"
msgstr "REALITY 公钥"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1153
#: htdocs/luci-static/resources/view/homeproxy/server.js:725
#: htdocs/luci-static/resources/view/homeproxy/node.js:1148
#: htdocs/luci-static/resources/view/homeproxy/server.js:724
msgid "REALITY short ID"
msgstr "REALITY 标识符"
@@ -1977,7 +1993,7 @@ msgstr "Redirect TCP + TProxy UDP"
msgid "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."
msgstr "每 %s 秒刷新。"
@@ -1994,11 +2010,11 @@ msgstr "拒绝"
msgid "Remote"
msgstr "远程"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1432
#: htdocs/luci-static/resources/view/homeproxy/node.js:1427
msgid "Remove %s nodes"
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"
msgstr "移除所有订阅节点"
@@ -2022,15 +2038,15 @@ msgstr "解析"
msgid "Resolve strategy"
msgstr "解析策略"
#: htdocs/luci-static/resources/view/homeproxy/status.js:194
#: htdocs/luci-static/resources/view/homeproxy/status.js:240
msgid "Resources management"
msgstr "资源管理"
#: htdocs/luci-static/resources/view/homeproxy/server.js:870
#: htdocs/luci-static/resources/view/homeproxy/server.js:868
msgid "Reuse address"
msgstr "复用地址"
#: htdocs/luci-static/resources/view/homeproxy/server.js:871
#: htdocs/luci-static/resources/view/homeproxy/server.js:869
msgid "Reuse listener address."
msgstr "复用监听地址。"
@@ -2112,7 +2128,7 @@ msgstr "SSH"
msgid "STUN"
msgstr "STUN"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1176
#: htdocs/luci-static/resources/view/homeproxy/node.js:1171
msgid "SUoT version"
msgstr "SUoT 版本"
@@ -2129,16 +2145,16 @@ msgstr "Salamander"
msgid "Same as main node"
msgstr "保持与主节点一致"
#: htdocs/luci-static/resources/view/homeproxy/status.js:220
#: htdocs/luci-static/resources/view/homeproxy/status.js:225
#: htdocs/luci-static/resources/view/homeproxy/status.js:266
#: htdocs/luci-static/resources/view/homeproxy/status.js:271
msgid "Save"
msgstr "保存"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1396
#: htdocs/luci-static/resources/view/homeproxy/node.js:1391
msgid "Save current settings"
msgstr "保存当前设置"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1393
#: htdocs/luci-static/resources/view/homeproxy/node.js:1388
msgid "Save subscriptions settings"
msgstr "保存订阅设置"
@@ -2296,19 +2312,19 @@ msgstr ""
msgid "String"
msgstr "字符串"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1321
#: htdocs/luci-static/resources/view/homeproxy/node.js:1316
msgid "Sub (%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"
msgstr "订阅地址"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1332
#: htdocs/luci-static/resources/view/homeproxy/node.js:1327
msgid "Subscriptions"
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."
msgstr "成功导入 %s 个节点,共 %s 个。"
@@ -2316,8 +2332,8 @@ msgstr "成功导入 %s 个节点,共 %s 个。"
msgid "Successfully updated."
msgstr "更新成功。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1232
#: htdocs/luci-static/resources/view/homeproxy/node.js:1349
#: htdocs/luci-static/resources/view/homeproxy/node.js:1227
#: htdocs/luci-static/resources/view/homeproxy/node.js:1344
msgid ""
"Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) "
"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:949
#: 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"
msgstr "TCP"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1160
#: htdocs/luci-static/resources/view/homeproxy/server.js:835
#: htdocs/luci-static/resources/view/homeproxy/node.js:1155
#: htdocs/luci-static/resources/view/homeproxy/server.js:833
msgid "TCP fast open"
msgstr "TCP 快速打开"
@@ -2567,7 +2583,7 @@ msgid ""
"allowed to open."
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."
msgstr "服务器和客户端之间的最大时间差。"
@@ -2582,7 +2598,7 @@ msgid "The modern ImmortalWrt proxy platform for ARM64/AMD64."
msgstr "为 ARM64/AMD64 设计的现代 ImmortalWrt 代理平台。"
#: 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."
msgstr "绑定到的网络接口。"
@@ -2590,7 +2606,7 @@ msgstr "绑定到的网络接口。"
msgid "The path of the DNS server."
msgstr "DNS 服务器的路径。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1100
#: htdocs/luci-static/resources/view/homeproxy/node.js:1095
msgid ""
"The path to the ECH config, in PEM format. If empty, load from DNS will be "
"attempted."
@@ -2612,11 +2628,11 @@ msgstr "DNS 服务器的端口。"
msgid "The response code."
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."
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."
msgstr "服务端公钥,需要 PEM 格式。"
@@ -2649,7 +2665,7 @@ msgstr ""
"检测到任何活动,则会关闭连接。"
#: 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 ""
"This is <strong>DANGEROUS</strong>, your traffic is almost like "
"<strong>PLAIN TEXT</strong>! Use at your own risk!"
@@ -2697,6 +2713,10 @@ msgid ""
msgstr ""
"要启用 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/server.js:409
msgid "Transport"
@@ -2726,21 +2746,21 @@ msgstr "类型"
#: 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:1099
#: htdocs/luci-static/resources/view/homeproxy/server.js:858
#: htdocs/luci-static/resources/view/homeproxy/server.js:856
msgid "UDP"
msgstr "UDP"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1166
#: htdocs/luci-static/resources/view/homeproxy/server.js:844
#: htdocs/luci-static/resources/view/homeproxy/node.js:1161
#: htdocs/luci-static/resources/view/homeproxy/server.js:842
msgid "UDP Fragment"
msgstr "UDP 分片"
#: 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"
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"
msgstr "UDP over TCP"
@@ -2781,15 +2801,15 @@ msgstr "UUID"
msgid "Unknown error."
msgstr "未知错误。"
#: htdocs/luci-static/resources/view/homeproxy/status.js:148
#: htdocs/luci-static/resources/view/homeproxy/status.js:194
msgid "Unknown error: %s"
msgstr "未知错误:%s"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1137
#: htdocs/luci-static/resources/view/homeproxy/node.js:1131
msgid "Unsupported fingerprint!"
msgstr "不支持的指纹!"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1407
#: htdocs/luci-static/resources/view/homeproxy/node.js:1402
msgid "Update %s subscriptions"
msgstr "更新 %s 个订阅"
@@ -2805,23 +2825,23 @@ msgstr "更新间隔"
msgid "Update interval of rule set."
msgstr "规则集更新间隔。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1402
#: htdocs/luci-static/resources/view/homeproxy/node.js:1397
msgid "Update nodes from subscriptions"
msgstr "从订阅更新节点"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1345
#: htdocs/luci-static/resources/view/homeproxy/node.js:1340
msgid "Update subscriptions via proxy."
msgstr "使用代理更新订阅。"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1338
#: htdocs/luci-static/resources/view/homeproxy/node.js:1333
msgid "Update time"
msgstr "更新时间"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1344
#: htdocs/luci-static/resources/view/homeproxy/node.js:1339
msgid "Update via proxy"
msgstr "使用代理更新"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1105
#: htdocs/luci-static/resources/view/homeproxy/node.js:1100
msgid "Upload ECH config"
msgstr "上传 ECH 配置"
@@ -2836,18 +2856,18 @@ msgid "Upload bandwidth in Mbps."
msgstr "上传带宽单位Mbps。"
#: 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"
msgstr "上传证书"
#: htdocs/luci-static/resources/view/homeproxy/server.js:778
#: htdocs/luci-static/resources/view/homeproxy/server.js:776
msgid "Upload key"
msgstr "上传密钥"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1084
#: htdocs/luci-static/resources/view/homeproxy/node.js:1108
#: htdocs/luci-static/resources/view/homeproxy/server.js:762
#: htdocs/luci-static/resources/view/homeproxy/server.js:781
#: htdocs/luci-static/resources/view/homeproxy/node.js:1103
#: htdocs/luci-static/resources/view/homeproxy/server.js:760
#: htdocs/luci-static/resources/view/homeproxy/server.js:779
msgid "Upload..."
msgstr "上传..."
@@ -2871,7 +2891,7 @@ msgstr "用于验证返回证书上的主机名。"
msgid "User"
msgstr "用户"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1378
#: htdocs/luci-static/resources/view/homeproxy/node.js:1373
msgid "User-Agent"
msgstr "用户代理"
@@ -2899,12 +2919,16 @@ msgstr "WAN DNS从接口获取"
msgid "WAN IP Policy"
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/server.js:416
msgid "WebSocket"
msgstr "WebSocket"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1369
#: htdocs/luci-static/resources/view/homeproxy/node.js:1364
msgid "Whitelist mode"
msgstr "白名单模式"
@@ -2929,7 +2953,7 @@ msgid "Write proxy protocol in the connection header."
msgstr "在连接头中写入代理协议。"
#: 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)"
msgstr "Xudp (Xray-core)"
@@ -2942,7 +2966,7 @@ msgid "ZeroSSL"
msgstr "ZeroSSL"
#: 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"
msgstr "证书"
@@ -2986,19 +3010,19 @@ msgstr "gVisor"
#: htdocs/luci-static/resources/view/homeproxy/client.js:504
#: 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:1133
#: htdocs/luci-static/resources/view/homeproxy/node.js:1127
#: htdocs/luci-static/resources/view/homeproxy/server.js:226
msgid "non-empty value"
msgstr "非空值"
#: 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:1389
#: htdocs/luci-static/resources/view/homeproxy/node.js:1384
msgid "none"
msgstr "无"
#: 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+)"
msgstr "packet addr (v2ray-core v5+)"
@@ -3006,7 +3030,7 @@ msgstr "packet addr (v2ray-core v5+)"
msgid "passed"
msgstr "通过"
#: htdocs/luci-static/resources/view/homeproxy/server.js:783
#: htdocs/luci-static/resources/view/homeproxy/server.js:781
msgid "private key"
msgstr "私钥"
@@ -3014,19 +3038,19 @@ msgstr "私钥"
msgid "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"
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"
msgstr "sing-box 服务端"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1115
#: htdocs/luci-static/resources/view/homeproxy/node.js:1109
msgid "uTLS fingerprint"
msgstr "uTLS 指纹"
#: htdocs/luci-static/resources/view/homeproxy/node.js:1116
#: htdocs/luci-static/resources/view/homeproxy/node.js:1110
msgid ""
"uTLS is a fork of \"crypto/tls\", which provides ClientHello fingerprinting "
"resistance."
@@ -3038,7 +3062,7 @@ msgid "unchecked"
msgstr "未检查"
#: 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"
msgstr "独立 UCI 标识"
@@ -3048,13 +3072,13 @@ msgstr "独立值"
#: 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:1177
#: htdocs/luci-static/resources/view/homeproxy/node.js:1172
msgid "v1"
msgstr "v1"
#: 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:1178
#: htdocs/luci-static/resources/view/homeproxy/node.js:1173
msgid "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:1360
#: 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:1358
#: htdocs/luci-static/resources/view/homeproxy/node.js:1350
#: htdocs/luci-static/resources/view/homeproxy/node.js:1353
msgid "valid URL"
msgstr "有效网址"

View File

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

View File

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

View File

@@ -1 +1 @@
20250822033443
20250912032015

View File

@@ -40,6 +40,7 @@
2400:7fc0:2a0::/44
2400:7fc0:2c0::/44
2400:7fc0:4000::/40
2400:7fc0:4100::/48
2400:7fc0:6000::/40
2400:7fc0:8000::/36
2400:7fc0:a000::/36
@@ -131,6 +132,7 @@
2401:3480:3000::/36
2401:34a0::/31
2401:3800::/32
2401:5560:1000::/48
2401:5c20:10::/48
2401:70e0::/32
2401:71c0::/48
@@ -171,9 +173,8 @@
2401:f860:86::/47
2401:f860:88::/47
2401:f860:90::/46
2401:f860:94::/48
2401:f860:94::/47
2401:f860:100::/40
2401:f860:f100::/40
2401:fa00:40::/43
2402:840:d000::/46
2402:840:e000::/46
@@ -194,9 +195,7 @@
2402:6f40:2::/48
2402:7d80::/48
2402:7d80:240::/47
2402:7d80:6666::/48
2402:7d80:8888::/48
2402:7d80:9999::/48
2402:8bc0::/32
2402:8cc0::/40
2402:8cc0:200::/40
@@ -341,6 +340,8 @@
2404:2280:1f0::/45
2404:2280:1f8::/46
2404:2280:1fd::/48
2404:2280:1fe::/48
2404:2280:201::/48
2404:2280:202::/47
2404:2280:204::/46
2404:2280:208::/46
@@ -349,18 +350,22 @@
2404:2280:210::/46
2404:2280:214::/48
2404:2280:216::/47
2404:2280:218::/48
2404:2280:21a::/48
2404:2280:218::/46
2404:2280:21d::/48
2404:2280:221::/48
2404:2280:259::/48
2404:2280:25a::/47
2404:2280:25c::/48
2404:2280:265::/48
2404:2280:266::/47
2404:2280:268::/46
2404:2280:26c::/48
2404:2280:271::/48
2404:2280:268::/45
2404:2280:270::/47
2404:2280:272::/48
2404:2280:274::/48
2404:2280:27a::/48
2404:2280:27c::/47
2404:2280:27f::/48
2404:2280:282::/48
2404:3700::/48
2404:4dc0::/32
2404:6380::/48
@@ -391,6 +396,7 @@
2404:c2c0:2c0::/44
2404:c2c0:501::/48
2404:c2c0:4000::/40
2404:c2c0:4100::/48
2404:c2c0:6000::/40
2404:c2c0:8000::/36
2404:c2c0:bb00::/40
@@ -447,9 +453,9 @@
2406:840:e230::/44
2406:840:e260::/48
2406:840:e2cf::/48
2406:840:e500::/47
2406:840:e621::/48
2406:840:e666::/47
2406:840:e720::/44
2406:840:e80f::/48
2406:840:eb00::/46
2406:840:eb04::/47
@@ -595,6 +601,7 @@
2408:8181:a000::/40
2408:8181:a220::/44
2408:8181:e000::/40
2408:8182:6000::/40
2408:8182:c000::/40
2408:8183:4000::/40
2408:8183:8000::/40
@@ -737,7 +744,6 @@
2408:8406:b4c0::/42
2408:8406:b500::/41
2408:8406:b580::/42
2408:8407:500::/43
2408:8409::/40
2408:8409:100::/41
2408:8409:180::/42
@@ -1187,6 +1193,8 @@
240e::/20
2602:2e0:ff::/48
2602:f7ee:ee::/48
2602:f92a:a478::/48
2602:f92a:dead::/48
2602:f92a:e100::/44
2602:f93b:400::/38
2602:f9ba:a8::/48
@@ -1242,28 +1250,25 @@
2a06:3603::/32
2a06:3604::/30
2a06:9f81:4600::/43
2a06:9f81:4640::/44
2a06:a005:260::/43
2a06:a005:280::/43
2a06:a005:2a0::/44
2a06:a005:8d0::/44
2a06:a005:9c0::/48
2a06:a005:9e0::/44
2a06:a005:a13::/48
2a06:a005:e9a::/48
2a06:a005:1c40::/44
2a09:b280:ff81::/48
2a09:b280:ff83::/48
2a09:b280:ff84::/47
2a0a:2840::/30
2a0a:2840:20::/43
2a0a:2840:2000::/47
2a0a:2842::/32
2a0a:2845:aab8::/46
2a0a:2845:d647::/48
2a0a:2846::/48
2a0a:6040:ec00::/40
2a0a:6044:6600::/40
2a0b:b87:ffb5::/48
2a0b:2542::/48
2a0b:4340:a6::/48
2a0b:4b81:1001::/48
2a0b:4e07:b8::/47
2a0c:9a40:84e0::/48
2a0c:9a40:9e00::/43
@@ -1285,10 +1290,9 @@
2a0e:aa07:e044::/48
2a0e:aa07:e151::/48
2a0e:aa07:e155::/48
2a0e:aa07:e160::/47
2a0e:aa07:e162::/48
2a0e:aa07:e16a::/48
2a0e:aa07:e1a0::/44
2a0e:aa07:e1e1::/48
2a0e:aa07:e1e2::/47
2a0e:aa07:e1e4::/47
2a0e:aa07:e1e6::/48
@@ -1324,19 +1328,17 @@
2a0f:7d07::/32
2a0f:85c1:ba5::/48
2a0f:85c1:ca0::/44
2a0f:85c1:cf1::/48
2a0f:9400:6110::/48
2a0f:9400:7700::/48
2a0f:ac00::/29
2a10:2f00:15a::/48
2a10:cc40:190::/48
2a10:ccc0:d00::/46
2a10:ccc0:d0a::/47
2a10:ccc0:d0c::/47
2a10:ccc6:66c4::/48
2a10:ccc6:66c6::/48
2a10:ccc6:66c9::/48
2a10:ccc6:66c8::/47
2a10:ccc6:66ca::/48
2a10:ccc6:66cc::/47
2a12:f8c3::/36
2a13:1800::/48
2a13:1800:10::/48
@@ -1351,7 +1353,6 @@
2a13:a5c7:2102::/48
2a13:a5c7:2121::/48
2a13:a5c7:2801::/48
2a13:a5c7:2803::/48
2a13:a5c7:3108::/48
2a13:a5c7:31a0::/43
2a13:aac4:f000::/44
@@ -1372,9 +1373,7 @@
2a14:67c1:a040::/47
2a14:67c1:a061::/48
2a14:67c1:a064::/48
2a14:67c1:a090::/46
2a14:67c1:a095::/48
2a14:67c1:a096::/48
2a14:67c1:a090::/45
2a14:67c1:a099::/48
2a14:67c1:a100::/43
2a14:67c1:b000::/48
@@ -1384,15 +1383,15 @@
2a14:67c1:b100::/46
2a14:67c1:b105::/48
2a14:67c1:b107::/48
2a14:67c1:b130::/48
2a14:67c1:b132::/47
2a14:67c1:b130::/46
2a14:67c1:b134::/47
2a14:67c1:b140::/48
2a14:67c1:b4a1::/48
2a14:67c1:b4a2::/48
2a14:67c1:b4c0::/45
2a14:67c1:b4d0::/44
2a14:67c1:b4d0::/45
2a14:67c1:b4e0::/43
2a14:67c1:b500::/48
2a14:67c1:b500::/47
2a14:67c1:b549::/48
2a14:67c1:b561::/48
2a14:67c1:b563::/48
@@ -1401,16 +1400,17 @@
2a14:67c1:b582::/48
2a14:67c1:b588::/47
2a14:67c1:b590::/48
2a14:67c1:b599::/48
2a14:67c5:1900::/40
2a14:7580:9200::/40
2a14:7580:9400::/39
2a14:7580:d000::/37
2a14:7580:d800::/39
2a14:7580:da00::/40
2a14:7580:e200::/40
2a14:7580:fe00::/40
2a14:7581:3100::/40
2a14:7581:9010::/44
2a14:7583:f4fe::/48
2a14:7583:f500::/48
2c0f:f7a8:8011::/48
2c0f:f7a8:8050::/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.network
bsky.social
bt4g.org
bt4gprx.com
bt95.com
btaia.com
btbit.net
@@ -1131,6 +1133,7 @@ costco.com
cotweet.com
counter.social
coursehero.com
covenantswatch.org.tw
coze.com
cpj.org
cpu-monkey.com
@@ -1679,6 +1682,7 @@ fdc64.org
fdc89.jp
feedburner.com
feeder.co
feedly.com
feeds.fileforum.com
feedx.net
feelssh.com
@@ -1851,6 +1855,8 @@ ftvnews.com.tw
ftx.com
fucd.com
fuchsia.dev
fuckccp.com
fuckccp.xyz
fuckgfw.org
fulione.com
fullerconsideration.com
@@ -2702,6 +2708,7 @@ iphone4hongkong.com
iphonetaiwan.org
iphonix.fr
ipicture.ru
ipify.org
ipjetable.net
ipobar.com
ipoock.com
@@ -3307,6 +3314,7 @@ mofos.com
mog.com
mohu.club
mohu.rocks
moj.gov.tw
mojim.com
mol.gov.tw
molihua.org
@@ -4676,6 +4684,7 @@ talkcc.com
talkonly.net
tanc.org
tangren.us
tanks.gg
taoism.net
tapanwap.com
tapatalk.com

View File

@@ -1 +1 @@
202508212214
202509112212

View File

@@ -5,7 +5,7 @@
NAME="homeproxy"
log_max_size="10" #KB
log_max_size="50" #KB
main_log_file="/var/run/$NAME/$NAME.log"
singc_log_file="/var/run/$NAME/sing-box-c.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');
}
}
const log_level = uci.get(uciconfig, ucimain, 'log_level') || 'warn';
/* UCI config end */
/* Config helper start */
@@ -399,7 +401,7 @@ const config = {};
/* Log */
config.log = {
disabled: false,
level: 'warn',
level: log_level,
output: RUN_DIR + '/sing-box-c.log',
timestamp: true
};

View File

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

View File

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

View File

@@ -50,8 +50,7 @@ if (github_token) {
}
/* tun_gso was deprecated in sb 1.11 */
const tun_gso = uci.get(uciconfig, uciinfra, 'tun_gso');
if (tun_gso || tun_gso === '0')
if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
uci.delete(uciconfig, uciinfra, 'tun_gso');
/* create migration section */
@@ -65,6 +64,13 @@ if (!migration_crontab) {
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 */
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
uci.delete(uciconfig, ucimain, 'routing_port');
@@ -204,6 +210,10 @@ uci.foreach(uciconfig, ucinode, (cfg) => {
if (!isEmpty(cfg.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 */
if (!isEmpty(cfg.wireguard_gso))
uci.delete(uciconfig, cfg['.name'], 'wireguard_gso');
@@ -228,7 +238,7 @@ uci.foreach(uciconfig, uciroutingrule, (cfg) => {
/* server options */
/* auto_firewall was moved into server options */
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.foreach(uciconfig, uciserver, (cfg) => {

View File

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

View File

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

View File

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

View File

@@ -331,6 +331,32 @@ public class Utils
.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
#region
@@ -857,6 +883,55 @@ public class Utils
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()
{
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)
{
if (Contains(strData, "port", "socks-port", "proxies"))
if (Contains(strData, "external-controller", "-port", "proxies"))
{
var fileName = WriteAllText(strData, "yaml");

View File

@@ -94,17 +94,7 @@ public partial class CoreConfigSingboxService
if (!simpleDNSItem.Hosts.IsNullOrEmpty())
{
var userHostsMap = 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()
);
var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
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()
{
outbound = Global.DirectTag,

View File

@@ -261,17 +261,7 @@ public partial class CoreConfigV2rayService
if (!simpleDNSItem.Hosts.IsNullOrEmpty())
{
var userHostsMap = 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()
);
var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
foreach (var kvp in userHostsMap)
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,8 +5,6 @@ import zlib
from .anvato import AnvatoIE
from .common import InfoExtractor
from .paramountplus import ParamountPlusIE
from ..networking import HEADRequest
from ..utils import (
ExtractorError,
UserNotLive,
@@ -132,13 +130,7 @@ class CBSNewsEmbedIE(CBSNewsBaseIE):
video_id = item['mpxRefId']
video_url = self._get_video_url(item)
if not video_url:
# Old embeds redirect user to ParamountPlus but most links are 404
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)
raise ExtractorError('This video is no longer available', expected=True)
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',
'md5': '45ebc8a47ee1c2240178757caf8881b5',
'md5': '8b9bda03eba4d066928ae8d71f19befb',
'info_dict': {
'id': 'c64916eb-10fb-46a9-9a19-8c4b7ed064e7',
'ext': 'mp4',
@@ -55,9 +55,9 @@ class LocoIE(InfoExtractor):
'tags': ['Gameplay'],
'series': 'GTA 5',
'timestamp': 1740612872,
'modified_timestamp': 1740613037,
'modified_timestamp': 1750948439,
'upload_date': '20250226',
'modified_date': '20250226',
'modified_date': '20250626',
},
}, {
# Requires video authorization
@@ -123,8 +123,8 @@ class LocoIE(InfoExtractor):
def _real_extract(self, url):
video_type, video_id = self._match_valid_url(url).group('type', 'id')
webpage = self._download_webpage(url, video_id)
stream = traverse_obj(self._search_nextjs_data(webpage, video_id), (
'props', 'pageProps', ('liveStreamData', 'stream', 'liveStream'), {dict}, any, {require('stream info')}))
stream = traverse_obj(self._search_nextjs_v13_data(webpage, video_id), (
..., (None, 'ssrData'), ('liveStreamData', 'stream', 'liveStream'), {dict}, any, {require('stream info')}))
if access_token := self._get_access_token(video_id):
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 ..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):
_VALID_URL = r'https?://smotrim\.ru/(?P<type>brand|video|article|live)/(?P<id>[0-9]+)'
_TESTS = [{ # video
class SmotrimBaseIE(InfoExtractor):
_BASE_URL = 'https://smotrim.ru'
_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',
'md5': 'b1923a533c8cab09679789d720d0b1c5',
'info_dict': {
'id': '1539617',
'ext': 'mp4',
'title': 'Полиглот. Китайский с нуля за 16 часов! Урок №16',
'description': '',
'title': 'Урок №16',
'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',
'md5': 'e0ac453952afbc6a2742e850b4dc8e77',
'info_dict': {
'id': '2431846',
'ext': 'mp4',
'title': 'Новости культуры. Съёмки первой программы "Большие и маленькие"',
'description': 'md5:94a4a22472da4252bf5587a4ee441b99',
'title': 'Съёмки первой программы "Большие и маленькие"',
'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://smotrim.ru/brand/64356',
'md5': '740472999ccff81d7f6df79cecd91c18',
}, {
'url': 'https://www.vesti.ru/article/4642878',
'info_dict': {
'id': '2354523',
'id': '3007209',
'ext': 'mp4',
'title': 'Большие и маленькие. Лучшее. 4-й выпуск',
'description': 'md5:84089e834429008371ea41ea3507b989',
'title': 'Иностранные мессенджеры используют не только мошенники, но и вербовщики',
'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):
video_id, typ = self._match_valid_url(url).group('id', 'type')
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'
video_id = self._match_id(url)
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
from .common import InfoExtractor
from ..utils import (
OnDemandPagedList,
determine_ext,
UnsupportedError,
clean_html,
int_or_none,
join_nonempty,
parse_iso8601,
traverse_obj,
update_url_query,
url_or_none,
)
from ..utils.traversal import traverse_obj
class TuneInBaseIE(InfoExtractor):
_VALID_URL_BASE = r'https?://(?:www\.)?tunein\.com'
def _extract_metadata(self, webpage, content_id):
return self._search_json(r'window.INITIAL_STATE=', webpage, 'hydration', content_id, fatal=False)
def _call_api(self, item_id, endpoint=None, note='Downloading JSON metadata', fatal=False, query=None):
return self._download_json(
join_nonempty('https://api.tunein.com/profiles', item_id, endpoint, delim='/'),
item_id, note=note, fatal=fatal, query=query) or {}
def _extract_formats_and_subtitles(self, content_id):
streams = self._download_json(
f'https://opml.radiotime.com/Tune.ashx?render=json&formats=mp3,aac,ogg,flash,hls&id={content_id}',
content_id)['body']
'https://opml.radiotime.com/Tune.ashx', content_id, query={
'formats': 'mp3,aac,ogg,flash,hls',
'id': content_id,
'render': 'json',
})
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':
fmts, subs = self._extract_m3u8_formats_and_subtitles(stream['url'], content_id, fatal=False)
formats.extend(fmts)
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:
formats.append({
'url': stream['url'],
'abr': stream.get('bitrate'),
'ext': stream.get('media_type'),
})
formats.append(traverse_obj(stream, {
'abr': ('bitrate', {int_or_none}),
'ext': ('media_type', {str}),
'url': ('url', {self._proto_relative_url}),
}))
return formats, subtitles
class TuneInStationIE(TuneInBaseIE):
_VALID_URL = TuneInBaseIE._VALID_URL_BASE + r'(?:/radio/[^?#]+-|/embed/player/)(?P<id>s\d+)'
_EMBED_REGEX = [r'<iframe[^>]+src=["\'](?P<url>(?:https?://)?tunein\.com/embed/player/s\d+)']
IE_NAME = 'tunein:station'
_VALID_URL = r'https?://tunein\.com/radio/[^/?#]+(?P<id>s\d+)'
_TESTS = [{
'url': 'https://tunein.com/radio/Jazz24-885-s34682/',
'info_dict': {
'id': 's34682',
'title': str,
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
'location': 'Seattle-Tacoma, US',
'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',
'thumbnail': r're:https?://.+',
},
'params': {
'skip_download': True,
},
}, {
'url': 'https://tunein.com/embed/player/s6404/',
'only_matching': True,
'params': {'skip_download': 'Livestream'},
}, {
'url': 'https://tunein.com/radio/BBC-Radio-1-988-s24939/',
'info_dict': {
'id': 's24939',
'title': str,
'description': 'md5:ee2c56794844610d045f8caf5ff34d0c',
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
'location': 'London, UK',
'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',
'thumbnail': r're:https?://.+',
},
'params': {
'skip_download': True,
'params': {'skip_download': 'Livestream'},
}]
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 = [{
'url': 'https://www.martiniinthemorning.com/',
'info_dict': {
'id': 's55412',
'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):
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)
metadata = self._extract_metadata(webpage, station_id)
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,
}
return self.url_result(
f'https://tunein.com/{kind}/?{kind}id={embed_id[1:]}')
class TuneInShortenerIE(InfoExtractor):
_WORKING = False
IE_NAME = 'tunein:shortener'
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 = [{
# test redirection
'url': 'http://tun.in/ser7s',
'info_dict': {
'id': 's34682',
'title': str,
'description': 'md5:d6d0b89063fd68d529fa7058ee98619b',
'thumbnail': r're:https?://cdn-profiles\.tunein\.com/.+',
'location': 'Seattle-Tacoma, US',
'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',
'thumbnail': r're:https?://.+',
},
'params': {
'skip_download': True, # live stream
'params': {'skip_download': 'Livestream'},
}, {
'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):
redirect_id = self._match_id(url)
# The server doesn't support HEAD requests
urlh = self._request_webpage(
url, redirect_id, note='Downloading redirect page')
url = urlh.url
url_parsed = urllib.parse.urlparse(url)
if url_parsed.port == 443:
url = url_parsed._replace(netloc=url_parsed.hostname).url
self.to_screen(f'Following redirect: {url}')
return self.url_result(url)
urlh = self._request_webpage(url, redirect_id, 'Downloading redirect page')
# Need to strip port from URL
parsed = urllib.parse.urlparse(urlh.url)
new_url = parsed._replace(netloc=parsed.hostname).geturl()
# Prevent infinite loop in case redirect fails
if self.suitable(new_url):
raise UnsupportedError(new_url)
return self.url_result(new_url)

View File

@@ -30,13 +30,13 @@ class KnownDRMIE(UnsupportedInfoExtractor):
r'play\.hbomax\.com',
r'channel(?:4|5)\.com',
r'peacocktv\.com',
r'(?:[\w\.]+\.)?disneyplus\.com',
r'open\.spotify\.com/(?:track|playlist|album|artist)',
r'(?:[\w.]+\.)?disneyplus\.com',
r'open\.spotify\.com',
r'tvnz\.co\.nz',
r'oneplus\.ch',
r'artstation\.com/learning/courses',
r'philo\.com',
r'(?:[\w\.]+\.)?mech-plus\.com',
r'(?:[\w.]+\.)?mech-plus\.com',
r'aha\.video',
r'mubi\.com',
r'vootkids\.com',
@@ -57,6 +57,14 @@ class KnownDRMIE(UnsupportedInfoExtractor):
r'ctv\.ca',
r'noovo\.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 = [{
@@ -78,10 +86,7 @@ class KnownDRMIE(UnsupportedInfoExtractor):
'url': r'https://www.disneyplus.com',
'only_matching': True,
}, {
'url': 'https://open.spotify.com/artist/',
'only_matching': True,
}, {
'url': 'https://open.spotify.com/track/',
'url': 'https://open.spotify.com',
'only_matching': True,
}, {
# 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',
'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):
@@ -222,6 +260,7 @@ class KnownPiracyIE(UnsupportedInfoExtractor):
r'91porn\.com',
r'einthusan\.(?:tv|com|ca)',
r'yourupload\.com',
r'xanimu\.com',
)
_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,
}