commit ba8c0faf0e1163dfb68f79f272ee8df20c180538 Author: ZxwyWebSite Date: Sun Dec 17 00:32:25 2023 +0800 2023-12-16 发布源码 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b21819b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +bin/ +# cache/ +data/ +outdated/ +# conf.ini +test.go +test_test.go \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5bdcb9d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Zxwy + +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. \ No newline at end of file diff --git a/build.go b/build.go new file mode 100644 index 0000000..6511949 --- /dev/null +++ b/build.go @@ -0,0 +1,152 @@ +//go:build ignore + +// 一键编译脚本 `go run build.go` +package main + +import ( + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/ZxwyWebSite/ztool" +) + +// 使用前请设置一下编译参数 +var ( + // 系统-架构-C编译工具 + list_os_arch_cc = map[string]map[string]string{ + "linux": { + "amd64": `x86_64-linux-gnu-gcc`, + "arm": `arm-linux-gnueabihf-gcc`, + }, + "windows": { + "amd64": `/usr/local/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-gcc`, + }, + } + // 架构-版本 + list_arch_ver = map[string][]string{ + "amd64": {`v2`}, //{`v1`, `v2`, `v3`}, + "arm": {`7`}, //{`5`, `6`, `7`}, + } +) + +const ( + // 运行参数 + args_name = `lx-source` // 程序名称 + args_path = `bin/` // 输出目录 + args_zpak = true // 打包文件 +) + +// 编译 +func doCompile(v_os, v_arch, v_archv, v_cc string) error { + // 构建 | 目标系统 | 目标架构 | 优化等级 | 不包含调试信息 | 使用外部链接器 | 输出详细操作 | 静态编译 | JSON解释器 + // `go build -o bin/$1-$(go env GOOS)-$(go env GOARCH)$(go env GOAMD64)$(go env GOARM) -ldflags "-s -w -linkmode external -extldflags '-v -static'" -tags=jsoniter` + fname := func() string { + name := strings.Join([]string{args_name, v_os, v_arch}, `-`) + var wexe string + if v_os == `windows` { + wexe = `.exe` + } + return ztool.Str_FastConcat(name, v_archv, wexe) + }() + pname := filepath.Clean(ztool.Str_FastConcat(args_path, fname)) + cmd := ztool.Str_FastConcat(`go build -o `, pname, ` -ldflags "-s -w -linkmode external -extldflags ''" -tags "go_json"`) // go_json | json(std) | jsoniter | sonic + // 输出要执行的命令 + ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`执行命令:`, cmd)) + // 设置环境&执行编译 + envmap := map[string]string{ + `GOOS`: v_os, + `GOARCH`: v_arch, + `AR`: `llvm-ar`, // 脚本默认使用Clang的Archiver, 没装llvm请注释掉以使用系统默认值 + `CC`: v_cc, + `CGO_ENABLED`: `1`, + ztool.Str_FastConcat(`GO`, strings.ToUpper(v_arch)): v_archv, // GO{ARCH} Eg: GOARM, GOAMD64 + } + setenv := func(env map[string]string) error { + var handler ztool.Err_HandleList + for k, v := range env { + handler.Do(func() error { + return os.Setenv(k, v) + }) + } + return handler.Err + } + if err := setenv(envmap); err != nil { + return err + } + if err := ztool.Cmd_aSyncExec(cmd); err != nil { + return err + } + // 打包文件 + if args_zpak { // DoSomeThing... + if !ztool.Fbj_IsExists(`archieve`) { + os.MkdirAll(filepath.Join(args_path, `archieve`), 0755) + } + if err := ztool.Pak_ZipFile(pname, ztool.Str_FastConcat(filepath.Join(args_path, `archieve`, fname), `.zip`), ztool.Pak_ZipConfig{UnPath: true}); err != nil { + ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`打包["`, pname, `"]出错:`, err.Error())) + } else { + ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`打包["`, pname, `"]完成`)) + } + } + return nil +} + +func init() { + if runtime.GOOS != `linux` { + ztool.Cmd_FastPrintln("简易脚本,未对Linux以外系统做适配,请复制执行以下命令编译:\ngo build -ldflags \"-s -w\" -tags \"go_json\"\n如无报错则会在本目录生成名为lx-source的可执行文件。") + os.Exit(1) + } + ztool.Cmd_FastPrintln(ztool.Str_FastConcat(` + ================================ + | Golang 一键编译脚本 + | 程序名称:`, args_name, ` + | 输出目录:`, args_path, ` + | 打包文件:`, strconv.FormatBool(args_zpak), ` + ================================ +`)) +} + +func main() { + var handler = ztool.Err_NewDefHandleList() + handler.Do(func() error { + // 检测入口函数是否存在 + if !ztool.Fbj_IsExists(`main.go`) { + ztool.Cmd_FastPrintln(`入口函数不存在,请在源码根目录运行此程序!`) + return ztool.Err_EsContinue + } + // 检测输出目录是否存在 (已在zTool中增加相关检测) + // if !ztool.Fbj_IsExists(args_path) { + // ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`输出目录 "`, args_path, `" 不存在,尝试创建`)) + // return os.MkdirAll(args_path, 0755) + // } + return nil + }) + for v_os, v_arch_cc := range list_os_arch_cc { + for v_arch, v_cc := range v_arch_cc { + // 检测CC是否存在 + o, e := ztool.Cmd_aWaitExec(ztool.Str_FastConcat(`which `, v_cc)) + if !ztool.Fbj_IsExists(v_cc) && (e != nil || o == ``) { + ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`编译工具 ["`, v_cc, `"] 不存在,跳过 `, v_arch, ` 架构`)) + continue + } + // 继续编译 + for _, v_arch_ver := range list_arch_ver[v_arch] { + // handler.Do(func() error { return tool.ErrContinue }) + handler.Do(func() error { + // (测试) 快速输出编译参数 + ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`开始编译:`, v_os, `/`, v_arch, `/`, v_arch_ver, `/`, `[`, v_cc, `], 任务编号 `, handler.NumStr())) + // 编译对应文件 + // return nil + return doCompile(v_os, v_arch, v_arch_ver, v_cc) + }) // handler.Do(func() error { return doCompile(v_os, v_arch, v_arch_ver, v_cc) }) + } + } + } + if res := handler.Result(); res != nil { + ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`发生错误:`, res.Errors())) + return + } + ztool.Cmd_FastPrintln(`恭喜!所有任务成功完成`) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f4375f5 --- /dev/null +++ b/go.mod @@ -0,0 +1,41 @@ +module lx-source + +go 1.21.4 + +require ( + github.com/ZxwyWebSite/ztool v0.0.1 + github.com/gin-contrib/gzip v0.0.6 + github.com/gin-gonic/gin v1.9.1 + github.com/mattn/go-colorable v0.1.13 // indirect +) + +require ( + github.com/bytedance/sonic v1.10.2 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/ZxwyWebSite/ztool v0.0.1 => ./pkg/ztool // ../ztool diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..32137ca --- /dev/null +++ b/go.sum @@ -0,0 +1,141 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= +github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +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.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..74b8c35 --- /dev/null +++ b/main.go @@ -0,0 +1,191 @@ +package main + +import ( + "context" + "encoding/base64" + "flag" + "lx-source/src/caches" + "lx-source/src/caches/localcache" + "lx-source/src/env" + "lx-source/src/router" + "lx-source/src/sources" + "lx-source/src/sources/builtin" + "math/rand" + "net/http" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "github.com/ZxwyWebSite/ztool" + "github.com/ZxwyWebSite/ztool/logs" + "github.com/gin-gonic/gin" +) + +func genAuth() { + ga := env.Loger.NewGroup(`LxM-Auth`) + // 检测Key是否存在, 否则生成并保存 + if env.Config.Auth.ApiKey_Value == `` { + randomBytes := func(size int) []byte { + buf := make([]byte, size) + for i := 0; i < size; i++ { + buf[i] = byte(rand.New(rand.NewSource(time.Now().UnixNano() + int64(i*i+rand.Intn(256)))).Intn(256)) + } + return buf + } + pass := base64.StdEncoding.EncodeToString(randomBytes(4 * 4)) + env.Config.Auth.ApiKey_Value = pass // env.Config.Apis.LxM_Auth + ga.Info(`已生成默认连接码: %q`, pass) + ga.Info(`可在配置文件 [Auth].ApiKey_Value 中修改`) //可在配置文件 [Apis].LxM_Auth 中修改, 填写 "null" 关闭验证 + if err := env.Cfg.Save(``); err != nil { + ga.Error(`写入配置文件失败: %s, 将导致下次启动时连接码发生变化`, err) + } + } + if !env.Config.Auth.ApiKey_Enable { + ga.Warn(`已关闭Key验证, 公开部署可能导致安全隐患`) + } else { + ga.Warn(`已开启Key验证, 记得在脚本中填写 apipass=%q`, env.Config.Auth.ApiKey_Value) + } +} + +// 加载文件日志 (请在初始化配置文件后调用) +func loadFileLoger() { + // 最后加载FileLoger保证必要日志已输出 (Debug模式强制在控制台输出日志) + if env.Config.Main.LogPath != `` { + lg := env.Loger.NewGroup(`FileLoger`) + printout := env.Config.Main.Print // || env.Config.Main.Debug + _, do, err := env.Loger.SetOutFile(ztool.Str_FastConcat(env.RunPath, env.Config.Main.LogPath), printout) + if err == nil { + env.Defer.Add(do) + gin.DefaultWriter = env.Loger.GetOutput() + gin.ForceConsoleColor() + // lg.Info(`文件日志初始化成功`) + } else { + lg.Error(`文件日志初始化失败:%v`, err) + } + } +} + +// 初始化 +func init() { + ztool.Cmd_FastPrint(ztool.Str_FastConcat(` + __ __ __ ______ ______ __ __ ____ ______ ______ + / / / / / / / ____/ / __ / / / / / / __ \ / ____/ / ____/ + / / / /_/ / __ / /___ / / / / / / / / / /_/ / / / / /___ + / / \_\ \ /_/ /___ / / / / / / / / / / ___/ / / / ____/ + / /___ / / / / ____/ / / /_/ / / /_/ / / / \ / /___ / /___ +/_____/ /_/ /_/ /_____/ /_____/ /_____/ /_/ \_\ /_____/ /_____/ +======================================================================= + Version: `, env.Version, ` Github: https://github.com/ZxwyWebSite/lx-source +`, "\n")) + env.RunPath, _ = os.Getwd() + var confPath string + flag.StringVar(&confPath, `c`, ztool.Str_FastConcat(env.RunPath, `/data/conf.ini`), `指定配置文件路径`) + flag.Parse() + // fileLoger() // 注:记录日志必然会影响性能,自行选择是否开启 + // logs.DefLogger(`LX-SOURCE`, logs.LevelDebu) + // logs.Main = `LX-SOURCE` + env.Cfg.MustInit(confPath) //conf.InitConfig(confPath) + env.Loger.NewGroup(`ServHello`).Info(`欢迎使用 LX-SOURCE 洛雪音乐自定义源`) + if !env.Config.Main.Debug { + gin.SetMode(gin.ReleaseMode) + } else { + logs.Levell = logs.LevelDebu // logs.Level = 3 + env.Loger.NewGroup(`DebugMode`).Debug(`已开启调试模式, 将输出更详细日志 (配置文件中 [Main].Debug 改为 false 关闭)`) + } + genAuth() +} + +func main() { + defer env.Defer.Do() + + // 初始化缓存 + icl := env.Loger.NewGroup(`InitCache`) + switch env.Config.Cache.Mode { + case `0`, `off`: + // NothingToDo... (已默认禁用缓存) + break + case `1`, `local`: + // 注:由于需要修改LocalCachePath参数,必须在InitRouter之前执行 + cache, err := caches.New(&localcache.Cache{ + Path: filepath.Join(env.RunPath, env.Config.Cache.Local_Path), + Bind: env.Config.Cache.Local_Bind, + }) + if err != nil { + icl.Error(`驱动["local"]初始化失败: %v, 将禁用缓存功能`, err) + } + caches.UseCache = cache + icl.Warn(`本地缓存绑定地址: %q, 请确认其与实际访问地址相符`, env.Config.Cache.Local_Bind) + // LocalCachePath = filepath.Join(runPath, env.Config.Cache.Local_Path) + // UseCache = &localcache.Cache{ + // Path: LocalCachePath, + // Addr: env.Config.Apis.BindAddr, + // } + // icl.Info(`使用本地缓存,文件路径 %q,绑定地址 %v`, LocalCachePath, env.Config.Apis.BindAddr) + case `2`, `cloudreve`: + icl.Fatal(`Cloudreve驱动暂未完善,未兼容新版调用方式,当前版本禁用`) + // icl.Warn(`Cloudreve驱动暂未完善,使用非本机存储时存在兼容性问题,请谨慎使用`) + // cs, err := cloudreve.NewSite(&cloudreve.Config{ + // SiteUrl: env.Config.Cache.Cloud_Site, + // Username: env.Config.Cache.Cloud_User, + // Password: env.Config.Cache.Cloud_Pass, + // Session: env.Config.Cache.Cloud_Sess, + // }) + // if err != nil { + // icl.Error(`驱动["cloudreve"]初始化失败: %v, 将禁用缓存功能`, err) + // } + // UseCache = &crcache.Cache{ + // Cs: cs, + // Path: env.Config.Cache.Cloud_Path, + // IsOk: err == nil, + // } + default: + icl.Error(`未定义的缓存模式,请检查配置 [Cache].Mode,本次启动禁用缓存`) + } + + // 初始化音乐源 + ise := env.Loger.NewGroup(`InitSource`) + switch env.Config.Source.Mode { + case `0`, `off`: + break + case `1`, `builtin`: + sources.UseSource = &builtin.Source{} + case `2`, `custom`: + ise.Fatal(`暂未实现账号解析源`) + default: + ise.Error(`未定义的音乐源,请检查配置 [Source].Mode,本次启动禁用内置源`) + } + + // 启动Http服务 + env.Loger.NewGroup(`ServStart`).Info(`服务端启动, 监听地址 %s`, env.Config.Main.Listen) + loadFileLoger() + r := router.InitRouter() //InitRouter() + server := &http.Server{ + Addr: env.Config.Main.Listen, + Handler: r, + } + go func() { + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + env.Loger.NewGroup(`InitRouter().Run`).Fatal(`%s`, err) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) + <-quit + sc := env.Loger.NewGroup(`ServClose`) + sc.Info(`等待结束活动连接...`) // Shutdown Server ... + + ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + sc.Fatal(`未安全退出: %s`, err) // Server Shutdown + } + sc.Info(`已安全退出 :)`) // Server exited + + // if err := InitRouter().Run(env.Config.Main.Listen); err != nil { + // env.Loger.NewGroup(`InitRouter().Run`).Fatal(`%s`, err) + // } +} diff --git a/pkg/ztool b/pkg/ztool new file mode 120000 index 0000000..cebc151 --- /dev/null +++ b/pkg/ztool @@ -0,0 +1 @@ +../../ztool \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..62acf04 --- /dev/null +++ b/readme.md @@ -0,0 +1,100 @@ +``` +(屏幕宽度问题头图无法正常显示请忽略) + __ __ __ ______ ______ __ __ ____ ______ ______ + / / / / / / / ____/ / __ / / / / / / __ \ / ____/ / ____/ + / / / /_/ / __ / /___ / / / / / / / / / /_/ / / / / /___ + / / \_\ \ /_/ /___ / / / / / / / / / / ___/ / / / ____/ + / /___ / / / / ____/ / / /_/ / / /_/ / / / \ / /___ / /___ +/_____/ /_/ /_/ /_____/ /_____/ /_____/ /_/ \_\ /_____/ /_____/ +======================================================================= +``` +## ZxwyWebSite/LX-Source +### 简介 ++ LX-Music 解析源 (洛雪音乐自定义源) ++ 使用Golang编写,运行效率较高 ++ 测试阶段,不代表最终品质 ++ 验证部分暂未完善,建议仅本地使用,不要公开发布 + + +### 使用 +#### 服务端 ++ 到Release下载对应平台可执行文件运行 ++ 或下载源码自行编译 +#### 客户端 ++ 使用自定义源脚本:`data/public/lx-custom-source.js` ++ (请先运行服务端以释放资源文件) ++ **修改 apiaddr 为服务端地址,apipass 为验证Key** + +### 配置 ++ 第一次运行自动生成配置后按注释填写即可 ++ 位置:`{运行目录}/data/conf.ini` ++ **注:默认使用本地缓存,请修改 [Cache].Local_Bind 为实际访问地址** + +### 功能 *(未全部实现)* ++ 兼容原版测试接口 ++ 提供自定义源脚本 ++ 支持多种工作模式 *(PART)* + - 仅解析(0): 解析后返回原始外链 + - 仅缓存(1): 缓存并返回本地数据外链 + - 都使用(2): 无缓存返回原始外链并缓存, 有缓存返回本地数据外链 ++ 设置缓存直链类型 *(TODO)* + - 永久链(0): `file/:s/:id/:q`, 和解析参数相同 + - 临时链(1): `file/:{time.unix}/:{md5(cquery)}`, 参考网易云样式, 有效期默认10分钟 +#### Api ++ `/` 获取服务端信息 ++ `/link/:s/:id/:q` 查询音乐链接 ++ `/file/` 本地缓存访问地址 + + +### 音乐源 ++ 内置源 (抓取自网络公开接口) **注:文明上网,请勿滥用** ++ 账号源 (登录Vip账号解析) **注:可能导致封号,如出问题本项目不负责** + +### 开发 ++ 环境要求:Golang 1.21 (建议 >=1.20) ++ 可不开启CGO编译 ++ 源码较乱,暂未整理... ++ zTool包不存在:解压发布页 `ztool.zip` 放在源码上级目录即可使用 +#### 源码结构 ++ / + - pkg/ 依赖包,一般在外部调用,不轻易修改 + - src/ 源码包,用于实现各种功能 + * env 公用变量,需要全局调用的参数 + * caches 文件缓存封装 + * router Gin路由 + * middleware 请求中间件 + * sources 音乐源 + + + - build.go 快速构建脚本 (请先根据本地环境编辑配置) + - main.go 主程序 + +### 其它 ++ 基于 Golang + Gin框架 编写 ++ 部分功能参考 [Python版](https://github.com/lxmusics/lx-music-api-server-python) 实现 + +### 更新 +#### ~2023-12-16 ++ 参考Python版移植部分功能 ++ 完善、优化逻辑 ++ 发布源码 +#### 2023-10-21 ++ 立项制作 + +### 项目协议 + +本项目基于 [MIT] 许可证发行,以下协议是对于 MIT 原协议的补充,如有冲突,以以下协议为准。 + +词语约定:本协议中的“本项目”指本开源项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。 + +1. 本项目的数据来源原理是从各官方音乐平台的公开服务器中拉取数据,经过对数据简单地筛选与合并后进行展示,因此本项目不对数据的准确性负责。 +2. 使用本项目的过程中可能会产生版权数据,对于这些版权数据,本项目不拥有它们的所有权,为了避免造成侵权,使用者务必在**24 小时**内清除使用本项目的过程中所产生的版权数据。 +3. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。 +4. 本项目完全免费,且开源发布于 GitHub 面向全世界人用作对技术的学习交流,本项目不对项目内的技术可能存在违反当地法律法规的行为作保证,**禁止在违反当地法律法规的情况下使用本项目**,对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。 + +若你使用了本项目,将代表你接受以上协议。 + +音乐平台不易,请尊重版权,支持正版。 +本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。 +若对此有疑问请 mail to: admin+zxwy.tk (请将`+`替换为`@`) + \ No newline at end of file diff --git a/src/caches/cache.go b/src/caches/cache.go new file mode 100644 index 0000000..f29693a --- /dev/null +++ b/src/caches/cache.go @@ -0,0 +1,90 @@ +package caches + +import ( + "lx-source/src/env" + + "github.com/ZxwyWebSite/ztool" +) + +type ( + // 查询参数 + Query struct { + Source string // source 平台 wy, mg + MusicID string // sid 音乐ID wy: songmid, mg: copyrightId + Quality string // quality 音质 128k / 320k / flac / flac24bit + Extname string // rext 扩展名 mp3 / flac (没有前缀点) + query string // 查询字符串缓存 + } + // 缓存需实现以下接口 + Cache interface { + // 获取缓存 (查询参数 query)(外链) + /* `wy/10086/128k.mp3`->`http://192.168.10.22:1011/file/wy/10086/128k.mp3` */ + Get(*Query) string + // 设置缓存 (查询参数 query, 音乐直链 link)(外链) + /* (`wy/10086/128k.mp3`,`https://xxx.xxxx.xx/file.mp3`)->`http://192.168.10.22:1011/file/wy/10086/128k.mp3` */ + Set(*Query, string) string + // 可用状态 true/false + Stat() bool + // 初始化 ()(错误) + Init() error + } +) + +// 默认无缓存的缓存 +type Nullcache struct{} + +func (*Nullcache) Get(*Query) string { return `` } +func (*Nullcache) Set(*Query, string) string { return `` } +func (*Nullcache) Stat() bool { return false } +func (*Nullcache) Init() error { return nil } + +var ( + Loger = env.Loger.NewGroup(`Caches`) + + UseCache Cache = &Nullcache{} + + // ErrNotInited = errors.New(`缓存策略未初始化`) +) + +// 根据音质判断文件后缀 +func rext(q string) string { + if q == `128k` || q == `320k` { + return `mp3` + } + return `flac` +} + +// 生成查询参数 (必须使用此函数初始化) +func NewQuery(s, id, q string) *Query { + return &Query{ + Source: s, + MusicID: id, + Quality: q, + Extname: rext(q), + } +} + +// 获取旧版查询字符串 +func (c *Query) Query() string { + if c.query == `` { + c.query = ztool.Str_FastConcat(c.Source, `/`, c.MusicID, `/`, c.Quality, `.`, c.Extname) + } + return c.query +} + +// 初始化缓存 +func New(c Cache) (Cache, error) { + err := c.Init() + return c, err + // if err != nil { + // return nil, err + // } + // return c, nil +} +func MustNew(c Cache) Cache { + out, err := New(c) + if err != nil { + panic(err) + } + return out +} diff --git a/src/caches/cloudcache/cloud.go b/src/caches/cloudcache/cloud.go new file mode 100644 index 0000000..c547d28 --- /dev/null +++ b/src/caches/cloudcache/cloud.go @@ -0,0 +1 @@ +package cloudcache diff --git a/src/caches/localcache/local.go b/src/caches/localcache/local.go new file mode 100644 index 0000000..fa925ab --- /dev/null +++ b/src/caches/localcache/local.go @@ -0,0 +1,91 @@ +package localcache + +import ( + "errors" + "lx-source/src/caches" + "lx-source/src/env" + "net/url" + "os" + "strings" + + "github.com/ZxwyWebSite/ztool" +) + +type Cache struct { + Path string // 本地缓存目录 cache + Bind string // Api地址,用于生成外链 http://192.168.10.22:1011/ + state bool // 激活状态 +} + +var loger = caches.Loger.AppGroup(`local`) + +func (c *Cache) getLink(q string) string { + return ztool.Str_FastConcat(c.Bind, `/file/`, q) // c.Addr + `file/` + q +} + +func (c *Cache) Get(q *caches.Query) string { + // 加一层缓存,减少重复检测文件造成的性能损耗 + if _, ok := env.Cache.Get(q.Query()); !ok { + if _, e := os.Stat(ztool.Str_FastConcat(c.Path, `/`, q.Query())); e != nil { + return `` + } + env.Cache.Set(q.Query(), struct{}{}, 3600) + } + return c.getLink(q.Query()) + // fpath := filepath.Join(c.Path, q.Source, q.MusicID, q.Quality) + // if _, e := os.Stat(fpath); e != nil { + // return `` + // } + // return c.getLink(fpath) +} + +func (c *Cache) Set(q *caches.Query, l string) string { + err := ztool.Net_DownloadFile(l, ztool.Str_FastConcat(c.Path, `/`, q.Query()), nil) + if err != nil { + loger.Error(`DownloadFile: %v`, err) + return `` + } + env.Cache.Set(q.Query(), struct{}{}, 3600) + return c.getLink(q.Query()) + // fpath := filepath.Join(c.Path, q.String) + // os.MkdirAll(filepath.Dir(fpath), fs.ModePerm) + // g := c.Loger.NewGroup(`localcache`) + // ret, err := ztool.Net_HttpReq(http.MethodGet, l, nil, nil, nil) + // if err != nil { + // g.Warn(`HttpReq: %s`, err) + // return `` + // } + // if err := os.WriteFile(fpath, ret, fs.ModePerm); err != nil { + // g.Warn(`WriteFile: %s`, err) + // return `` + // } + // return c.getLink(fpath) +} + +func (c *Cache) Stat() bool { + return c.state +} + +func (c *Cache) Init() error { + if c.Bind == `` { + return errors.New(`请输入Api地址以生成外链`) + } else { + ubj, err := url.Parse(c.Bind) + if err != nil { + return err + } + ubj.Path = strings.TrimSuffix(ubj.Path, `/`) + c.Bind = ubj.String() + } + c.state = true + return nil +} + +// func New(path, addr string, loger *logs.Logger) *Cache { +// return &Cache{ +// Path: path, +// Addr: addr, +// Loger: loger, +// emsg: cache.ErrNotInited, +// } +// } diff --git a/src/env/env.go b/src/env/env.go new file mode 100644 index 0000000..96b03f2 --- /dev/null +++ b/src/env/env.go @@ -0,0 +1,150 @@ +// 全局变量 +package env + +import ( + "github.com/ZxwyWebSite/ztool" + "github.com/ZxwyWebSite/ztool/cache/memo" + "github.com/ZxwyWebSite/ztool/conf" + "github.com/ZxwyWebSite/ztool/logs" +) + +const ( + Version = `1.0.2-β0.1` +) + +var ( + RunPath string +) + +// 配置结构 +/* + 注:Mode字段建议使用名称方式调用,序号可能频繁更改 + e.g. 0: off(), 1: builtin(), 2: custom() + 序号 名称 描述 +*/ +type ( + Conf_Main struct { + Debug bool `comment:"调试模式"` + Listen string `comment:"监听地址"` + Gzip bool `comment:"开启GZip (对已压缩的内容使用会产生反效果)"` + LogPath string `comment:"文件日志路径,不填禁用"` + Print bool `comment:"控制台输出"` + } + Conf_Apis struct { + // BindAddr string `comment:"外部访问地址,用于生成文件链接"` + // LxM_Auth string `comment:"验证Key,自动生成,填写null禁用"` + } + Conf_Auth struct { + // ApiKey + ApiKey_Enable bool `comment:"是否开启Key验证"` + ApiKey_Value string `comment:"验证Key值,留空自动生成"` + // 速率限制 + RateLimit_Enable bool `comment:"是否开启速率限制"` + RateLimit_Global int `comment:"全局速率限制,单位秒"` + RateLimit_Single int `comment:"单IP速率限制,单位秒"` + // 黑白名单 + BanList_Mode string `comment:"名单模式 0: off(关闭), 1: white(白名单), 2: black(黑名单)"` + BanList_White []string `comment:"host白名单"` + BanList_Black []string `comment:"host黑名单"` + } + Conf_Source struct { + Mode string `comment:"音乐来源 0: off(关闭 仅本地), 1: builtin(内置), 2: custom(登录账号 暂不支持)"` + // 伪装IP + FakeIP_Mode string `comment:"伪装IP模式 0: off(关闭), 1: req(传入值), 2: val(静态)"` + FakeIP_Value string `comment:"静态伪装IP"` + // 代理 + Proxy_Enable bool `comment:"使用代理"` + Proxy_Address string `comment:"代理地址 (支持http, socks)"` + // 平台账号 + // ...(待实现) + } // `comment:""` + Conf_Script struct { + Ver string `comment:"自定义脚本版本" json:"ver"` + Log string `comment:"更新日志" json:"log"` + Url string `comment:"脚本下载地址 (public目录内文件名)" json:"url"` + Force bool `comment:"强制推送更新" json:"force"` + } + Conf_Cache struct { + Mode string `comment:"缓存模式 0: off(关闭), 1: local(本地), 2: cloudreve(云盘 未完善)"` + // 本地 + Local_Path string `comment:"本地缓存保存路径"` + Local_Bind string `comment:"本地缓存外部访问地址"` + // 云盘 + Cloud_Site string `comment:"Cloudreve站点地址"` + Cloud_User string `comment:"Cloudreve用户名"` + Cloud_Pass string `comment:"Cloudreve密码"` + Cloud_Sess string `comment:"Cloudreve会话"` + Cloud_Path string `comment:"Cloudreve存储路径"` + } + Conf struct { + Main *Conf_Main `comment:"程序主配置"` + Apis *Conf_Apis `comment:"接口设置"` + Auth *Conf_Auth `comment:"访问控制"` + Source *Conf_Source `comment:"解析源配置"` + Script *Conf_Script `comment:"自定义脚本更新"` // ini:",omitempty" + Cache *Conf_Cache `comment:"音乐缓存设置"` + } +) + +var ( + // 默认配置 + defCfg = Conf{ + Main: &Conf_Main{ + Debug: false, + Listen: `0.0.0.0:1011`, + Gzip: false, + LogPath: `/data/logfile.log`, + Print: true, + }, + Apis: &Conf_Apis{ + // BindAddr: `http://192.168.10.22:1011/`, + }, + Auth: &Conf_Auth{ + ApiKey_Enable: true, + RateLimit_Enable: false, + RateLimit_Global: 1, + RateLimit_Single: 5, + BanList_Mode: `off`, + BanList_White: []string{`127.0.0.1`}, + }, + Source: &Conf_Source{ + Mode: `builtin`, + FakeIP_Mode: `0`, + FakeIP_Value: `192.168.10.2`, + Proxy_Enable: false, + Proxy_Address: `{protocol}://({user}:{password})@{address}:{port}`, + }, + Script: &Conf_Script{ + Log: `发布更新 (请删除旧源后重新导入):进行了部分优化,修复了部分Bug`, // 更新日志 + + Ver: `1.0.1`, // 自定义脚本版本 + Url: `lx-custom-source.js`, // 脚本下载地址 + Force: false, // 强制推送更新 + }, + Cache: &Conf_Cache{ + Mode: `local`, // 缓存模式 + Local_Path: `data/cache`, + Local_Bind: `http://127.0.0.1:1011/`, + Cloud_Site: `https://cloudreveplus-demo.onrender.com/`, + Cloud_User: `admin@cloudreve.org`, + Cloud_Pass: `CloudrevePlusDemo`, + Cloud_Sess: ``, + Cloud_Path: `/Lx-Source/cache`, + }, + } + Config = defCfg + // 通用对象 + Loger = logs.NewLogger(`LX-SOURCE`) + Cfg, _ = conf.New(&Config, &conf.Confg{ + AutoFormat: true, + UseBuf: true, + UnPretty: true, + Loger: Loger.NewGroup(`Config`), + }) + Defer = new(ztool.Err_DeferList) + Cache = memo.NewMemoStoreConf(Loger, 300) //memo.NewMemoStore() +) + +// func init() { + +// } diff --git a/src/middleware/auth/auth.go b/src/middleware/auth/auth.go new file mode 100644 index 0000000..f78d7ca --- /dev/null +++ b/src/middleware/auth/auth.go @@ -0,0 +1,25 @@ +// 全局验证 +package auth + +import ( + "lx-source/src/env" + "lx-source/src/middleware/resp" + + "github.com/gin-gonic/gin" +) + +// 请求验证 +func AuthHandler(c *gin.Context) { + loger := env.Loger.NewGroup(`AuthHandler`) + resp.Wrap(c, func() *resp.Resp { + // ApiKey + if env.Config.Auth.ApiKey_Enable { + if auth := c.Request.Header.Get(`X-LxM-Auth`); auth != env.Config.Auth.ApiKey_Value { + loger.Debug(`验证失败: %q`, auth) + return &resp.Resp{Code: 3, Msg: `验证Key失败, 请联系网站管理员`} + } + } + return nil + }) + // c.Next() +} diff --git a/src/middleware/loadpublic/loadpublic.go b/src/middleware/loadpublic/loadpublic.go new file mode 100644 index 0000000..4a298fa --- /dev/null +++ b/src/middleware/loadpublic/loadpublic.go @@ -0,0 +1,70 @@ +// 静态资源 +package loadpublic + +import ( + "embed" + "fmt" + "io" + "io/fs" + "lx-source/src/env" + "net/http" + "path/filepath" + + "github.com/ZxwyWebSite/ztool" + "github.com/gin-gonic/gin" +) + +//go:embed public +var publicEM embed.FS // 打包默认Public目录 src/router/router.go + +// 载入Public目录并设置路由 +func LoadPublic(r *gin.Engine) { + pf := env.Loger.NewGroup(`PublicFS`) + var httpFS http.FileSystem + dir := ztool.Str_FastConcat(env.RunPath, `/data/public`) + if !ztool.Fbj_IsExists(dir) { + pf.Info(`不存在Public目录, 释放默认静态文件`) + publicFS, err := fs.Sub(publicEM, `public`) + if err != nil { + pf.Fatal(`内置Public目录载入错误: %s, 请尝试重新编译`, err) + } + walk := func(relPath string, d fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf(`无法获取[%q]的信息: %s`, relPath, err) + } + if !d.IsDir() { + out, err := ztool.Fbj_CreatFile(filepath.Join(dir, relPath)) + if err != nil { + return fmt.Errorf(`无法创建文件[%q]: %s`, relPath, err) + } + defer out.Close() + pf.Info(`导出 [%q]...`, relPath) + obj, err := publicFS.Open(relPath) + if err != nil { + return fmt.Errorf(`无法打开文件[%q]: %s`, relPath, err) + } + if _, err := io.Copy(out, obj); err != nil { + return fmt.Errorf(`无法写入文件[%q]: %s`, relPath, err) + } + } + return nil + } + if err := fs.WalkDir(publicFS, `.`, walk); err != nil { + pf.Fatal(`无法释放静态文件: %s`, err) + // pf.Warn(`正在使用内置Public目录, 将无法自定义静态文件`) + // httpFS = http.FS(publicFS) + } else { + pf.Info(`全部静态资源导出完成, 祝你使用愉快 ^_^`) + } + } + httpFS = gin.Dir(dir, false) + r.GET(`/:file`, func(c *gin.Context) { + file := c.Param(`file`) + switch file { + case `favicon.ico`: + c.FileFromFS(`icon.ico`, httpFS) + default: + c.FileFromFS(file, httpFS) + } + }) +} diff --git a/src/middleware/loadpublic/public/icon.ico b/src/middleware/loadpublic/public/icon.ico new file mode 100644 index 0000000..dee4340 Binary files /dev/null and b/src/middleware/loadpublic/public/icon.ico differ diff --git a/src/middleware/loadpublic/public/lx-custom-source.js b/src/middleware/loadpublic/public/lx-custom-source.js new file mode 100644 index 0000000..0ce617f --- /dev/null +++ b/src/middleware/loadpublic/public/lx-custom-source.js @@ -0,0 +1,130 @@ +/** + * @name Lx-Custom-Source + * @description Client + * version 1.0.1 + * @author Zxwy + * @homepage https://github.com/ZxwyWebSite/lx-source + */ + +// 脚本配置 +const version = '1.0.2' // 脚本版本 +const apiaddr = 'http://127.0.0.1:1011/' // 服务端地址,末尾加斜杠 +const apipass = '' // 验证密钥,由服务端自动生成 '${apipass}' +const devmode = true // 调试模式 + +// 常量 & 默认值 +const { EVENT_NAMES, request, on, send } = window.lx ?? globalThis.lx +const defaults = { + type: 'music', // 目前固定为 music + actions: ['musicUrl'], // 目前固定为 ['musicUrl'] + qualitys: ['128k', '320k', 'flac', 'flac24bit'], // 当前脚本的该源所支持获取的Url音质,有效的值有:['128k', '320k', 'flac', 'flac24bit'] +} +const defheaders = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36 HBPC/12.1.2.300', + 'Accept': 'application/json, text/plain, */*', + 'X-LxM-Auth': apipass, +} +const conf = { + api: { + addr: apiaddr, // 服务端地址,末尾加斜杠 + pass: apipass, // 验证密钥,由服务端自动生成 '${apipass}' + glbv: 'v1' // 大版本号 + }, + info: { + version: version, // 脚本版本 + devmode: devmode, // 调试模式 + }, +} + +const httpRequest = (url, options) => new Promise((resolve, reject) => { + options.headers = { ...defheaders, ...options.headers } // 添加默认请求头 + request(url, options, (err, resp) => { + if (err) return reject(err) + resolve(resp.body) + }) +}) + +const musicUrl = async (source, info, quality) => { + const id = info.hash ?? info.copyrightId ?? info.songmid // 音乐id kg源为hash, mg源为copyrightId + const query = `${source}/${id}/${quality}`; console.log('创建任务: %s, 音乐信息: %O', query, info) + const body = await httpRequest(`${apiaddr}link/${query}`, { method: 'get' }); console.log('返回数据: %O', body) + return body.data != '' ? body.data : Promise.reject(body.msg) // 没有获取到链接则将msg作为错误抛出 +} + +// 注册应用API请求事件 +// source 音乐源,可能的值取决于初始化时传入的sources对象的源key值 +// info 请求附加信息,内容根据action变化 +// action 请求操作类型,目前只有musicUrl,即获取音乐URL链接, +// 当action为musicUrl时info的结构:{type, musicInfo}, +// info.type:音乐质量,可能的值有128k / 320k / flac / flac24bit(取决于初始化时对应源传入的qualitys值中的一个), +// info.musicInfo:音乐信息对象,里面有音乐ID、名字等信息 +on(EVENT_NAMES.request, ({ source, action, info }) => { + // 回调必须返回 Promise 对象 + switch (action) { + // action 为 musicUrl 时需要在 Promise 返回歌曲 url + case 'musicUrl': + return musicUrl(source, info.musicInfo, info.type).catch(err => { + console.log('发生错误: %o', err) + return Promise.reject(err) + }) + } +}) + +// 脚本初始化 (目前只有检查更新) +const init = () => { + 'use strict'; + console.log('初始化脚本, 版本: %s, 服务端地址: %s', version, apiaddr) + var stat = false; var msg = ''; var updUrl = ''; var sourcess = {} + httpRequest(apiaddr, { method: 'get' }) + .catch((err) => { msg = '初始化失败: ' + err ?? '连接服务端超时'; console.log(msg) }) + .then((body) => { + if (!body) { msg = '初始化失败:' + '无返回数据'; return } + console.log('获取服务端数据成功: %o', body) + // 检查Api大版本 + if (body.msg != `Hello~::^-^::~${conf.api.glbv}~`) { + msg = 'Api大版本不匹配,请检查服务端与脚本是否兼容!'; return + } + // 检查脚本更新 + const script = body.script // 定位到Script部分 + const lv = version.split('.'); const rv = script.ver.split('.') // 分别对主次小版本检查更新 + for (var i = 0; i < 3; i++) { + if (lv[i] < rv[i]) { + console.log('发现更新, 版本: %s, 信息: %s, 地址: %s, 强制推送: %o', script.ver, script.log, script.url, script.force) + msg = `${script.force ? '强制' : '发现'}更新:` + script.log; updUrl = script.url; if (script.force) return; break + } + } + // 激活可用源 + const source = body.source // 定位到Source部分 + // const defs = { type: 'music', actions: ['musicUrl'] } + Object.keys(source).forEach(v => { + if (source[v] == true) { + sourcess[v] = { + name: v, + ...defaults, + // ...defs, qualitys: source[v].qualitys, // 支持返回音质时启用 使用后端音质表 + } + } + }) + // 完成初始化 + stat = true + }) + .finally(() => { + // 脚本初始化完成后需要发送inited事件告知应用 + send(EVENT_NAMES.inited, { + status: stat, // 初始化成功 or 失败 (初始化失败不打开控制台, 使用更新提示接口返回信息) + openDevTools: stat ? devmode : false, // 是否打开开发者工具,方便用于调试脚本 'devmode' or 'stat ? devmode : false' + sources: sourcess, // 使用服务端源列表 + // sources: { // 当前脚本支持的源 + // wy: { name: '网易音乐', ...defaults, }, + // mg: { name: '咪咕音乐', ...defaults, }, + // kw: { name: '酷我音乐', ...defaults, }, + // // kg: { name: '酷狗音乐', ...defaults, }, // 暂不支持,仅供换源 + // }, + }) + // 发送更新提示 + if (msg) send(EVENT_NAMES.updateAlert, { log: '提示:' + msg, updateUrl: updUrl ? apiaddr + updUrl : '' }) + }) +} + +console.log('\n __ __ __ ______ ______ __ __ ____ ______ ______\n / / / / / / / ____/ / __ / / / / / / __ \\ / ____/ / ____/\n / / / /_/ / __ / /___ / / / / / / / / / /_/ / / / / /___\n / / \\_\\ \\ /_/ /___ / / / / / / / / / / ___/ / / / ____/\n / /___ / / / / ____/ / / /_/ / / /_/ / / / \\ / /___ / /___\n/_____/ /_/ /_/ /_____/ /_____/ /_____/ /_/ \\_\\ /_____/ /_____/\n=======================================================================\n') +init() // 启动!!! \ No newline at end of file diff --git a/src/middleware/loadpublic/public/test.txt b/src/middleware/loadpublic/public/test.txt new file mode 100644 index 0000000..cd31301 --- /dev/null +++ b/src/middleware/loadpublic/public/test.txt @@ -0,0 +1 @@ +:) \ No newline at end of file diff --git a/src/middleware/resp/resp.go b/src/middleware/resp/resp.go new file mode 100644 index 0000000..585958f --- /dev/null +++ b/src/middleware/resp/resp.go @@ -0,0 +1,62 @@ +// 返回值处理 +package resp + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// 统一输出 +/* +返回码对应表: + 0: http.StatusOK, // 成功 + 1: http.StatusForbidden, // IP被封禁 + 2: http.StatusServiceUnavailable, // 获取失败 + 3: http.StatusUnauthorized, // 验证失败 + 4: http.StatusInternalServerError, // 服务器内部错误 + 5: http.StatusTooManyRequests, // 请求过于频繁 + 6: http.StatusBadRequest, // 参数错误 +*/ +type Resp struct { + Code int `json:"code"` // 状态码 为兼容内置源设置 暂无实际作用 (1.0.2后已兼容Python版定义) + Msg string `json:"msg"` // 提示or报错信息 + Data string `json:"data"` // 音乐URL + Ext string `json:"ext"` // 其它信息 +} + +// 返回码对应列表 (参考Python版) +var statusMap = map[int]int{ + 0: http.StatusOK, // 成功 + 1: http.StatusForbidden, // IP被封禁 + 2: http.StatusServiceUnavailable, // 获取失败 + 3: http.StatusUnauthorized, // 验证失败 + 4: http.StatusInternalServerError, // 服务器内部错误 + 5: http.StatusTooManyRequests, // 请求过于频繁 + 6: http.StatusBadRequest, // 参数错误 +} + +// 返回请求 +/* + 注:Code不为0时调用c.Abort()终止Handler +*/ +func (o *Resp) Execute(c *gin.Context) { + status, ok := statusMap[o.Code] + if !ok { + status = http.StatusOK + } + if o.Code != 0 { + c.Abort() + } + c.JSON(status, o) +} + +// 包装请求并自动处理 +/* + 注:返回nil以继续执行下一个Handler +*/ +func Wrap(c *gin.Context, f func() *Resp) { + if r := f(); r != nil { + r.Execute(c) + } +} diff --git a/src/router/router.go b/src/router/router.go new file mode 100644 index 0000000..1640daf --- /dev/null +++ b/src/router/router.go @@ -0,0 +1,136 @@ +package router + +import ( + "lx-source/src/caches" + "lx-source/src/caches/localcache" + "lx-source/src/env" + "lx-source/src/middleware/auth" + "lx-source/src/middleware/loadpublic" + "lx-source/src/middleware/resp" + "lx-source/src/sources" + "net/http" + + "github.com/ZxwyWebSite/ztool" + "github.com/gin-contrib/gzip" + "github.com/gin-gonic/gin" +) + +// 载入路由 +func InitRouter() *gin.Engine { + r := gin.Default() + // Gzip压缩 + if env.Config.Main.Gzip { + r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{"/file/"}))) + } + // 源信息 + r.GET(`/`, func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + `version`: env.Version, // 服务端程序版本 + `name`: `lx-music-source`, // 名称 + `msg`: `Hello~::^-^::~v1~`, // Api大版本 + `developer`: []string{`Zxwy`}, // 开发者列表, 可在保留原作者的基础上添加你自己的名字? + // 仓库地址 + `github`: `https://github.com/ZxwyWebSite/lx-source`, + // 可用平台 + `source`: gin.H{ + `mg`: true, + `wy`: true, + `kg`: []string{`128k`, `320k`}, // 测试结构2, 启用时返回音质列表, 禁用为false + `tx`: gin.H{ // "测试结构 不代表最终方式" + `enable`: false, + `qualitys`: []string{`128k`, `320k`, `flac`, `flac24bit`}, + }, + `kw`: true, + }, + // 自定义源脚本更新 + `script`: env.Config.Script, + }) + }) + // 静态文件 + loadpublic.LoadPublic(r) + // r.StaticFile(`/favicon.ico`, `public/icon.ico`) + // r.StaticFile(`/lx-custom-source.js`, `public/lx-custom-source.js`) + // 解析接口 + r.GET(`/link/:s/:id/:q`, auth.AuthHandler, linkHandler) + if cache, ok := caches.UseCache.(*localcache.Cache); ok { + r.Static(`/file`, cache.Path) + } + // if env.Config.Cache.Mode == `local` { + // r.Static(`/file`, env.Config.Cache.Local_Path) + // } + // 数据接口 + // r.GET(`/file/:t/:hq/:n`, func(c *gin.Context) { + // c.String(http.StatusOK, time.Now().Format(`20060102150405`)) + // }) + // 暂不对文件接口进行验证 脚本返回链接无法附加请求头 只可在Get添加Query + // g := r.Group(``) + // { + // g.Use(authHandler) + // g.GET(`/link/:s/:id/:q`, linkHandler) + // g.Static(`/file`, LocalCachePath) + // } + return r +} + +// 数据返回格式 +const ( + CacheHIT = `Cache HIT` // 缓存已命中 + CacheMISS = `Cache MISS` // 缓存未命中 + CacheSet = `Cache Seted` // 缓存已设置 +) + +// 外链解析 +func linkHandler(c *gin.Context) { + resp.Wrap(c, func() *resp.Resp { + // 获取传入参数 检查合法性 + parmlen := len(c.Params) + parms := make(map[string]string, parmlen) + for i := 0; i < parmlen; i++ { + parms[c.Params[i].Key] = c.Params[i].Value + } + // getParam := func(p string) string { return strings.TrimSuffix(strings.TrimPrefix(c.Param(p), `/`), `/`) } //strings.Trim(c.Param(p), `/`) + s := parms[`s`] //c.Param(`s`) //getParam(`s`) // source 平台 wy, mg, kw + id := parms[`id`] //c.Param(`id`) //getParam(`id`) // sid 音乐ID wy: songmid, mg: copyrightId + q := parms[`q`] //c.Param(`q`) //getParam(`q`) // quality 音质 128k / 320k / flac / flac24bit + env.Loger.NewGroup(`LinkQuery`).Debug(`s: %v, id: %v, q: %v`, s, id, q) + if ztool.Chk_IsNil(s, q, id) { + return &resp.Resp{Code: 6, Msg: `参数不全`} // http.StatusBadRequest + } + cquery := caches.NewQuery(s, id, q) + // _, ok := sources.UseSource.Verify(cquery) // 获取请求音质 同时检测是否支持(如kw源没有flac24bit) qualitys[q][s]rquery + // if !ok { + // return &resp.Resp{Code: 6, Msg: `不支持的平台或音质`} + // } + // 查询缓存 + var cstat bool + if caches.UseCache != nil { + cstat = caches.UseCache.Stat() + } + sc := env.Loger.NewGroup(`StatCache`) + if cstat { + sc.Debug(`Method: Get, Query: %v`, cquery.Query()) + if link := caches.UseCache.Get(cquery); link != `` { + return &resp.Resp{Msg: CacheHIT, Data: link} + } + } else { + sc.Debug(`Disabled`) + } + // 解析歌曲外链 + outlink, emsg := sources.UseSource.GetLink(cquery) + if emsg != `` { + if emsg == sources.Err_Verify { // Verify Failed: 不支持的平台或音质 + return &resp.Resp{Code: 6, Msg: ztool.Str_FastConcat(emsg, `: 不支持的平台或音质`)} + } + return &resp.Resp{Code: 2, Msg: emsg} + } + // 缓存并获取直链 + if outlink != `` && cstat { + sc.Debug(`Method: Set, Link: %v`, outlink) + if link := caches.UseCache.Set(cquery, outlink); link != `` { + return &resp.Resp{Msg: CacheSet, Data: link} + } + } + // 无法获取直链 直接返回原链接 + return &resp.Resp{Msg: CacheMISS, Data: outlink} + }) +} diff --git a/src/sources/builtin/driver.go b/src/sources/builtin/driver.go new file mode 100644 index 0000000..8f49843 --- /dev/null +++ b/src/sources/builtin/driver.go @@ -0,0 +1,110 @@ +// 内置解析源 +package builtin + +import ( + "lx-source/src/caches" + "lx-source/src/sources" + "net/http" + "strings" + "sync" + "time" + + "github.com/ZxwyWebSite/ztool" +) + +type Source struct{} + +// 预检 (兼容旧接口) +func (s *Source) Verify(c *caches.Query) (rquery string, ok bool) { + rquery, ok = qualitys[c.Quality][c.Source] + return +} + +var ( + // 并发对象池 (用户限制在Router处实现) + wy_pool = &sync.Pool{New: func() any { return new(FyApi_Song) }} + mg_pool = &sync.Pool{New: func() any { return new(MgApi_Song) }} + kw_pool = &sync.Pool{New: func() any { return new(KwApi_Song) }} +) + +// 查询 +func (s *Source) GetLink(c *caches.Query) (outlink string, msg string) { + rquery, ok := s.Verify(c) + if !ok { + msg = sources.Err_Verify //`Verify Failed` + return + } + // var outlink string + jx := sources.Loger.AppGroup(`builtin`) //env.Loger.NewGroup(`JieXiApis`) + switch c.Source { + case s_wy: + resp := wy_pool.Get().(*FyApi_Song) + defer wy_pool.Put(resp) + + url := ztool.Str_FastConcat(`http://nm.fyapi.site/song/url/v1?id=`, c.MusicID, `&level=`, rquery, `&noCookie=true`) + jx.Debug(`Wy, Url: %v`, url) + // wy源增加后端重试 默认3次 + for i := 0; true; i++ { + _, err := ztool.Net_HttpReq(http.MethodGet, url, nil, header_wy, &resp) + if err != nil { + jx.Error(`HttpReq, Err: %s, ReTry: %v`, err, i) + if i > 3 { + msg = err.Error() + return + } + time.Sleep(time.Second) + continue + } + break + } + if len(resp.Data) == 0 { + msg = `No Data` + return + } + var data = resp.Data[0] + if data.FreeTrialInfo != nil || data.Level != rquery { + jx.Error("发生错误, 返回数据:\n%#v", resp) + msg = `触发风控或专辑单独收费或音质不匹配,请稍后重试` + return + } + // jx.Info(`WyLink, RealQuality: %v`, data.Level) + outlink = data.URL + case s_mg: + resp := mg_pool.Get().(*MgApi_Song) + defer mg_pool.Put(resp) + + url := ztool.Str_FastConcat(`https://m.music.migu.cn/migumusic/h5/play/auth/getSongPlayInfo?copyrightId=`, c.MusicID, `&type=`, rquery) + jx.Debug(`Mg, Url: %v`, url) + _, err := ztool.Net_HttpReq(http.MethodGet, url, nil, header_mg, &resp) + if err != nil { + msg = err.Error() + return + } + if link := resp.Data.PlayURL; link != `` { + outlink = `https:` + link + } else { + jx.Debug(`Mg, Err: %#v`, resp) + } + case s_kw: + resp := kw_pool.Get().(*KwApi_Song) + defer kw_pool.Put(resp) + + url := ztool.Str_FastConcat(`https://bd-api.kuwo.cn/api/service/music/downloadInfo/`, c.MusicID, `?isMv=0&format=`, c.Extname, `&br=`, rquery, c.Extname, `&level=`) + jx.Debug(`Kw, Url: %s`, url) + _, err := ztool.Net_HttpReq(http.MethodGet, url, nil, header_kw, &resp) + if err != nil { + msg = err.Error() + return + } + if resp.Code != 200 || resp.Data.AudioInfo.Bitrate == `1` { + jx.Debug(`Kw, Err: %#v`, resp) + msg = `failed` + return + } + outlink = strings.Split(resp.Data.URL, `?`)[0] + default: + msg = `不支持的平台` + return + } + return +} diff --git a/src/sources/builtin/types.go b/src/sources/builtin/types.go new file mode 100644 index 0000000..076463b --- /dev/null +++ b/src/sources/builtin/types.go @@ -0,0 +1,167 @@ +package builtin + +type ( + // FongerData 数据更新 + // FgData_Headers struct { + // Kw struct { + // Headers string `json:"headers"` + // } `json:"kw"` + // Mg struct { + // Referer string `json:"Referer"` + // UserAgent string `json:"User-Agent"` + // By string `json:"By"` + // Channel string `json:"channel"` + // Cookie string `json:"Cookie"` + // } `json:"mg"` + // Wy struct { + // Cookie string `json:"Cookie"` + // } `json:"wy"` + // } + + // FongerApi 方格音乐接口 + FyApi_Song struct { + Code int `json:"code"` + Data []struct { + ID int `json:"id"` + URL string `json:"url"` + Br int `json:"br"` + Size int `json:"size"` + Md5 string `json:"md5"` + Code int `json:"code"` + Expi int `json:"expi"` + Type string `json:"type"` + Gain float64 `json:"gain"` + Peak float64 `json:"peak"` + Fee int `json:"fee"` + Uf interface{} `json:"uf"` + Payed int `json:"payed"` + Flag int `json:"flag"` + CanExtend bool `json:"canExtend"` + FreeTrialInfo interface{} `json:"freeTrialInfo"` + Level string `json:"level"` + EncodeType string `json:"encodeType"` + FreeTrialPrivilege struct { + ResConsumable bool `json:"resConsumable"` + UserConsumable bool `json:"userConsumable"` + ListenType interface{} `json:"listenType"` + CannotListenReason interface{} `json:"cannotListenReason"` + PlayReason interface{} `json:"playReason"` + } `json:"freeTrialPrivilege"` + FreeTimeTrialPrivilege struct { + ResConsumable bool `json:"resConsumable"` + UserConsumable bool `json:"userConsumable"` + Type int `json:"type"` + RemainTime int `json:"remainTime"` + } `json:"freeTimeTrialPrivilege"` + URLSource int `json:"urlSource"` + RightSource int `json:"rightSource"` + PodcastCtrp interface{} `json:"podcastCtrp"` + EffectTypes interface{} `json:"effectTypes"` + Time int `json:"time"` + } `json:"data"` + } + // MiguApi 咪咕音乐接口 + MgApi_Song struct { + Code string `json:"code"` + Msg string `json:"msg"` + Data struct { + PlayURL string `json:"playUrl"` + FormatID string `json:"formatId"` + SalePrice string `json:"salePrice"` + BizType string `json:"bizType"` + BizCode string `json:"bizCode"` + AuditionsLength int `json:"auditionsLength"` + } `json:"data"` + } + // BodianApi 波点音乐接口 + KwApi_Song struct { + Code int `json:"code"` + Msg string `json:"msg"` + ReqID string `json:"reqId"` + Data struct { + Duration int `json:"duration"` + AudioInfo struct { + Bitrate string `json:"bitrate"` + Format string `json:"format"` + Level string `json:"level"` + Size string `json:"size"` + } `json:"audioInfo"` + URL string `json:"url"` + } `json:"data"` + ProfileID string `json:"profileId"` + CurTime int64 `json:"curTime"` + } +) + +const ( + // FongerData + // fgdata = `http://api.fonger.top/pc/` + // fgdata_banner = `banner.json` // 新闻 + // fgdata_update = `update.json` // 更新 + // fgdata_channel = `channel.json` // 可用源 + // fgdata_headers = `headers.json` // VipCookie + // FongerHeader + // fyhdr_ua = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36 HBPC/12.1.2.300` + + // FongerApi + // fyapi = `http://nm.fyapi.site/` + // fyapi_song = `song/url/v1` // (id: 网易云ID, level: 音质[HQ: exhigh, SQ: lossless, ZQ: hires]) ?id=1885551650&level=exhigh&noCookie=true + + // Source + s_wy = `wy` + s_mg = `mg` + s_kw = `kw` + // s_kg = `kg` + // s_tx = `tx` +) + +var ( + // def_headers FgData_Headers + + // 音质列表 ( [通用音质][音乐平台]对应音质 ) + qualitys = map[string]map[string]string{ + `128k`: { + s_wy: `standard`, + s_mg: `1`, + s_kw: `128k`, + }, + `320k`: { + s_wy: `exhigh`, + s_mg: `2`, + s_kw: `320k`, + }, + `flac`: { + s_wy: `lossless`, + s_mg: `3`, + s_kw: `2000k`, + }, + `flac24bit`: { + s_wy: `hires`, + s_mg: `4`, + }, + } + // Headers + header_mg = map[string]string{ + `Referer`: `https://m.music.migu.cn/v4/`, + `By`: `04f81461a98c7af557fea3cf28c4ea15`, + `channel`: `014000D`, + `Cookie`: `SESSION=ZTIwODkyMDQtOTE1NS00MDhlLThhMWEtMjQ0N2Y2Mzk2OTAz`, + } + header_wy = map[string]string{ + // MUSIC_U=000A32B5F2905E3227DBEFFC5C36250FC49DE0CF33A49B5FC6998B8507664B1E5408FC29A5C06EA23100E83D8E4C239090993406AB1F27ED03A7A978B4836527AF9189CB3BA0449C16AD634A2D50A78323B240368E04E05968460671EF377EFFA4B07319A6768D8D8A974B0E70E6F94195A52D77FC145049F05C1320401D0CE974C0604A1622C3EC5B7E5478B3E9F8004758E8C78D7900180F53F16BE9E5424E493FCAF122D8B3CB1C16CAACD7567F886790583AEB8B5D455EE1B48FBEEC1FB3F1C4BF5CEF685D718709C00DB1C76007D3BC32D5E5DB26927731DD4116F750356DB71380EF3523BCD47BD27A31C340B8444A4497AE277811AFD3B519DB585F85985EE7AF85765A567B54360FD59C54228CAF283D8D821251B94B09DB4ADC4F412951484B9150E9271166B475E2388BA75628912359A3DC5FDF64C68255225D3D070F1633447571ADC27909D3A5A3DF072A + `Cookie`: `MUSIC_U=00B4C1E3FD77410780EF1C0840D08F3F5E7030E2D052CA8EC98A7368F7A7F6649B216E9533A1A174D72CCADF99554228E852DE46BBD2EA2A6B2A1433A3DF48B62EAA76FC18CD59256FEF6E76D39FB42DF76CE5068C69E3944E3A6E8E3C26135DBE0D9791FCE0BD524BD27F6226FD6460B05646A549A5C429F5E01EBA4E2D8D615BD715A7D245B13D9E570E87D0ADA608A607F2FAEF22AF8EE94F827AF150E9E1C517CB0F1588EF8F1D61947C43784985CF74F69458748960CE92053CA72B5FEF92C93F12F36714F0B346C2EAF89FAA516A8974E8CF53D5492DE95ED8591CCCF45AEB627C93B0CD370AEFB656EADAD031F688A6BB2CE3C9FA31BD6166A16ABEBEDADFCFEFBDCED5D4E12FFF1403C4F2B5A3F2422EF9D0878C0B52D08967D58E2E9DACE754404E2D6E1F81F52A1F1735CA9FBB85D758F81E0A7CBA41C5739D29E284F68430EB13E4F493890840031D3BD27E`, + } + header_kw = map[string]string{ + // `headers`: `Secret:6c3e1759abe6bd58f56bb713f6aee0bb738189eae7837be83636389b96fd4d7104c13520&&&Cookie:Hm_Iuvt_cdb524f42f0ce19b169b8072123a4727=2bm5QbPQKPZSRHyFN4pbZnGcNJ4J2DZJ`, + `channel`: `qq`, + `plat`: `ar`, + `net`: `wifi`, + `ver`: `3.1.2`, + `uid`: ``, + `devId`: `0`, + } +) + +// func init() { +// json.Unmarshal([]byte(`{"kw":{"headers":"Secret:6c3e1759abe6bd58f56bb713f6aee0bb738189eae7837be83636389b96fd4d7104c13520&&&Cookie:Hm_Iuvt_cdb524f42f0ce19b169b8072123a4727=2bm5QbPQKPZSRHyFN4pbZnGcNJ4J2DZJ"},"mg":{"Referer":"https://m.music.migu.cn/v4/","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.84 Safari/537.36 HBPC/12.1.2.300","By":"04f81461a98c7af557fea3cf28c4ea15","channel":"014000D","Cookie":"SESSION=ZTIwODkyMDQtOTE1NS00MDhlLThhMWEtMjQ0N2Y2Mzk2OTAz"},"wy":{"Cookie":"MUSIC_U=000A32B5F2905E3227DBEFFC5C36250FC49DE0CF33A49B5FC6998B8507664B1E5408FC29A5C06EA23100E83D8E4C239090993406AB1F27ED03A7A978B4836527AF9189CB3BA0449C16AD634A2D50A78323B240368E04E05968460671EF377EFFA4B07319A6768D8D8A974B0E70E6F94195A52D77FC145049F05C1320401D0CE974C0604A1622C3EC5B7E5478B3E9F8004758E8C78D7900180F53F16BE9E5424E493FCAF122D8B3CB1C16CAACD7567F886790583AEB8B5D455EE1B48FBEEC1FB3F1C4BF5CEF685D718709C00DB1C76007D3BC32D5E5DB26927731DD4116F750356DB71380EF3523BCD47BD27A31C340B8444A4497AE277811AFD3B519DB585F85985EE7AF85765A567B54360FD59C54228CAF283D8D821251B94B09DB4ADC4F412951484B9150E9271166B475E2388BA75628912359A3DC5FDF64C68255225D3D070F1633447571ADC27909D3A5A3DF072A"}}`), &def_headers) +// } diff --git a/src/sources/custom/driver.go b/src/sources/custom/driver.go new file mode 100644 index 0000000..a5660aa --- /dev/null +++ b/src/sources/custom/driver.go @@ -0,0 +1,2 @@ +// 账号解析源 +package custom diff --git a/src/sources/source.go b/src/sources/source.go new file mode 100644 index 0000000..0e1398e --- /dev/null +++ b/src/sources/source.go @@ -0,0 +1,45 @@ +package sources + +import ( + "lx-source/src/caches" + "lx-source/src/env" +) + +var Loger = env.Loger.NewGroup(`Sources`) // JieXiApis +const ( + Err_Verify = `Verify Failed` +) + +// 源查询接口 +/* + Origin: + 首先调用Verify验证源是否支持 + 再尝试查询缓存 + 无缓存则解析链接 + + 参考Python版: + 不验证当前源是否支持,直接查询缓存 + 验证部分放到GetLink里 + +*/ +type Source interface { + Verify(*caches.Query) (string, bool) // 验证是否可用 <查询参数> + GetLink(*caches.Query) (string, string) // 查询获取链接 <查询参数> <链接,信息> +} + +// 默认空接口 +type NullSource struct{} + +func (*NullSource) Verify(*caches.Query) (string, bool) { return ``, false } +func (*NullSource) GetLink(*caches.Query) (string, string) { return ``, `NullSource` } + +var UseSource Source = &NullSource{} // = &builtin.Source{} + +// 统一错误 +// type Error struct { +// msg string +// } + +// func (e *Error) Error() string { +// return ztool.Str_FastConcat(e.msg) +// }