1
0
mirror of https://github.com/nitezs/sub2clash.git synced 2024-12-23 20:24:42 -05:00
This commit is contained in:
Nite07 2023-09-13 00:46:17 +08:00
parent d894fea89e
commit 6e5e999937
26 changed files with 10919 additions and 1155 deletions

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
PORT=8011
META_TEMPLATE=meta_template.json
CLASH_TEMPLATE=clash_template.json
REQUEST_RETRY_TIMES=3
REQUEST_MAX_FILE_SIZE=1048576

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
.idea .idea
dist dist
subs subs
templates test

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Nite07
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -4,9 +4,10 @@
## 特性 ## 特性
- 开箱即用的规则、策略组配置 - 开箱即用的规则、策略组配置
- 自动根据节点名称按国家划分策略组 - 自动根据节点名称按国家划分策略组
- 支持协议 - 支持多订阅合并
- 支持多种协议
- [x] Shadowsocks - [x] Shadowsocks
- [x] ShadowsocksR - [x] ShadowsocksR
- [x] Vmess - [x] Vmess
@ -22,16 +23,22 @@
获取 Clash 配置链接 获取 Clash 配置链接
| Query 参数 | 类型 | 说明 | | Query 参数 | 类型 | 说明 |
| ---------- | ------ | --------------------------------- | |----------|--------|-------------------------|
| sub | string | 订阅链接 | | sub | string | 订阅链接(可以输入多个订阅,用 `,` 分隔) |
| refresh | bool | 强制获取新配置(默认缓存 5 分钟) | | refresh | bool | 强制刷新配置(默认缓存 5 分钟) |
### /meta ### /meta
获取 Meta 配置链接 获取 Meta 配置链接
| Query 参数 | 类型 | 说明 | | Query 参数 | 类型 | 说明 |
| ---------- | ------ | --------------------------------- | |----------|--------|-------------------------|
| sub | string | 订阅链接 | | sub | string | 订阅链接(可以输入多个订阅,用 `,` 分隔) |
| refresh | bool | 强制获取新配置(默认缓存 5 分钟) | | refresh | bool | 强制刷新配置(默认缓存 5 分钟) |
## TODO
- [ ] 完善日志功能
- [ ] 支持自动测速分组
- [ ] 完善配置模板

View File

@ -2,9 +2,9 @@ package controller
import ( import (
"net/http" "net/http"
"net/url" "strings"
"sub/config" "sub2clash/config"
"sub/validator" "sub2clash/validator"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -17,9 +17,10 @@ func SubmodHandler(c *gin.Context) {
c.String(http.StatusBadRequest, "参数错误: "+err.Error()) c.String(http.StatusBadRequest, "参数错误: "+err.Error())
return return
} }
query.Sub, _ = url.QueryUnescape(query.Sub)
// 混合订阅和模板节点 // 混合订阅和模板节点
sub, err := MixinSubTemp(query, config.Default.ClashTemplate) sub, err := MixinSubsAndTemplate(
strings.Split(query.Sub, ","), query.Refresh, config.Default.ClashTemplate,
)
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, err.Error()) c.String(http.StatusInternalServerError, err.Error())
return return

View File

@ -3,14 +3,16 @@ package controller
import ( import (
"errors" "errors"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"net/url"
"strings" "strings"
"sub/model" "sub2clash/model"
"sub/parser" "sub2clash/parser"
"sub/utils" "sub2clash/utils"
"sub/validator"
) )
func MixinSubTemp(query validator.SubQuery, template string) (*model.Subscription, error) { func MixinSubsAndTemplate(subs []string, refresh bool, template string) (
*model.Subscription, error,
) {
// 定义变量 // 定义变量
var temp *model.Subscription var temp *model.Subscription
var sub *model.Subscription var sub *model.Subscription
@ -24,28 +26,36 @@ func MixinSubTemp(query validator.SubQuery, template string) (*model.Subscriptio
if err != nil { if err != nil {
return nil, errors.New("解析模板失败: " + err.Error()) return nil, errors.New("解析模板失败: " + err.Error())
} }
var proxies []model.Proxy
// 加载订阅 // 加载订阅
data, err := utils.LoadSubscription( for i := range subs {
query.Sub, subs[i], _ = url.QueryUnescape(subs[i])
query.Refresh, if _, err := url.ParseRequestURI(subs[i]); err != nil {
) return nil, errors.New("订阅地址错误: " + err.Error())
if err != nil { }
return nil, errors.New("加载订阅失败: " + err.Error()) data, err := utils.LoadSubscription(
} subs[i],
// 解析订阅 refresh,
var proxyList []model.Proxy )
err = yaml.Unmarshal(data, &sub)
if err != nil {
// 如果无法直接解析尝试Base64解码
base64, err := parser.DecodeBase64(string(data))
if err != nil { if err != nil {
return nil, errors.New("加载订阅失败: " + err.Error()) return nil, errors.New("加载订阅失败: " + err.Error())
} }
proxyList = utils.ParseProxy(strings.Split(base64, "\n")...) // 解析订阅
} else { var proxyList []model.Proxy
proxyList = sub.Proxies err = yaml.Unmarshal(data, &sub)
if err != nil {
// 如果无法直接解析尝试Base64解码
base64, err := parser.DecodeBase64(string(data))
if err != nil {
return nil, errors.New("加载订阅失败: " + err.Error())
}
proxyList = utils.ParseProxy(strings.Split(base64, "\n")...)
} else {
proxyList = sub.Proxies
}
proxies = append(proxies, proxyList...)
} }
// 添加节点 // 添加节点
utils.AddProxy(temp, proxyList...) utils.AddProxy(temp, proxies...)
return temp, nil return temp, nil
} }

View File

@ -3,9 +3,9 @@ package controller
import ( import (
_ "embed" _ "embed"
"net/http" "net/http"
"net/url" "strings"
"sub/config" "sub2clash/config"
"sub/validator" "sub2clash/validator"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -18,9 +18,10 @@ func SubHandler(c *gin.Context) {
c.String(http.StatusBadRequest, "参数错误: "+err.Error()) c.String(http.StatusBadRequest, "参数错误: "+err.Error())
return return
} }
query.Sub, _ = url.QueryUnescape(query.Sub)
// 混合订阅和模板节点 // 混合订阅和模板节点
sub, err := MixinSubTemp(query, config.Default.MetaTemplate) sub, err := MixinSubsAndTemplate(
strings.Split(query.Sub, ","), query.Refresh, config.Default.MetaTemplate,
)
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, err.Error()) c.String(http.StatusInternalServerError, err.Error())
return return

View File

@ -2,7 +2,7 @@ package api
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"sub/api/controller" "sub2clash/api/controller"
) )
func SetRoute(r *gin.Engine) { func SetRoute(r *gin.Engine) {

View File

@ -1,16 +1,59 @@
package config package config
import (
"fmt"
"github.com/joho/godotenv"
"os"
"strconv"
)
type Config struct { type Config struct {
Port int //TODO: 使用自定义端口 Port int
MetaTemplate string MetaTemplate string
ClashTemplate string ClashTemplate string
RequestRetryTimes int
RequestMaxFileSize int64
} }
var Default *Config var Default *Config
func init() { func init() {
Default = &Config{ Default = &Config{
MetaTemplate: "template-meta.yaml", MetaTemplate: "template_meta.yaml",
ClashTemplate: "template-clash.yaml", ClashTemplate: "template_clash.yaml",
RequestRetryTimes: 3,
RequestMaxFileSize: 1024 * 1024 * 1,
Port: 8011,
}
err := godotenv.Load()
if err != nil {
return
}
if os.Getenv("PORT") != "" {
atoi, err := strconv.Atoi(os.Getenv("PORT"))
if err != nil {
fmt.Println("PORT 不合法")
}
Default.Port = atoi
}
if os.Getenv("META_TEMPLATE") != "" {
Default.MetaTemplate = os.Getenv("META_TEMPLATE")
}
if os.Getenv("CLASH_TEMPLATE") != "" {
Default.ClashTemplate = os.Getenv("CLASH_TEMPLATE")
}
if os.Getenv("REQUEST_RETRY_TIMES") != "" {
atoi, err := strconv.Atoi(os.Getenv("REQUEST_RETRY_TIMES"))
if err != nil {
fmt.Println("REQUEST_RETRY_TIMES 不合法")
}
Default.RequestRetryTimes = atoi
}
if os.Getenv("REQUEST_MAX_FILE_SIZE") != "" {
atoi, err := strconv.Atoi(os.Getenv("REQUEST_MAX_FILE_SIZE"))
if err != nil {
fmt.Println("REQUEST_MAX_FILE_SIZE 不合法")
}
Default.RequestMaxFileSize = int64(atoi)
} }
} }

3
go.mod
View File

@ -1,4 +1,4 @@
module sub module sub2clash
go 1.21 go 1.21
@ -13,6 +13,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.3 // indirect github.com/go-playground/validator/v10 v10.15.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect

2
go.sum
View File

@ -27,6 +27,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=

12
main.go
View File

@ -6,14 +6,16 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"os" "os"
"path/filepath" "path/filepath"
"sub/api" "strconv"
"sub/config" "sub2clash/api"
"sub2clash/config"
_ "sub2clash/config"
) )
//go:embed templates/template-meta.yaml //go:embed templates/template_meta.yaml
var templateMeta string var templateMeta string
//go:embed templates/template-clash.yaml //go:embed templates/template_clash.yaml
var templateClash string var templateClash string
func writeTemplate(path string, template string) error { func writeTemplate(path string, template string) error {
@ -75,7 +77,7 @@ func main() {
// 设置路由 // 设置路由
api.SetRoute(r) api.SetRoute(r)
fmt.Println("Server is running at 8011") fmt.Println("Server is running at 8011")
err := r.Run(":8011") err := r.Run(":" + strconv.Itoa(config.Default.Port))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return return

File diff suppressed because it is too large Load Diff

1044
model/contry_map.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,9 +14,10 @@ type Subscription struct {
} }
type ProxyGroup struct { type ProxyGroup struct {
Name string `yaml:"name,omitempty"` Name string `yaml:"name,omitempty"`
Type string `yaml:"type,omitempty"` Type string `yaml:"type,omitempty"`
Proxies []string `yaml:"proxies,omitempty"` Proxies []string `yaml:"proxies,omitempty"`
IsCountryGrop bool `yaml:"-"`
} }
type RuleProvider struct { type RuleProvider struct {

View File

@ -5,7 +5,7 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sub/model" "sub2clash/model"
) )
// ParseSS 解析 SSShadowsocksURL // ParseSS 解析 SSShadowsocksURL

View File

@ -5,7 +5,7 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sub/model" "sub2clash/model"
) )
func ParseShadowsocksR(proxy string) (model.Proxy, error) { func ParseShadowsocksR(proxy string) (model.Proxy, error) {

View File

@ -5,7 +5,7 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sub/model" "sub2clash/model"
) )
func ParseTrojan(proxy string) (model.Proxy, error) { func ParseTrojan(proxy string) (model.Proxy, error) {

View File

@ -5,7 +5,7 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"sub/model" "sub2clash/model"
) )
func ParseVless(proxy string) (model.Proxy, error) { func ParseVless(proxy string) (model.Proxy, error) {
@ -48,18 +48,22 @@ func ParseVless(proxy string) (model.Proxy, error) {
Fingerprint: params.Get("fp"), Fingerprint: params.Get("fp"),
Alpn: strings.Split(params.Get("alpn"), ","), Alpn: strings.Split(params.Get("alpn"), ","),
Servername: params.Get("sni"), Servername: params.Get("sni"),
WSOpts: model.WSOptsStruct{ RealityOpts: model.RealityOptsStruct{
PublicKey: params.Get("pbk"),
},
}
if params.Get("type") == "ws" {
result.WSOpts = model.WSOptsStruct{
Path: params.Get("path"), Path: params.Get("path"),
Headers: model.HeaderStruct{ Headers: model.HeaderStruct{
Host: params.Get("host"), Host: params.Get("host"),
}, },
}, }
GRPCOpts: model.GRPCOptsStruct{ }
if params.Get("type") == "grpc" {
result.GRPCOpts = model.GRPCOptsStruct{
GRPCServiceName: params.Get("serviceName"), GRPCServiceName: params.Get("serviceName"),
}, }
RealityOpts: model.RealityOptsStruct{
PublicKey: params.Get("pbk"),
},
} }
// 如果有节点名称 // 如果有节点名称
if len(serverInfo) == 2 { if len(serverInfo) == 2 {

View File

@ -6,11 +6,9 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"sub/model" "sub2clash/model"
) )
// vmess://eyJ2IjoiMiIsInBzIjoiXHU4MmYxXHU1NmZkLVx1NGYxOFx1NTMxNjMiLCJhZGQiOiJ0LmNjY2FvLmN5b3UiLCJwb3J0IjoiMTY2NDUiLCJpZCI6ImVmNmNiMGY0LTcwZWYtNDY2ZS04NGUwLWRiNDQwMWRmNmZhZiIsImFpZCI6IjAiLCJuZXQiOiJ3cyIsInR5cGUiOiJub25lIiwiaG9zdCI6IiIsInBhdGgiOiIiLCJ0bHMiOiIifQ==
func ParseVmess(proxy string) (model.Proxy, error) { func ParseVmess(proxy string) (model.Proxy, error) {
// 判断是否以 vmess:// 开头 // 判断是否以 vmess:// 开头
if !strings.HasPrefix(proxy, "vmess://") { if !strings.HasPrefix(proxy, "vmess://") {
@ -57,12 +55,14 @@ func ParseVmess(proxy string) (model.Proxy, error) {
SkipCertVerify: true, SkipCertVerify: true,
Servername: vmess.Add, Servername: vmess.Add,
Network: vmess.Net, Network: vmess.Net,
WSOpts: model.WSOptsStruct{ }
if vmess.Net == "ws" {
result.WSOpts = model.WSOptsStruct{
Path: vmess.Path, Path: vmess.Path,
Headers: model.HeaderStruct{ Headers: model.HeaderStruct{
Host: vmess.Host, Host: vmess.Host,
}, },
}, }
} }
return result, nil return result, nil
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,62 @@
port: 7890
socks-port: 7891
allow-lan: true
mode: Rule
log-level: info
proxies:
proxy-groups:
- name: 节点选择
type: select
proxies:
- 手动切换
- DIRECT
- name: 手动切换
type: select
proxies:
- name: 游戏平台
type: select
proxies:
- 节点选择
- 手动切换
- DIRECT
- name: 巴哈姆特
type: select
proxies:
- 节点选择
- 手动切换
- DIRECT
- name: 哔哩哔哩
type: select
proxies:
- 节点选择
- 手动切换
- DIRECT
- name: 全球直连
type: select
proxies:
- DIRECT
- 节点选择
- name: 广告拦截
type: select
proxies:
- REJECT
- DIRECT
- name: 漏网之鱼
type: select
proxies:
- 节点选择
- 手动切换
- DIRECT
rules:
- GEOSITE,private,全球直连
- GEOIP,private,全球直连
- GEOSITE,biliintl,哔哩哔哩
- GEOSITE,bilibili,哔哩哔哩
- GEOSITE,bahamut,巴哈姆特
- GEOSITE,CN,全球直连
- GEOIP,CN,全球直连
- GEOSITE,category-games,游戏平台
- GEOSITE,geolocation-!cn,节点选择
- GEOIP,ad,广告拦截
- GEOSITE,category-ads-all,广告拦截
- MATCH,漏网之鱼

View File

@ -1,12 +1,14 @@
package utils package utils
import ( import (
"errors"
"net/http" "net/http"
"sub2clash/config"
"time" "time"
) )
func GetWithRetry(url string) (resp *http.Response, err error) { func Get(url string) (resp *http.Response, err error) {
retryTimes := 3 retryTimes := config.Default.RequestRetryTimes
haveTried := 0 haveTried := 0
retryDelay := time.Second // 延迟1秒再重试 retryDelay := time.Second // 延迟1秒再重试
for haveTried < retryTimes { for haveTried < retryTimes {
@ -16,8 +18,13 @@ func GetWithRetry(url string) (resp *http.Response, err error) {
time.Sleep(retryDelay) time.Sleep(retryDelay)
continue continue
} else { } else {
// 如果文件大小大于设定,直接返回错误
if get != nil && get.ContentLength > config.Default.RequestMaxFileSize {
return nil, errors.New("文件过大")
}
return get, nil return get, nil
} }
} }
return nil, err return nil, err
} }

View File

@ -1,23 +1,29 @@
package utils package utils
import ( import (
"sort"
"strings" "strings"
"sub/model" "sub2clash/model"
"sub/parser" "sub2clash/parser"
) )
func GetContryCode(proxy model.Proxy) string { func GetContryName(proxy model.Proxy) string {
keys := make([]string, 0, len(model.CountryKeywords)) // 创建一个切片包含所有的国家映射
for k := range model.CountryKeywords { countryMaps := []map[string]string{
keys = append(keys, k) model.CountryFlag,
model.CountryChineseName,
model.CountryISO,
model.CountryEnglishName,
} }
sort.Strings(keys)
for _, k := range keys { // 对每一个映射进行检查
if strings.Contains(strings.ToLower(proxy.Name), strings.ToLower(k)) { for _, countryMap := range countryMaps {
return model.CountryKeywords[k] for k, v := range countryMap {
if strings.Contains(proxy.Name, k) {
return v
}
} }
} }
return "其他地区" return "其他地区"
} }
@ -29,17 +35,18 @@ var skipGroups = map[string]bool{
} }
func AddProxy(sub *model.Subscription, proxies ...model.Proxy) { func AddProxy(sub *model.Subscription, proxies ...model.Proxy) {
newContryNames := make([]string, 0, len(proxies)) newCountryGroupNames := make([]string, 0)
for p := range proxies {
proxy := proxies[p] for _, proxy := range proxies {
sub.Proxies = append(sub.Proxies, proxy) sub.Proxies = append(sub.Proxies, proxy)
haveProxyGroup := false haveProxyGroup := false
countryName := GetContryName(proxy)
for i := range sub.ProxyGroups { for i := range sub.ProxyGroups {
group := &sub.ProxyGroups[i] group := &sub.ProxyGroups[i]
groupName := []rune(group.Name)
proxyName := []rune(proxy.Name)
if string(groupName[:2]) == string(proxyName[:2]) || GetContryCode(proxy) == group.Name { if group.Name == countryName {
group.Proxies = append(group.Proxies, proxy.Name) group.Proxies = append(group.Proxies, proxy.Name)
haveProxyGroup = true haveProxyGroup = true
} }
@ -48,28 +55,25 @@ func AddProxy(sub *model.Subscription, proxies ...model.Proxy) {
group.Proxies = append(group.Proxies, proxy.Name) group.Proxies = append(group.Proxies, proxy.Name)
} }
} }
if !haveProxyGroup { if !haveProxyGroup {
contryCode := GetContryCode(proxy)
newGroup := model.ProxyGroup{ newGroup := model.ProxyGroup{
Name: contryCode, Name: countryName,
Type: "select", Type: "select",
Proxies: []string{proxy.Name}, Proxies: []string{proxy.Name},
IsCountryGrop: true,
} }
newContryNames = append(newContryNames, contryCode)
sub.ProxyGroups = append(sub.ProxyGroups, newGroup) sub.ProxyGroups = append(sub.ProxyGroups, newGroup)
newCountryGroupNames = append(newCountryGroupNames, countryName)
} }
} }
newContryNamesMap := make(map[string]bool)
for _, n := range newContryNames {
newContryNamesMap[n] = true
}
for i := range sub.ProxyGroups { for i := range sub.ProxyGroups {
if !skipGroups[sub.ProxyGroups[i].Name] && !newContryNamesMap[sub.ProxyGroups[i].Name] { if sub.ProxyGroups[i].IsCountryGrop {
newProxies := make( continue
[]string, len(newContryNames), len(newContryNames)+len(sub.ProxyGroups[i].Proxies), }
) if !skipGroups[sub.ProxyGroups[i].Name] {
copy(newProxies, newContryNames) sub.ProxyGroups[i].Proxies = append(newCountryGroupNames, sub.ProxyGroups[i].Proxies...)
sub.ProxyGroups[i].Proxies = append(newProxies, sub.ProxyGroups[i].Proxies...)
} }
} }
} }

View File

@ -4,11 +4,11 @@ import (
"fmt" "fmt"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"io" "io"
"sub/model" "sub2clash/model"
) )
func AddRulesByUrl(sub *model.Subscription, url string, proxy string) { func AddRulesByUrl(sub *model.Subscription, url string, proxy string) {
get, err := GetWithRetry(url) get, err := Get(url)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return return

View File

@ -7,18 +7,20 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"time" "time"
) )
var subsDir = "subs" var subsDir = "subs"
var fileLock sync.RWMutex
func LoadSubscription(url string, refresh bool) ([]byte, error) { func LoadSubscription(url string, refresh bool) ([]byte, error) {
if refresh { if refresh {
return FetchSubscriptionFromAPI(url) return FetchSubscriptionFromAPI(url)
} }
hash := md5.Sum([]byte(url)) hash := md5.Sum([]byte(url))
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:])+".yaml") fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
const refreshInterval = 5 * 60 // 5分钟 const refreshInterval = 500 * 60 // 5分钟
stat, err := os.Stat(fileName) stat, err := os.Stat(fileName)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
@ -38,6 +40,8 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) {
fmt.Println(err) fmt.Println(err)
} }
}(file) }(file)
fileLock.RLock()
defer fileLock.RUnlock()
subContent, err := io.ReadAll(file) subContent, err := io.ReadAll(file)
if err != nil { if err != nil {
return nil, err return nil, err
@ -49,17 +53,12 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) {
func FetchSubscriptionFromAPI(url string) ([]byte, error) { func FetchSubscriptionFromAPI(url string) ([]byte, error) {
hash := md5.Sum([]byte(url)) hash := md5.Sum([]byte(url))
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:])+".yaml") fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
resp, err := GetWithRetry(url) resp, err := Get(url)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func(Body io.ReadCloser) { defer resp.Body.Close()
err := Body.Close()
if err != nil {
fmt.Println(err)
}
}(resp.Body)
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err) return nil, fmt.Errorf("failed to read response body: %w", err)
@ -74,6 +73,8 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) {
fmt.Println(err) fmt.Println(err)
} }
}(file) }(file)
fileLock.Lock()
defer fileLock.Unlock()
_, err = file.Write(data) _, err = file.Write(data)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to write to sub.yaml: %w", err) return nil, fmt.Errorf("failed to write to sub.yaml: %w", err)