├── .github └── workflows │ └── action.yml ├── .gitignore ├── LICENSE ├── action.go ├── build.go ├── go.mod ├── go.sum ├── init.go ├── main.go ├── menu.go ├── pkg └── ztool ├── readme.md ├── release.go ├── rsrc_windows_amd64.syso ├── src ├── caches │ ├── cache.go │ ├── cloudcache │ │ └── cloud.go │ └── localcache │ │ └── local.go ├── database │ ├── driver.go │ ├── modules │ │ ├── sqlite_cgo.go │ │ └── sqlite_etc.go │ └── types.go ├── env │ └── env.go ├── middleware │ ├── auth │ │ ├── auth.go │ │ └── ratelimit.go │ ├── dynlink │ │ └── dynlink.go │ ├── resp │ │ └── resp.go │ └── util │ │ └── util.go ├── server │ ├── api_music.go │ ├── app_lxmusic.go │ ├── app_musicfree.go │ ├── loadpublic.go │ ├── loadquality.go │ ├── public │ │ ├── icon.ico │ │ ├── lx-custom-source.js │ │ ├── lx-icon.ico │ │ ├── lx-source-script.js │ │ ├── status.html │ │ └── test.txt │ └── router.go └── sources │ ├── builtin │ ├── driver.go │ └── types.go │ ├── custom │ ├── custom.go │ ├── driver.go │ ├── kg │ │ ├── musicinfo.go │ │ ├── player.go │ │ ├── refresh.go │ │ ├── types.go │ │ └── utils.go │ ├── kw │ │ ├── encrypt.go │ │ ├── player.go │ │ ├── types.go │ │ └── utils.go │ ├── mg │ │ ├── albuminfo.go │ │ ├── musicinfo.go │ │ ├── player.go │ │ ├── refresh.go │ │ ├── types.go │ │ └── utils.go │ ├── tx │ │ ├── encrypt.go │ │ ├── info.go │ │ ├── login.go │ │ ├── musicinfo.go │ │ ├── player.go │ │ ├── refresh.go │ │ ├── types.go │ │ └── utils.go │ ├── utils │ │ └── utils.go │ └── wy │ │ ├── modules │ │ ├── core_crypto.go │ │ ├── core_request.go │ │ ├── core_types.go │ │ ├── login_qr_check.go │ │ ├── login_qr_create.go │ │ ├── login_qr_key.go │ │ ├── login_refresh.go │ │ ├── song_url.go │ │ └── song_url_v1.go │ │ ├── player.go │ │ ├── refresh.go │ │ └── types.go │ ├── example │ └── data.go │ ├── source.go │ └── types.go └── update.md /.github/workflows/action.yml: -------------------------------------------------------------------------------- 1 | name: Action 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - name: Checkout git repo 15 | uses: actions/checkout@v4 16 | with: 17 | path: ./repo 18 | fetch-depth: 0 19 | 20 | - name: Set up Golang 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: '1.21' 24 | 25 | - name: Install Dependencies 26 | run: | 27 | go version && go env && export PATH=$PATH:$(go env GOPATH)/bin 28 | go install golang.org/dl/go1.20.14@latest && go1.20.14 download && go1.20.14 version 29 | sudo apt-get update 30 | sudo apt-get -y install gcc-mingw-w64-x86-64 31 | sudo apt-get -y install gcc-arm-linux-gnueabihf libc6-dev-armhf-cross 32 | sudo apt-get -y install gcc-aarch64-linux-gnu libc6-dev-arm64-cross 33 | wget -q https://dl.google.com/android/repository/android-ndk-r26b-linux.zip && unzip -d ~ android-ndk-r26b-linux.zip && rm android-ndk-r26b-linux.zip 34 | 35 | - name: Fetch Modules 36 | run: | 37 | wget -q "https://r2eu.zxwy.link/gh/lx-source/static/ztool_20240525.zip" -O ztool.zip && unzip ztool.zip && rm ztool.zip 38 | wget -q "https://r2eu.zxwy.link/gh/lx-source/static/cr-go-sdk_20240525.zip" -O cr-go-sdk.zip && unzip cr-go-sdk.zip && rm cr-go-sdk.zip 39 | 40 | - name: Run Action 41 | run: cd repo && go run action.go && mv dist ../ 42 | 43 | - name: Short SHA 44 | uses: benjlevesque/short-sha@v3.0 45 | id: short-sha 46 | 47 | - name: Upload Artifact 48 | uses: actions/upload-artifact@v4 49 | with: 50 | name: lx-source-bin_${{ env.SHA }} 51 | path: ./dist 52 | 53 | - name: Generate Changelog 54 | run: cd repo && echo PACKAGE_VERSION=`go run release.go` >> $GITHUB_ENV && mv changelog.md ../ 55 | 56 | - name: Create git tag 57 | uses: pkgdeps/git-tag-action@v3 58 | with: 59 | github_token: ${{ github.token }} 60 | github_repo: ${{ github.repository }} 61 | version: ${{ env.PACKAGE_VERSION }} 62 | git_commit_sha: ${{ github.sha }} 63 | git_tag_prefix: "v" 64 | 65 | - name: Release 66 | uses: softprops/action-gh-release@v2 67 | with: 68 | body_path: ./changelog.md 69 | prerelease: false 70 | draft: false 71 | tag_name: v${{ env.PACKAGE_VERSION }} 72 | files: | 73 | ./dist/lx-source-android-386.zip 74 | ./dist/lx-source-android-amd64.zip 75 | ./dist/lx-source-android-arm.zip 76 | ./dist/lx-source-android-arm64.zip 77 | ./dist/lx-source-darwin-amd64v2-go1.20.14.zip 78 | ./dist/lx-source-darwin-amd64v3-go1.20.14.zip 79 | ./dist/lx-source-darwin-arm64-go1.20.14.zip 80 | ./dist/lx-source-linux-amd64v1.zip 81 | ./dist/lx-source-linux-amd64v2.zip 82 | ./dist/lx-source-linux-amd64v3.zip 83 | ./dist/lx-source-linux-amd64v4.zip 84 | ./dist/lx-source-linux-arm5.zip 85 | ./dist/lx-source-linux-arm5-go1.20.14.zip 86 | ./dist/lx-source-linux-arm6.zip 87 | ./dist/lx-source-linux-arm6-go1.20.14.zip 88 | ./dist/lx-source-linux-arm64.zip 89 | ./dist/lx-source-linux-arm64-go1.20.14.zip 90 | ./dist/lx-source-linux-arm7.zip 91 | ./dist/lx-source-linux-arm7-go1.20.14.zip 92 | ./dist/lx-source-linux-mips64hardfloat-go1.20.14.zip 93 | ./dist/lx-source-linux-mips64lehardfloat-go1.20.14.zip 94 | ./dist/lx-source-linux-mips64lesoftfloat-go1.20.14.zip 95 | ./dist/lx-source-linux-mips64softfloat-go1.20.14.zip 96 | ./dist/lx-source-linux-mipshardfloat-go1.20.14.zip 97 | ./dist/lx-source-linux-mipslehardfloat-go1.20.14.zip 98 | ./dist/lx-source-linux-mipslesoftfloat-go1.20.14.zip 99 | ./dist/lx-source-linux-mipssoftfloat-go1.20.14.zip 100 | ./dist/lx-source-windows-amd64v1-go1.20.14.zip 101 | ./dist/lx-source-windows-amd64v2-go1.20.14.zip 102 | ./dist/lx-source-windows-amd64v2.zip 103 | ./dist/lx-source-windows-amd64v3-go1.20.14.zip 104 | ./dist/lx-source-windows-amd64v3.zip 105 | ./dist/lx-source-windows-amd64v4.zip 106 | env: 107 | GITHUB_TOKEN: ${{ github.token }} 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | # cache/ 3 | data/ 4 | .outdated/ 5 | # conf.ini 6 | test.go 7 | test_test.go 8 | # src/sources/builtin/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Zxwy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /build.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | // 一键编译脚本 `go run build.go` 4 | package main 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/ZxwyWebSite/ztool" 14 | ) 15 | 16 | // 使用前请设置一下编译参数 17 | var ( 18 | // 系统-架构-C编译工具 19 | list_os_arch_cc = map[string]map[string]string{ 20 | "linux": { 21 | "amd64": `x86_64-linux-gnu-gcc`, 22 | "arm": `arm-linux-gnueabihf-gcc`, 23 | }, 24 | "windows": { 25 | "amd64": `/usr/local/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-gcc`, 26 | }, 27 | } 28 | // 架构-版本 29 | list_arch_ver = map[string][]string{ 30 | "amd64": {`v2`}, //{`v1`, `v2`, `v3`}, 31 | "arm": {`7`}, //{`5`, `6`, `7`}, 32 | } 33 | ) 34 | 35 | const ( 36 | // 运行参数 37 | args_name = `lx-source` // 程序名称 38 | args_path = `bin/` // 输出目录 39 | args_zpak = true // 打包文件 40 | ) 41 | 42 | var workDir string 43 | 44 | // 编译 45 | func doCompile(v_os, v_arch, v_archv, v_cc string) error { 46 | // 构建 | 目标系统 | 目标架构 | 优化等级 | 不包含调试信息 | 使用外部链接器 | 输出详细操作 | 静态编译 | JSON解释器 47 | // `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` 48 | fname := func() string { 49 | name := strings.Join([]string{args_name, v_os, v_arch}, `-`) 50 | var wexe string 51 | if v_os == `windows` { 52 | wexe = `.exe` 53 | } 54 | return ztool.Str_FastConcat(name, v_archv, wexe) 55 | }() 56 | pname := filepath.Clean(ztool.Str_FastConcat(args_path, fname)) 57 | cmd := ztool.Str_FastConcat( 58 | `go build -o `, pname, 59 | ` -gcflags=-trimpath="`, workDir, `" -asmflags=-trimpath="`, workDir, `" -trimpath -buildvcs=false`, 60 | ` -ldflags "-s -w -linkmode external" -tags "go_json"`, // go_json | json(std) | jsoniter | sonic 61 | ) 62 | // 输出要执行的命令 63 | ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`执行命令:`, cmd)) 64 | // 设置环境&执行编译 65 | envmap := map[string]string{ 66 | `GOOS`: v_os, 67 | `GOARCH`: v_arch, 68 | `AR`: `llvm-ar`, // 脚本默认使用Clang的Archiver, 没装llvm请注释掉以使用系统默认值 69 | `CC`: v_cc, 70 | `CGO_ENABLED`: `1`, 71 | ztool.Str_FastConcat(`GO`, strings.ToUpper(v_arch)): v_archv, // GO{ARCH} Eg: GOARM, GOAMD64 72 | } 73 | setenv := func(env map[string]string) error { 74 | var handler ztool.Err_HandleList 75 | for k, v := range env { 76 | handler.Do(func() error { 77 | return os.Setenv(k, v) 78 | }) 79 | } 80 | return handler.Err 81 | } 82 | if err := setenv(envmap); err != nil { 83 | return err 84 | } 85 | if err := ztool.Cmd_aSyncExec(cmd); err != nil { 86 | return err 87 | } 88 | // 打包文件 89 | if args_zpak { // DoSomeThing... 90 | if !ztool.Fbj_IsExists(`archieve`) { 91 | os.MkdirAll(filepath.Join(args_path, `archieve`), 0755) 92 | } 93 | if err := ztool.Pak_ZipFile( 94 | pname, 95 | filepath.Join(args_path, `archieve`, ztool.Str_LastBefore(fname, `.`))+`.zip`, 96 | ztool.Pak_ZipConfig{UnPath: true}, 97 | ); err != nil { 98 | ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`打包["`, pname, `"]出错:`, err.Error())) 99 | } else { 100 | ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`打包["`, pname, `"]完成`)) 101 | } 102 | } 103 | return nil 104 | } 105 | 106 | func init() { 107 | if runtime.GOOS != `linux` { 108 | ztool.Cmd_FastPrintln("简易脚本,未对Linux以外系统做适配,请复制执行以下命令编译:\ngo build -ldflags \"-s -w\" -tags \"go_json\"\n如无报错则会在本目录生成名为lx-source的可执行文件。") 109 | os.Exit(1) 110 | } 111 | workDir, _ = os.Getwd() 112 | ztool.Cmd_FastPrintln(ztool.Str_FastConcat(` 113 | ================================ 114 | | Golang 一键编译脚本 115 | | 程序名称:`, args_name, ` 116 | | 输出目录:`, args_path, ` 117 | | 打包文件:`, strconv.FormatBool(args_zpak), ` 118 | ================================ 119 | `)) 120 | } 121 | 122 | func main() { 123 | var handler = ztool.Err_NewDefHandleList() 124 | handler.Do(func() error { 125 | // 检测入口函数是否存在 126 | if !ztool.Fbj_IsExists(`main.go`) { 127 | ztool.Cmd_FastPrintln(`入口函数不存在,请在源码根目录运行此程序!`) 128 | return ztool.Err_EsContinue 129 | } 130 | // 检测输出目录是否存在 (已在zTool中增加相关检测) 131 | // if !ztool.Fbj_IsExists(args_path) { 132 | // ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`输出目录 "`, args_path, `" 不存在,尝试创建`)) 133 | // return os.MkdirAll(args_path, 0755) 134 | // } 135 | return nil 136 | }) 137 | for v_os, v_arch_cc := range list_os_arch_cc { 138 | for v_arch, v_cc := range v_arch_cc { 139 | // 检测CC是否存在 140 | o, e := ztool.Cmd_aWaitExec(ztool.Str_FastConcat(`which `, v_cc)) 141 | if !ztool.Fbj_IsExists(v_cc) && (e != nil || o == ``) { 142 | ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`编译工具 ["`, v_cc, `"] 不存在,跳过 `, v_arch, ` 架构`)) 143 | continue 144 | } 145 | // 继续编译 146 | for _, v_arch_ver := range list_arch_ver[v_arch] { 147 | // handler.Do(func() error { return tool.ErrContinue }) 148 | handler.Do(func() error { 149 | // (测试) 快速输出编译参数 150 | ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`开始编译:`, v_os, `/`, v_arch, `/`, v_arch_ver, `/`, `[`, v_cc, `], 任务编号 `, handler.NumStr())) 151 | // 编译对应文件 152 | // return nil 153 | return doCompile(v_os, v_arch, v_arch_ver, v_cc) 154 | }) // handler.Do(func() error { return doCompile(v_os, v_arch, v_arch_ver, v_cc) }) 155 | } 156 | } 157 | } 158 | if res := handler.Result(); res != nil { 159 | ztool.Cmd_FastPrintln(ztool.Str_FastConcat(`发生错误:`, res.Errors())) 160 | return 161 | } 162 | ztool.Cmd_FastPrintln(`恭喜!所有任务成功完成`) 163 | } 164 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module lx-source 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/ZxwyWebSite/cr-go-sdk v0.0.2 7 | github.com/ZxwyWebSite/ztool v0.0.1 8 | github.com/gin-contrib/gzip v1.0.0 9 | github.com/gin-gonic/gin v1.9.1 10 | github.com/google/uuid v1.6.0 11 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 12 | ) 13 | 14 | require ( 15 | github.com/bytedance/sonic v1.11.5 // indirect 16 | github.com/bytedance/sonic/loader v0.1.1 // indirect 17 | github.com/cloudwego/base64x v0.1.3 // indirect 18 | github.com/cloudwego/iasm v0.2.0 // indirect 19 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 20 | github.com/gin-contrib/cors v1.7.1 21 | github.com/gin-contrib/sse v0.1.0 // indirect 22 | github.com/go-ini/ini v1.67.0 // indirect 23 | github.com/go-playground/locales v0.14.1 // indirect 24 | github.com/go-playground/universal-translator v0.18.1 // indirect 25 | github.com/go-playground/validator/v10 v10.19.0 // indirect 26 | github.com/goccy/go-json v0.10.2 // indirect 27 | github.com/json-iterator/go v1.1.12 // indirect 28 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 29 | github.com/kr/text v0.2.0 // indirect 30 | github.com/leodido/go-urn v1.4.0 // indirect 31 | github.com/mattn/go-colorable v0.1.13 // indirect 32 | github.com/mattn/go-isatty v0.0.20 // indirect 33 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 34 | github.com/modern-go/reflect2 v1.0.2 // indirect 35 | github.com/pelletier/go-toml/v2 v2.2.1 // indirect 36 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 37 | github.com/ugorji/go/codec v1.2.12 // indirect 38 | golang.org/x/arch v0.7.0 // indirect 39 | golang.org/x/crypto v0.22.0 // indirect 40 | golang.org/x/net v0.24.0 // indirect 41 | golang.org/x/sys v0.19.0 // indirect 42 | golang.org/x/text v0.14.0 // indirect 43 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 44 | google.golang.org/protobuf v1.33.0 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | ) 47 | 48 | replace ( 49 | github.com/ZxwyWebSite/cr-go-sdk v0.0.2 => ../cr-go-sdk 50 | github.com/ZxwyWebSite/ztool v0.0.1 => ../ztool // ./pkg/ztool 51 | ) 52 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.11.5 h1:G00FYjjqll5iQ1PYXynbg/hyzqBqavH8Mo9/oTopd9k= 2 | github.com/bytedance/sonic v1.11.5/go.mod h1:X2PC2giUdj/Cv2lliWFLk6c/DUQok5rViJSemeB0wDw= 3 | github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY= 4 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 5 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 6 | github.com/cloudwego/base64x v0.1.3 h1:b5J/l8xolB7dyDTTmhJP2oTs5LdrjyrUFuNxdfq5hAg= 7 | github.com/cloudwego/base64x v0.1.3/go.mod h1:1+1K5BUHIQzyapgpF7LwvOGAEDicKtt1umPV+aN8pi8= 8 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 9 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 10 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 15 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 16 | github.com/gin-contrib/cors v1.7.1 h1:s9SIppU/rk8enVvkzwiC2VK3UZ/0NNGsWfUKvV55rqs= 17 | github.com/gin-contrib/cors v1.7.1/go.mod h1:n/Zj7B4xyrgk/cX1WCX2dkzFfaNm/xJb6oIUk7WTtps= 18 | github.com/gin-contrib/gzip v1.0.0 h1:UKN586Po/92IDX6ie5CWLgMI81obiIp5nSP85T3wlTk= 19 | github.com/gin-contrib/gzip v1.0.0/go.mod h1:CtG7tQrPB3vIBo6Gat9FVUsis+1emjvQqd66ME5TdnE= 20 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 21 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 22 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 23 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 24 | github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= 25 | github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 26 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 27 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 28 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 29 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 30 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 31 | github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= 32 | github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 33 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 34 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 35 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 36 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 37 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 38 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 39 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 40 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 41 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 42 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 43 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 44 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 45 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 46 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 47 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 48 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 49 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 50 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 51 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 52 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 53 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 54 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 55 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 58 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 59 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 60 | github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= 61 | github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 62 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 64 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 65 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 66 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 69 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 70 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 71 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 72 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 73 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 74 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 75 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 76 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 77 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 78 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 79 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 80 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 81 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 82 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 83 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 84 | golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= 85 | golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 86 | golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= 87 | golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 88 | golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= 89 | golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= 90 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 91 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 92 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 93 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 94 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 95 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 96 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 97 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= 98 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 99 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 100 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 101 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 102 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 103 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 104 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 105 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 106 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 107 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 108 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "lx-source/src/caches" 6 | "lx-source/src/caches/cloudcache" 7 | "lx-source/src/caches/localcache" 8 | "lx-source/src/env" 9 | 10 | // "lx-source/src/sources" 11 | // "lx-source/src/sources/builtin" 12 | "net/http" 13 | stdurl "net/url" 14 | "path/filepath" 15 | 16 | "github.com/ZxwyWebSite/cr-go-sdk" 17 | "github.com/ZxwyWebSite/ztool" 18 | "github.com/ZxwyWebSite/ztool/logs" 19 | "github.com/ZxwyWebSite/ztool/zcypt" 20 | "github.com/gin-gonic/gin" 21 | ) 22 | 23 | // 生成连接码 24 | func genAuth() { 25 | ga := env.Loger.NewGroup(`LxM-Auth`) 26 | // 检测Key是否存在, 否则生成并保存 27 | if env.Config.Auth.ApiKey_Value == `` { 28 | pass := zcypt.Base64ToString(base64.StdEncoding, zcypt.RandomBytes(4*4)) 29 | env.Config.Auth.ApiKey_Value = pass // env.Config.Apis.LxM_Auth 30 | ga.Info(`已生成默认连接码: %q`, pass) 31 | ga.Info(`可在配置文件 [Auth].ApiKey_Value 中修改`) //可在配置文件 [Apis].LxM_Auth 中修改, 填写 "null" 关闭验证 32 | if err := env.Cfg.Save(``); err != nil { 33 | ga.Error(`写入配置文件失败: %s, 将导致下次启动时连接码发生变化`, err) 34 | } 35 | } 36 | if !env.Config.Auth.ApiKey_Enable { 37 | ga.Warn(`已关闭Key验证, 公开部署可能导致安全隐患`) 38 | } else { 39 | ga.Warn(`已开启Key验证, 记得在脚本中填写 apipass=%q`, env.Config.Auth.ApiKey_Value) 40 | } 41 | ga.Free() 42 | } 43 | 44 | // 加载文件日志 (请在初始化配置文件后调用) 45 | func loadFileLoger() { 46 | // 最后加载FileLoger保证必要日志已输出 (Debug模式强制在控制台输出日志) 47 | if env.Config.Main.LogPath != `` { 48 | lg := env.Loger.NewGroup(`FileLoger`) 49 | printout := env.Config.Main.Print // || env.Config.Main.Debug 50 | f, do, err := env.Loger.SetOutFile(ztool.Str_FastConcat(env.RunPath, env.Config.Main.LogPath), printout) 51 | if err == nil { 52 | // env.Defer.Add(do) 53 | env.Defer.Add(func() { do(); f.Close() }) 54 | env.Tasker.Add(`flog_flush`, func(loger *logs.Logger, now int64) error { 55 | loger.Debug(`已写入文件并清理日志缓存`) 56 | return do() 57 | }, 3600, false) 58 | gin.DefaultWriter = env.Loger.GetOutput() 59 | gin.ForceConsoleColor() 60 | // lg.Info(`文件日志初始化成功`) 61 | } else { 62 | lg.Error(`文件日志初始化失败:%v`, err) 63 | } 64 | lg.Free() 65 | } 66 | } 67 | 68 | // 初始化基础功能 69 | func initMain() { 70 | // 载入内存缓存 71 | storepath := env.RunPath + env.Config.Main.Store 72 | env.Cache.MustRestore(storepath) 73 | env.Defer.Add(func() { env.Cache.MustPersist(storepath) }) 74 | env.Tasker.Add(`memo_flush`, func(*logs.Logger, int64) error { 75 | return env.Cache.Persist(storepath) 76 | }, 3600, false) 77 | 78 | // 初始化数据库 79 | // idb := env.Loger.NewGroup(`InitDB`) 80 | // switch `sqlite` { 81 | // case `memo`: 82 | // break 83 | // case `sqlite`: 84 | // err := database.InitDB(`data/data.db`) 85 | // if err != nil { 86 | // idb.Error(`数据库载入失败: %s`, err) 87 | // } 88 | // default: 89 | // idb.Error(`未定义的数据库模式,请检查配置 [DataBase].Mode`) 90 | // } 91 | // idb.Free() 92 | 93 | // 初始化代理 94 | ipr := env.Loger.NewGroup(`InitProxy`) 95 | switch env.Config.Source.FakeIP_Mode { 96 | case `0`, `off`: 97 | break 98 | case `1`, `req`: 99 | ipr.Fatal(`暂未实现此功能`) 100 | case `2`, `val`: 101 | if env.Config.Source.FakeIP_Value != `` { 102 | ipr.Info(`已开启伪装IP,当前值: %v`, env.Config.Source.FakeIP_Value) 103 | ztool.Net_header[`X-Real-IP`] = env.Config.Source.FakeIP_Value 104 | ztool.Net_header[`X-Forwarded-For`] = env.Config.Source.FakeIP_Value 105 | } else { 106 | ipr.Error(`伪装IP为空,请检查配置 [Source].FakeIP_Value`) 107 | } 108 | default: 109 | ipr.Error(`未定义的代理模式,请检查配置 [Source].FakeIP_Mode,本次启动禁用IP伪装`) 110 | } 111 | if env.Config.Source.Proxy_Enable { 112 | ipr.Debug(`ProxyAddr: %v`, env.Config.Source.Proxy_Address) 113 | addr, err := stdurl.Parse(env.Config.Source.Proxy_Address) 114 | if err != nil { 115 | ipr.Error(`代理Url解析失败: %s, 将禁用代理功能`, err) 116 | } else { 117 | type chkRegion struct { 118 | AmapFlag int `json:"amap_flag"` 119 | IPFlag int `json:"ip_flag"` 120 | AmapAddress string `json:"amap_address"` 121 | Country string `json:"country"` 122 | Flag int `json:"flag"` 123 | Errcode int `json:"errcode"` 124 | Status int `json:"status"` 125 | Error string `json:"error"` 126 | } 127 | var out chkRegion 128 | oldval := ztool.Net_client.Transport 129 | ztool.Net_client.Transport = &http.Transport{Proxy: http.ProxyURL(addr)} 130 | err := ztool.Net_Request(http.MethodGet, 131 | `https://mips.kugou.com/check/iscn?&format=json`, nil, 132 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeader(ztool.Net_header)}, 133 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&out)}, 134 | ) 135 | if err != nil { 136 | ztool.Net_client.Transport = oldval 137 | ipr.Error(`地区验证失败: %s, 已恢复默认配置`, err) 138 | } else { 139 | ipr.Debug(`Resp: %+v`, out) 140 | if out.Flag != 1 { 141 | ipr.Warn(`您正在使用非中国大陆(%v)代理,可能导致部分音乐不可用`, out.Country) 142 | } else { 143 | ipr.Warn(`代理开启成功,请注意潜在的Cookie泄露问题`) 144 | } 145 | } 146 | } 147 | } 148 | ipr.Free() 149 | 150 | // 初始化缓存 151 | icl := env.Loger.NewGroup(`InitCache`) 152 | switch env.Config.Cache.Mode { 153 | case `0`, `off`: 154 | // NothingToDo... (已默认禁用缓存) 155 | break 156 | case `1`, `local`: 157 | // 注:由于需要修改LocalCachePath参数,必须在InitRouter之前执行 158 | cache, err := caches.New(&localcache.Cache{ 159 | Path: filepath.Join(env.RunPath, env.Config.Cache.Local_Path), 160 | Bind: env.Config.Cache.Local_Bind, 161 | }) 162 | if err != nil { 163 | icl.Error(`驱动["local"]初始化失败: %v, 将禁用缓存功能`, err) 164 | } 165 | caches.UseCache = cache 166 | icl.Warn(`本地缓存绑定地址:%q,请确认其与实际访问地址相符`, env.Config.Cache.Local_Bind) 167 | // LocalCachePath = filepath.Join(runPath, env.Config.Cache.Local_Path) 168 | // UseCache = &localcache.Cache{ 169 | // Path: LocalCachePath, 170 | // Addr: env.Config.Apis.BindAddr, 171 | // } 172 | // icl.Info(`使用本地缓存,文件路径 %q,绑定地址 %v`, LocalCachePath, env.Config.Apis.BindAddr) 173 | case `2`, `cloudreve`: 174 | icl.Warn(`欢迎使用新版 Cloudreve 驱动, 由 cr-go-sdk 提供强力支持`) 175 | site := &cr.SiteObj{ 176 | Addr: env.Config.Cache.Cloud_Site, 177 | ApiVer: cr.ApiV383, 178 | Users: &cr.UserObj{ 179 | Mail: env.Config.Cache.Cloud_User, 180 | Pass: env.Config.Cache.Cloud_Pass, 181 | Cookie: cr.ParseCookie(env.Config.Cache.Cloud_Sess), 182 | }, 183 | } 184 | cache, err := caches.New(&cloudcache.Cache{ 185 | Site: site, 186 | Path: env.Config.Cache.Cloud_Path, 187 | }) 188 | if err != nil { 189 | icl.Error(`驱动["cloudreve"]初始化失败: %v, 将禁用缓存功能`, err) 190 | } else { 191 | env.Tasker.Add(`cloud_sess`, func(l *logs.Logger, i int64) error { 192 | if sess := site.Users.Cookie.String(); sess != env.Config.Cache.Cloud_Sess { 193 | env.Config.Cache.Cloud_Sess = sess 194 | } 195 | return env.Cfg.Save(``) 196 | }, 3600, true) 197 | } 198 | caches.UseCache = cache 199 | default: 200 | icl.Error(`未定义的缓存模式,请检查配置 [Cache].Mode,本次启动禁用缓存`) 201 | } 202 | icl.Free() 203 | 204 | // 初始化音乐源 205 | // ise := env.Loger.NewGroup(`InitSource`) 206 | // switch env.Config.Source.Mode { 207 | // case `0`, `off`: 208 | // break 209 | // case `1`, `builtin`: 210 | // sources.UseSource = &builtin.Source{} 211 | // case `2`, `custom`: 212 | // ise.Fatal(`暂未实现账号解析源`) 213 | // default: 214 | // ise.Error(`未定义的音乐源,请检查配置 [Source].Mode,本次启动禁用内置源`) 215 | // } 216 | // ise.Free() 217 | } 218 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "io/fs" 7 | "lx-source/src/env" 8 | "lx-source/src/server" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "strings" 13 | "sync" 14 | "sync/atomic" 15 | "syscall" 16 | "time" 17 | 18 | "github.com/ZxwyWebSite/ztool" 19 | "github.com/ZxwyWebSite/ztool/logs" 20 | "github.com/gin-gonic/gin" 21 | ) 22 | 23 | // 初始化 24 | func init() { 25 | ztool.Cmd_FastPrint(ztool.Str_FastConcat(` 26 | __ __ __ ______ ______ __ __ ____ ______ ______ 27 | / / / / / / / ____/ / __ / / / / / / __ \ / ____/ / ____/ 28 | / / / /_/ / __ / /___ / / / / / / / / / /_/ / / / / /___ 29 | / / \_\ \ /_/ /___ / / / / / / / / / / ___/ / / / ____/ 30 | / /___ / / / / ____/ / / /_/ / / /_/ / / / \ / /___ / /___ 31 | /_____/ /_/ /_/ /_____/ /_____/ /_____/ /_/ \_\ /_____/ /_____/ 32 | ======================================================================= 33 | Version: `, env.Version, ` Github: https://github.com/ZxwyWebSite/lx-source 34 | `, "\n")) 35 | env.RunPath, _ = os.Getwd() 36 | var confPath string 37 | flag.StringVar(&confPath, `c`, ztool.Str_FastConcat(env.RunPath, `/data/conf.ini`), `指定配置文件路径`) 38 | etag := flag.String(`e`, ``, `扩展启动参数`) 39 | perm := flag.Uint(`p`, 0, `自定义文件权限(8进制前面加0)`) 40 | flag.Parse() 41 | if perm != nil && *perm != 0 { 42 | ztool.Fbj_DefPerm = fs.FileMode(*perm) 43 | fp := env.Loger.NewGroup(`FilePerm`) 44 | // if ztool.Fbj_DefPerm > 777 { 45 | // fp.Fatal(`请在实际权限前面加0`) 46 | // } 47 | fp.Info(`设置默认文件权限为 %o (%v)`, *perm, ztool.Fbj_DefPerm).Free() 48 | } 49 | env.Cfg.MustInit(confPath) 50 | parseEtag(etag) 51 | // fmt.Printf("%+v\n", env.Config) 52 | env.Loger.NewGroup(`ServHello`).Info(`欢迎使用 LX-SOURCE 洛雪音乐自定义源`).Free() 53 | if !env.Config.Main.Debug { 54 | gin.SetMode(gin.ReleaseMode) 55 | } else { 56 | logs.Levell = logs.LevelDebu // logs.Level = 3 57 | env.Loger.NewGroup(`DebugMode`).Debug(`已开启调试模式, 将输出更详细日志 (配置文件中 [Main].Debug 改为 false 关闭)`).Free() 58 | } 59 | genAuth() 60 | if env.Config.Main.SysLev { 61 | sl := env.Loger.NewGroup(`(beta)SysLev`) 62 | if err := ztool.Sys_SetPriorityLev(ztool.Sys_GetPid(), ztool.Sys_PriorityHighest); err != nil { 63 | sl.Error(`系统优先级设置失败: %v`, err) 64 | } else { 65 | sl.Warn(`成功设置较高优先级,此功能可能导致系统不稳定`) 66 | } 67 | sl.Free() 68 | } 69 | if env.Config.Main.Timeout != env.DefCfg.Main.Timeout { 70 | ztool.Net_client.Timeout = time.Second * time.Duration(env.Config.Main.Timeout) // 自定义请求超时 71 | env.Loger.NewGroup(`InitNet`).Info(`请求超时已设为 %s`, ztool.Net_client.Timeout).Free() 72 | } 73 | } 74 | 75 | func main() { 76 | defer env.Defer.Do() 77 | // 初始化基础功能 78 | initMain() 79 | 80 | // 载入必要模块 81 | env.Inits.Do() 82 | env.Loger.NewGroup(`ServInit`).Info(`服务端启动, 监听地址 %s`, strings.Join(env.Config.Main.Listen, `|`)).Free() 83 | loadFileLoger() 84 | env.Defer.Add(env.Tasker.Run(env.Loger)) // wait 85 | 86 | // 启动Http服务 87 | listenAndServe(server.InitRouter(), env.Config.Main.Listen) 88 | } 89 | 90 | // 监听多端口 91 | func listenAndServe(handler http.Handler, addrs []string) { 92 | // 前置检测 93 | length := len(addrs) 94 | ss := env.Loger.NewGroup(`ServStart`) 95 | if length == 0 { 96 | ss.Fatal(`监听地址列表为空`) 97 | } 98 | // ss.Info(`服务端启动,请稍候...`) 99 | srvlist := make(map[int]*http.Server, length) // 伪数组,便于快速删除数据 100 | lock := new(sync.Mutex) 101 | var failnum int32 102 | length32 := int32(length) 103 | // 启动服务 104 | for i := 0; i < length; i++ { 105 | lock.Lock() 106 | srvlist[i] = &http.Server{Addr: addrs[i], Handler: handler} 107 | lock.Unlock() 108 | go func(n int) { 109 | server := srvlist[n] 110 | // ss.Info(`开始监听 %v`, server.Addr) 111 | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 112 | ss.Error(`监听%q失败: %s`, server.Addr, err) // 监听":1011"失败: http: Server closed 113 | fn := atomic.AddInt32(&failnum, 1) 114 | if fn == length32 { 115 | ss.Fatal(`所有地址监听失败,程序被迫退出`) 116 | } 117 | lock.Lock() 118 | delete(srvlist, n) 119 | lock.Unlock() 120 | } 121 | }(i) 122 | } 123 | // time.Sleep(time.Millisecond * 300) 124 | // if len(srvlist) == 0 { 125 | // ss.Fatal(`所有地址监听失败,程序被迫退出`) 126 | // } 127 | // ss.Free() 128 | // 安全退出 129 | quit := make(chan os.Signal, 1) 130 | signal.Notify(quit, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT) 131 | <-quit 132 | sc := env.Loger.NewGroup(`ServClose`) 133 | sc.Info(`等待结束活动连接...`) 134 | // 停止服务 135 | var unsafenum int32 136 | wg := new(sync.WaitGroup) 137 | for i := range srvlist { 138 | wg.Add(1) 139 | go func(n int) { 140 | server := srvlist[n] 141 | ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) 142 | if err := server.Shutdown(ctx); err != nil { 143 | sc.Warn(`连接%q未安全退出: %s`, server.Addr, err) // 连接":1011"未安全退出: timeout 144 | atomic.AddInt32(&unsafenum, 1) 145 | } 146 | cancel() 147 | wg.Done() 148 | }(i) 149 | } 150 | wg.Wait() 151 | if unsafenum != 0 { 152 | sc.Warn(`未安全退出 :(`) 153 | } else { 154 | sc.Info(`已安全退出 :)`) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /menu.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "lx-source/src/env" 6 | "lx-source/src/sources/custom/tx" 7 | wm "lx-source/src/sources/custom/wy/modules" 8 | "runtime" 9 | "strings" 10 | "time" 11 | 12 | qrcode "github.com/skip2/go-qrcode" 13 | ) 14 | 15 | func parseEtag(etag *string) { 16 | if etag == nil { 17 | return 18 | } 19 | loger := env.Loger.NewGroup(`ParseEtag`) 20 | switch *etag { 21 | case ``: 22 | break 23 | case `menu`: 24 | loger.Fatal(`暂不支持交互菜单,敬请期待...`) 25 | // menuMian() 26 | case `wyqr`: 27 | wyQrLogin() 28 | case `txqq`: 29 | txQqLogin() 30 | default: 31 | loger.Fatal(`未知参数:%q`, *etag) 32 | } 33 | loger.Free() 34 | } 35 | 36 | // 网易云扫码登录 37 | func wyQrLogin() { 38 | loger := env.Loger.NewGroup(`WyQrLogin`) 39 | defer loger.Free() 40 | loger.Info(`执行模块: 网易云扫码登录`) 41 | 42 | if env.Config.Custom.Wy_Api_Cookie != `` { 43 | loger.Warn("已存在账号数据, 继续操作可能导致数据覆盖丢失!") 44 | fmt.Print(`输入'y'继续: `) 45 | var input string 46 | fmt.Scanln(&input) 47 | if input != `y` { 48 | loger.Fatal(`用户取消操作`) 49 | } 50 | } 51 | 52 | res, err := wm.LoginQrKey() 53 | if err != nil { 54 | loger.Fatal(`无法创建请求: %s`, err) 55 | } 56 | key := res.Body[`unikey`].(string) 57 | loger.Info(`创建请求成功: %v`, key) 58 | 59 | link := wm.LoginQrCreate(key) 60 | qr, err := qrcode.New(link, qrcode.Low) 61 | if err != nil { 62 | loger.Fatal(`无法生成二维码: %s`, err) 63 | } 64 | loger.Info("\n请使用网易云音乐手机APP扫描以下二维码授权登录:\n%v", qr.ToSmallString(false)) 65 | 66 | for { 67 | time.Sleep(time.Second * 5) 68 | res, err = wm.LoginQrCheck(key) 69 | if err != nil { 70 | loger.Error(`检测状态失败: %s`, err) 71 | continue 72 | } 73 | msg := res.Body[`message`].(string) 74 | switch msg { 75 | case `等待扫码`: 76 | loger.Info(msg) 77 | case `授权中`: 78 | loger.Info(`扫码成功: %q, 请在手机上确认登录`, res.Body[`nickname`]) 79 | case `授权登陆成功`: 80 | loger.Info(`授权成功`) 81 | env.Config.Custom.Wy_Enable = true 82 | env.Config.Custom.Wy_Mode = `163api` 83 | env.Config.Custom.Wy_Api_Cookie = strings.Join(res.Cookie, `; `) 84 | env.Config.Custom.Wy_Refresh_Enable = true 85 | if err := env.Cfg.Save(``); err != nil { 86 | loger.Error(`配置保存失败: %s`, err) 87 | } else { 88 | loger.Info(`配置保存成功`) 89 | } 90 | return 91 | case `二维码不存在或已过期`: 92 | loger.Fatal(`授权请求超时,请重试!`) 93 | default: 94 | loger.Fatal(`未知状态: %v`, msg) 95 | } 96 | } 97 | } 98 | 99 | // QQ快速登录 100 | func txQqLogin() { 101 | loger := env.Loger.NewGroup(`TxQqLogin`) 102 | defer loger.Free() 103 | loger.Info(`执行模块: QQ快速登录`) 104 | 105 | if runtime.GOOS != `windows` { 106 | loger.Fatal(`该模块仅支持在windows环境下使用`) 107 | return 108 | } 109 | 110 | if env.Config.Custom.Tx_Ukey != `` { 111 | loger.Warn("已存在账号数据, 继续操作可能导致数据覆盖丢失!") 112 | fmt.Print(`输入'y'继续: `) 113 | var input string 114 | fmt.Scanln(&input) 115 | if input != `y` { 116 | loger.Fatal(`用户取消操作`) 117 | } 118 | } 119 | 120 | if err := tx.Qlogin_graph(loger); err != nil { 121 | loger.Fatal(err.Error()) 122 | } 123 | } 124 | 125 | // func menuMian() { 126 | // app := menu.NewApp(`Lx-Source`) 127 | // app.Data = menu.Data{ 128 | // `Main`: func(this *menu.App) string { return ` ` }, 129 | // } 130 | // app.Run() 131 | // os.Exit(0) 132 | // } 133 | -------------------------------------------------------------------------------- /pkg/ztool: -------------------------------------------------------------------------------- 1 | ../../ztool -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ``` 2 | (屏幕宽度问题头图无法正常显示请忽略) 3 | __ __ __ ______ ______ __ __ ____ ______ ______ 4 | / / / / / / / ____/ / __ / / / / / / __ \ / ____/ / ____/ 5 | / / / /_/ / __ / /___ / / / / / / / / / /_/ / / / / /___ 6 | / / \_\ \ /_/ /___ / / / / / / / / / / ___/ / / / ____/ 7 | / /___ / / / / ____/ / / /_/ / / /_/ / / / \ / /___ / /___ 8 | /_____/ /_/ /_/ /_____/ /_____/ /_____/ /_/ \_\ /_____/ /_____/ 9 | ======================================================================= 10 | ``` 11 | ## ZxwyWebSite/LX-Source 12 | ### 简介 13 | + LX-Music 解析源 (洛雪音乐自定义源) 14 | + **由于本项目的特殊性,请低调使用,切勿大肆宣传** 15 | + 测试阶段,不代表最终品质 16 | + 验证部分暂未完善,建议仅本地部署,不要公开发布 17 | + 视频教程:[使用教程.mp4](https://r2eu.zxwy.link/gh/lx-source/v1.0.2-b0.1/%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B.mp4) 18 | 19 | 20 | ### 注意 21 | + 不保证内置源可用性,如需长期使用请自备会员账号 22 | + 音源最好留着自己用,你非要当大好人当我没说(由于传播导致的账号被封与本项目无关) 23 | 24 | ### 使用 25 | #### 服务端 26 | + 到Release下载对应平台可执行文件运行 27 | + 或下载源码自行编译 28 | #### 客户端 29 | + 使用自定义源脚本:`data/public/lx-custom-source.js` 30 | + (请先运行服务端以释放资源文件) 31 | + **修改 apiaddr 为服务端地址,apipass 为验证Key** 32 | 33 | ### 配置 34 | + 第一次运行自动生成配置后按注释填写即可 35 | + 位置:`{运行目录}/data/conf.ini` 36 | + **注:默认使用本地缓存,请修改 [Cache].Local_Bind 为实际访问地址** 37 | 38 | ### 功能 *(未全部实现)* 39 | + 兼容原版测试接口 40 | + 提供自定义源脚本 41 | + 支持多种工作模式 *(PART)* 42 | - 仅解析(0): 解析后返回原始外链 43 | - 仅缓存(1): 缓存并返回本地数据外链 44 | - 都使用(2): 无缓存返回原始外链并缓存, 有缓存返回本地数据外链 45 | + 设置缓存直链类型 *(TODO)* 46 | - 永久链(0): `file/:s/:id/:q`, 和解析参数相同 47 | - 临时链(1): `file/:{time.unix}/:{md5(cquery)}`, 参考网易云样式, 有效期默认10分钟 48 | #### Api 49 | + `/` 获取服务端信息 50 | + `/link/:s/:id/:q` 查询音乐链接 51 | + `/file/` 本地缓存访问地址 52 | 53 | 54 | ### 音乐源 55 | + 内置源 (抓取自网络公开接口) **注:文明上网,请勿滥用,否则停止后续更新** 56 | + 账号源 (登录会员账号解析) **注:可能导致封号,如出问题本项目不负责** 57 | 58 | ### 开发 59 | + 环境要求:Golang 1.21 (建议 >=1.20) 60 | + 可不开启CGO编译 61 | + 源码较乱,暂未整理... 62 | + zTool包不存在:解压发布页 `ztool.zip` 放在源码上级目录即可使用 63 | #### 源码结构 64 | + / 65 | - pkg/ 依赖包,一般在外部调用,不轻易修改 66 | - src/ 源码包,用于实现各种功能 67 | * env 公用变量,需要全局调用的参数 68 | * database 数据库相关 69 | * caches 文件缓存封装 70 | * server Gin路由 71 | * middleware 请求中间件 72 | * sources 音乐源 73 | 74 | 75 | - build.go 快速构建脚本 (请先根据本地环境编辑配置) 76 | - init.go 初始化检测 77 | - menu.go 交互菜单 (暂未实现) 78 | - main.go 主程序 79 | 80 | ### 其它 81 | + 基于 Golang + Gin框架 编写 82 | + 感谢以下项目提供参考:[Python版](https://github.com/lxmusics/lx-music-api-server-python),[WyApi](https://github.com/ZxwyWebSite/NeteaseCloudMusicApi),... 83 | 84 | ### 更新 85 | + 见 `update.md` 86 | 87 | ### 项目协议 88 | 89 | 本项目基于 [MIT] 许可证发行,以下协议是对于 MIT 原协议的补充,如有冲突,以以下协议为准。 90 | 91 | 词语约定:本协议中的“本项目”指本开源项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本项目内置的包括酷我、酷狗、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。 92 | 93 | 1. 本项目的数据来源原理是从各官方音乐平台的公开服务器中拉取数据,经过对数据简单地筛选与合并后进行展示,因此本项目不对数据的准确性负责。 94 | 2. 使用本项目的过程中可能会产生版权数据,对于这些版权数据,本项目不拥有它们的所有权,为了避免造成侵权,使用者务必在**24 小时**内清除使用本项目的过程中所产生的版权数据。 95 | 3. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。 96 | 4. 本项目完全免费,且开源发布于 GitHub 面向全世界人用作对技术的学习交流,本项目不对项目内的技术可能存在违反当地法律法规的行为作保证,**禁止在违反当地法律法规的情况下使用本项目**,对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。 97 | 98 | 若你使用了本项目,将代表你接受以上协议。 99 | 100 | 音乐平台不易,请尊重版权,支持正版。 101 | 本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。 102 | 若对此有疑问请 mail to: admin+zxwy.link (请将`+`替换为`@`) 103 | -------------------------------------------------------------------------------- /release.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | // 获取版本号 13 | func version() string { 14 | fenv, _ := os.Open(`src/env/env.go`) 15 | benv := bufio.NewReader(fenv) 16 | var ever string 17 | for { 18 | line, _, _ := benv.ReadLine() 19 | length := len(line) 20 | if length == 0 { 21 | continue 22 | } 23 | sline := string(line) 24 | if strings.HasPrefix(sline, ` Version`) { 25 | ever = `v` + sline[12:length-1] 26 | break 27 | } 28 | } 29 | fenv.Close() 30 | if ever == `` { 31 | panic(`No Version`) 32 | } else { 33 | return ever 34 | } 35 | } 36 | 37 | // 生成更新日志 38 | func changelog(ever string) string { 39 | fupd, _ := os.Open(`update.md`) 40 | bupd := bufio.NewReader(fupd) 41 | var eupd strings.Builder 42 | eupd.WriteString(`### 更新内容:`) 43 | eupd.WriteByte('\n') 44 | for { 45 | line, _, _ := bupd.ReadLine() 46 | length := len(line) 47 | if length == 0 { 48 | continue 49 | } 50 | if strings.Contains(string(line), ever) { 51 | for { 52 | lline, _, _ := bupd.ReadLine() 53 | length := len(lline) 54 | if length == 0 { 55 | break 56 | } 57 | eupd.WriteString(string(lline)) 58 | eupd.WriteByte('\n') 59 | } 60 | break 61 | } 62 | } 63 | fupd.Close() 64 | eupd.WriteByte('\n') 65 | eupd.WriteString(`### CDN加速下载:`) 66 | eupd.WriteByte('\n') 67 | for _, v := range []string{ 68 | `lx-source-android-arm.zip`, 69 | `lx-source-android-arm64.zip`, 70 | `lx-source-linux-amd64v2.zip`, 71 | `lx-source-linux-amd64v3.zip`, 72 | `lx-source-linux-arm7.zip`, 73 | `lx-source-linux-arm64.zip`, 74 | `lx-source-windows-amd64v2.zip`, 75 | `lx-source-windows-amd64v2-go1.20.14.zip`, 76 | `lx-source-windows-amd64v3.zip`, 77 | } { 78 | eupd.WriteByte('+') 79 | eupd.WriteByte(' ') 80 | 81 | eupd.WriteByte('[') 82 | eupd.WriteString(v) 83 | eupd.WriteByte(']') 84 | eupd.WriteByte('(') 85 | eupd.WriteString(`https://r2eu.zxwy.link/gh/lx-source/`) 86 | eupd.WriteString(ever) 87 | eupd.WriteByte('/') 88 | eupd.WriteString(v) 89 | eupd.WriteByte(')') 90 | 91 | eupd.WriteByte('\n') 92 | } 93 | return eupd.String() 94 | } 95 | 96 | func main() { 97 | ever := version() 98 | fmt.Println(ever) 99 | 100 | eupd := changelog(ever) 101 | file, err := os.Create(`changelog.md`) 102 | if err != nil { 103 | panic(err) 104 | } 105 | file.WriteString(eupd) 106 | file.Close() 107 | } 108 | -------------------------------------------------------------------------------- /rsrc_windows_amd64.syso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZxwyWebSite/lx-source/9155f341051d2570e06bf9acaa38476705a3bc4d/rsrc_windows_amd64.syso -------------------------------------------------------------------------------- /src/caches/cache.go: -------------------------------------------------------------------------------- 1 | package caches 2 | 3 | import ( 4 | "lx-source/src/env" 5 | "net/http" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/ZxwyWebSite/ztool" 10 | "github.com/ZxwyWebSite/ztool/logs" 11 | ) 12 | 13 | type ( 14 | // 查询参数 15 | Query struct { 16 | Source string // source 平台 wy, mg 17 | MusicID string // sid 音乐ID wy: songmid, mg: copyrightId 18 | Quality string // quality 音质 128k / 320k / flac / flac24bit 19 | Extname string // rext 扩展名 mp3 / flac (没有前缀点) 20 | query string // 查询字符串缓存 21 | Request *http.Request 22 | } 23 | // 缓存需实现以下接口 24 | Cache interface { 25 | // 获取缓存 (查询参数 query)(外链) 26 | /* `wy/10086/128k.mp3`->`http://192.168.10.22:1011/file/wy/10086/128k.mp3` */ 27 | Get(*Query) string 28 | // 设置缓存 (查询参数 query, 音乐直链 link)(外链) 29 | /* (`wy/10086/128k.mp3`,`https://xxx.xxxx.xx/file.mp3`)->`http://192.168.10.22:1011/file/wy/10086/128k.mp3` */ 30 | Set(*Query, string) string 31 | // 可用状态 true/false 32 | Stat() bool 33 | // 初始化 ()(错误) 34 | Init() error 35 | } 36 | ) 37 | 38 | // 默认无缓存的缓存 39 | type Nullcache struct{} 40 | 41 | func (*Nullcache) Get(*Query) string { return `` } 42 | func (*Nullcache) Set(*Query, string) string { return `` } 43 | func (*Nullcache) Stat() bool { return false } 44 | func (*Nullcache) Init() error { return nil } 45 | 46 | var ( 47 | UseCache Cache = &Nullcache{} 48 | // ErrNotInited = errors.New(`缓存策略未初始化`) 49 | query_pool = sync.Pool{New: func() any { return new(Query) }} 50 | ) 51 | 52 | // 对象池相关 (注:结构体释放时一定要清理未导出字段) 53 | func newQuery() *Query { return query_pool.Get().(*Query) } 54 | func (c *Query) Free() { c.query = ``; query_pool.Put(c) } 55 | 56 | // 根据音质判断文件后缀 57 | func rext(q string) string { 58 | if /*ztool.Chk_IsMatch(q, `128k`, `320k`)*/ q == `128k` || q == `320k` { 59 | return `mp3` 60 | } 61 | return `flac` 62 | } 63 | 64 | // 生成查询参数 (必须使用此函数初始化) 65 | func NewQuery(s, id, q string) *Query { 66 | out := newQuery() 67 | out.Source = s 68 | out.MusicID = id 69 | out.Quality = q 70 | out.Extname = rext(q) 71 | return out 72 | // return &Query{ 73 | // Source: s, 74 | // MusicID: id, 75 | // Quality: q, 76 | // Extname: rext(q), 77 | // } 78 | } 79 | 80 | // 获取旧版查询字符串 81 | func (c *Query) Query() string { 82 | if c.query == `` { 83 | c.query = ztool.Str_FastConcat(c.Source, `/`, c.MusicID, `/`, c.Quality, `.`, c.Extname) 84 | } 85 | return c.query 86 | } 87 | 88 | // 分割查询字符串 (已弃用) 89 | /* 90 | kg: 分割 Hash-Album 如 "6DC276334F56E22BE2A0E8254D332B45-13097991" 91 | tx: 分割 songmid-strMediaMid 如 "002fktJg3cmSpC-000V6uuv35Cwnh" 92 | */ 93 | func (c *Query) Split() []string { 94 | sep := strings.Split(c.MusicID, `-`) 95 | if len(sep) >= 2 { 96 | return sep 97 | } 98 | return append(sep, ``) 99 | } 100 | 101 | // 初始化缓存 102 | func New(c Cache) (Cache, error) { 103 | err := c.Init() 104 | return c, err 105 | // if err != nil { 106 | // return nil, err 107 | // } 108 | // return c, nil 109 | } 110 | func MustNew(c Cache) Cache { 111 | out, err := New(c) 112 | if err != nil { 113 | panic(err) 114 | } 115 | return out 116 | } 117 | 118 | var Loger *logs.Logger 119 | 120 | // 初始化Loger 121 | func init() { 122 | env.Inits.Add(func() { 123 | Loger = env.Loger.NewGroup(`Caches`) 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /src/caches/cloudcache/cloud.go: -------------------------------------------------------------------------------- 1 | package cloudcache 2 | 3 | import ( 4 | "lx-source/src/caches" 5 | "lx-source/src/env" 6 | "net/http" 7 | "strings" 8 | 9 | cr "github.com/ZxwyWebSite/cr-go-sdk" 10 | "github.com/ZxwyWebSite/cr-go-sdk/service/explorer" 11 | "github.com/ZxwyWebSite/ztool" 12 | ) 13 | 14 | type Cache struct { 15 | Site *cr.SiteObj 16 | Path string 17 | state bool 18 | } 19 | 20 | func (c *Cache) Get(q *caches.Query) string { 21 | var b strings.Builder 22 | b.WriteString(c.Path) 23 | b.WriteByte('/') 24 | b.WriteString(q.Source) 25 | b.WriteByte('/') 26 | b.WriteString(q.MusicID) 27 | list, err := c.Site.Directory(b.String()) 28 | if err != nil { 29 | caches.Loger.Debug(`列出目录: %v`, err) 30 | return `` 31 | } 32 | name := q.Quality + `.` + q.Extname 33 | var id string 34 | for _, v := range list.Objects { 35 | if v.Name == name && v.Type == `file` { 36 | id = v.ID 37 | break 38 | } 39 | } 40 | if id == `` { 41 | caches.Loger.Debug(`文件不存在`) 42 | return `` 43 | } 44 | srcs, err := c.Site.FileSource(cr.GenerateSrc(false, id)) 45 | if err != nil { 46 | caches.Loger.Debug(`生成外链: %v`, err) 47 | return `` 48 | } 49 | return (*srcs)[0].URL 50 | /*link, err := c.Site.FileDownload(id) 51 | if err != nil { 52 | caches.Loger.Debug(`下载文件: %v`, err) 53 | return `` 54 | } 55 | if (*link)[0] == '/' { 56 | return c.Site.Addr + (*link)[1:] 57 | } 58 | return *link*/ 59 | } 60 | 61 | func (c *Cache) Set(q *caches.Query, l string) string { 62 | var b strings.Builder 63 | b.WriteString(c.Path) 64 | b.WriteByte('/') 65 | b.WriteString(q.Source) 66 | b.WriteByte('/') 67 | b.WriteString(q.MusicID) 68 | dir := b.String() 69 | err := c.Site.DirectoryNew(&explorer.DirectoryService{ 70 | Path: dir, 71 | }) 72 | if err != nil { 73 | caches.Loger.Debug(`创建目录: %v`, err) 74 | return `` 75 | } 76 | /*var buf bytes.Buffer 77 | err = ztool.Net_Download(l, &buf, nil) 78 | if err != nil { 79 | caches.Loger.Debug(`下载文件: %v`, err) 80 | return `` 81 | }*/ 82 | name := q.Quality + `.` + q.Extname 83 | err = ztool.Net_Request( 84 | http.MethodGet, l, nil, 85 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeaders()}, 86 | []ztool.Net_ResHandlerFunc{func(res *http.Response) error { 87 | return (&cr.UploadTask{ 88 | Site: c.Site, 89 | File: res.Body, 90 | Size: uint64(res.ContentLength), 91 | Name: name, 92 | Mime: `audio/mpeg`, 93 | }).Do(dir) 94 | }}, 95 | ) 96 | if err != nil { 97 | caches.Loger.Debug(`上传文件: %v`, err) 98 | return `` 99 | } 100 | return c.Get(q) 101 | } 102 | 103 | func (c *Cache) Stat() bool { 104 | return c.state 105 | } 106 | 107 | func (c *Cache) Init() error { 108 | cr.Cr_Debug = env.Config.Main.Debug 109 | err := c.Site.SdkInit() 110 | if err != nil { 111 | return err 112 | } 113 | if c.Site.Users.Cookie == nil || c.Site.Config.User.Anonymous { 114 | err = c.Site.SdkLogin() 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | c.state = true 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /src/caches/localcache/local.go: -------------------------------------------------------------------------------- 1 | package localcache 2 | 3 | import ( 4 | "errors" 5 | "lx-source/src/caches" 6 | "lx-source/src/env" 7 | "lx-source/src/middleware/util" 8 | "net/url" 9 | "os" 10 | "strings" 11 | "time" 12 | 13 | "github.com/ZxwyWebSite/ztool" 14 | ) 15 | 16 | type Cache struct { 17 | Path string // 本地缓存目录 cache 18 | Bind string // Api地址,用于生成外链 http://192.168.10.22:1011/ 19 | state bool // 激活状态 20 | } 21 | 22 | // var loger = env.Loger.NewGroup(`Caches`) //caches.Loger.AppGroup(`local`) 23 | 24 | func (c *Cache) getLink(q *caches.Query) (furl string) { 25 | // fmt.Printf("%#v\n", q.Request) 26 | if env.Config.Cache.Local_Auto { 27 | // 注:此方式无法确定是否支持HTTPS,暂时默认HTTP,让Nginx重定向 28 | furl = ztool.Str_FastConcat(util.GetPath(q.Request, `link/`), `file/`, q.Query()) 29 | } else { 30 | furl = ztool.Str_FastConcat(c.Bind, `/file/`, q.Query()) 31 | } 32 | return 33 | } 34 | 35 | func (c *Cache) Get(q *caches.Query) string { 36 | // 加一层缓存,减少重复检测文件造成的性能损耗 (缓存已移至Router) 37 | // if _, ok := env.Cache.Get(q.Query()); !ok { 38 | if _, e := os.Stat(ztool.Str_FastConcat(c.Path, `/`, q.Query())); e != nil { 39 | return `` 40 | } 41 | // env.Cache.Set(q.Query(), struct{}{}, 3600) 42 | // } 43 | return c.getLink(q) 44 | // fpath := filepath.Join(c.Path, q.Source, q.MusicID, q.Quality) 45 | // if _, e := os.Stat(fpath); e != nil { 46 | // return `` 47 | // } 48 | // return c.getLink(fpath) 49 | } 50 | 51 | func (c *Cache) Set(q *caches.Query, l string) string { 52 | fpath := ztool.Str_FastConcat(c.Path, `/`, q.Query()) 53 | // if env.Config.Main.FFConv && q.Source == `kg` { // ztool.Chk_IsMatch(q.Source, `kg`) 54 | // err := ztool.Fbj_MkdirAll(fpath, 0644) 55 | // if err != nil { 56 | // loger.Error(`DownloadFile_Mkdir: %v`, err) 57 | // return `` 58 | // } 59 | // out, err := ztool.Cmd_aWaitExec(ztool.Str_FastConcat(`ffmpeg -i "`, l, `" -vn`, ` -c:a copy`, ` "`, fpath, `"`)) 60 | // if err != nil { 61 | // loger.Error(`DownloadFile_Exec: %v, Output: %v`, err, out) 62 | // return `` 63 | // } 64 | // loger.Debug(`FFMpeg_Out: %v`, out) 65 | // } else { 66 | for i := 0; true; i++ { 67 | err := ztool.Net_DownloadFile(l, fpath, nil) 68 | if err == nil { 69 | break 70 | } 71 | caches.Loger.Error(`DownloadFile: %v, Retry: %v`, err, i) 72 | if i == 1 || !strings.Contains(err.Error(), `context deadline exceeded`) { 73 | if err := os.Remove(fpath); err != nil { 74 | caches.Loger.Error(`RemoveFile: %s`, err) 75 | } 76 | return `` 77 | } 78 | time.Sleep(time.Second) 79 | } 80 | // } 81 | // env.Cache.Set(q.Query(), struct{}{}, 3600) 82 | return c.getLink(q) 83 | // fpath := filepath.Join(c.Path, q.String) 84 | // os.MkdirAll(filepath.Dir(fpath), fs.ModePerm) 85 | // g := c.Loger.NewGroup(`localcache`) 86 | // ret, err := ztool.Net_HttpReq(http.MethodGet, l, nil, nil, nil) 87 | // if err != nil { 88 | // g.Warn(`HttpReq: %s`, err) 89 | // return `` 90 | // } 91 | // if err := os.WriteFile(fpath, ret, fs.ModePerm); err != nil { 92 | // g.Warn(`WriteFile: %s`, err) 93 | // return `` 94 | // } 95 | // return c.getLink(fpath) 96 | } 97 | 98 | func (c *Cache) Stat() bool { 99 | return c.state 100 | } 101 | 102 | func (c *Cache) Init() error { 103 | if c.Bind == `` { 104 | return errors.New(`请输入Api地址以生成外链`) 105 | } else { 106 | ubj, err := url.Parse(c.Bind) 107 | if err != nil { 108 | return err 109 | } 110 | ubj.Path = strings.TrimSuffix(ubj.Path, `/`) 111 | c.Bind = ubj.String() 112 | } 113 | c.state = true 114 | return nil 115 | } 116 | 117 | // func New(path, addr string, loger *logs.Logger) *Cache { 118 | // return &Cache{ 119 | // Path: path, 120 | // Addr: addr, 121 | // Loger: loger, 122 | // emsg: cache.ErrNotInited, 123 | // } 124 | // } 125 | -------------------------------------------------------------------------------- /src/database/driver.go: -------------------------------------------------------------------------------- 1 | //go:build gorm 2 | 3 | package database 4 | 5 | // import ( 6 | // "lx-source/src/database/modules" 7 | // "lx-source/src/env" 8 | // "lx-source/src/sources" 9 | 10 | // "gorm.io/gorm" 11 | // ) 12 | 13 | // var DB *gorm.DB 14 | 15 | // func InitDB(dsn string) (err error) { 16 | // loger := env.Loger.NewGroup(`InitDB`) 17 | // defer loger.Free() 18 | // DB, err = gorm.Open(modules.Sqlite(dsn), &gorm.Config{}) 19 | // if err == nil { 20 | // for _, typ := range []struct { 21 | // Name string 22 | // Type interface{} 23 | // }{ 24 | // {Name: T_music, Type: &XMusicItem{}}, 25 | // {Name: T_lyric, Type: &XLyricItem{}}, 26 | // } { 27 | // for _, src := range sources.S_al { 28 | // err = DB.Table(src + `_` + typ.Name).AutoMigrate(typ.Type) 29 | // if err != nil { 30 | // return 31 | // } 32 | // } 33 | // } 34 | // } 35 | // return 36 | // } 37 | 38 | // type Driver struct{} 39 | -------------------------------------------------------------------------------- /src/database/modules/sqlite_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build cgo && gorm 2 | 3 | package modules 4 | 5 | // import ( 6 | // "gorm.io/driver/sqlite" 7 | // ) 8 | 9 | // var Sqlite = sqlite.Open 10 | -------------------------------------------------------------------------------- /src/database/modules/sqlite_etc.go: -------------------------------------------------------------------------------- 1 | //go:build !cgo && gorm 2 | 3 | package modules 4 | 5 | // import ( 6 | // "github.com/glebarez/sqlite" 7 | // ) 8 | 9 | // var Sqlite = sqlite.Open 10 | -------------------------------------------------------------------------------- /src/database/types.go: -------------------------------------------------------------------------------- 1 | //go:build gorm 2 | 3 | package database 4 | 5 | // MusicFree 数据结构 6 | // type ( 7 | // // 其他 8 | // IExtra map[string]interface{} 9 | // // 音乐 10 | // IMusicItem struct { 11 | // Artist string `json:"artist"` // 作者 12 | // Title string `json:"title"` // 歌曲标题 13 | // Duration int `json:"duration,omitempty"` // 时长(s) 14 | // Album string `json:"album,omitempty"` // 专辑名 15 | // Artwork string `json:"artwork,omitempty"` // 专辑封面图 16 | // Url string `json:"url,omitempty"` // 默认音源 17 | // Lrc string `json:"lrc,omitempty"` // 歌词URL 18 | // RawLrc string `json:"rawLrc,omitempty"` // 歌词文本(lrc格式 带时间戳) 19 | // Other IExtra `json:"extra,omitempty"` // 其他 20 | // } 21 | // // 歌单 22 | // IMusicSheetItem struct { 23 | // Artwork string `json:"artwork,omitempty"` // 封面图 24 | // Title string `json:"title"` // 标题 25 | // Description string `json:"description,omitempty"` // 描述 26 | // WorksNum int `json:"worksNum,omitempty"` // 作品总数 27 | // PlayCount int `json:"playCount,omitempty"` // 播放次数 28 | // MusicList []IMusicItem `json:"musicList,omitempty"` // 播放列表 29 | // CreateAt int64 `json:"createAt,omitempty"` // 歌单创建日期 30 | // Artist string `json:"artist,omitempty"` // 歌单作者 31 | // Other IExtra `json:"extra,omitempty"` // 其他 32 | // } 33 | // // 专辑 34 | // IAlbumItem IMusicSheetItem 35 | // // 作者 36 | // IArtistItem struct { 37 | // Platform string `json:"platform,omitempty"` // 插件名 38 | // ID interface{} `json:"id"` // 唯一id 39 | // Name string `json:"name"` // 姓名 40 | // Fans int `json:"fans,omitempty"` // 粉丝数 41 | // Description string `json:"description,omitempty"` // 简介 42 | // Avatar string `json:"avatar,omitempty"` // 头像 43 | // WorksNum int `json:"worksNum,omitempty"` // 作品数目 44 | // MusicList []IMusicItem `json:"musicList,omitempty"` // 作者的音乐列表 45 | // AlbumList []IAlbumItem `json:"albumList,omitempty"` // 作者的专辑列表 46 | // Other IExtra `json:"extra,omitempty"` // 其他 47 | // } 48 | // ) 49 | 50 | // 结构表 51 | // type ( 52 | // // 重复 53 | // XPublicKeys struct { 54 | // ID string `json:"id" gorm:"primaryKey"` // 唯一ID 55 | // Exp int64 `json:"exp" gorm:"column:exp"` // 过期时间 56 | // } 57 | // // 音乐 58 | // XMusicItem struct { 59 | // ID string `json:"id" gorm:"primaryKey"` // 唯一ID 60 | // Name string `json:"name" gorm:"column:name"` // 歌曲名称 61 | // } 62 | // // 作者 63 | // // XArtistItem struct { 64 | // // ID string `json:"id" gorm:"primaryKey"` 65 | // // Name string `json:"name" gorm:"column:name"` 66 | // // } 67 | // // 歌词 68 | // XLyricItem struct { 69 | // ID string `json:"id" gorm:"primaryKey"` 70 | // Lyric string `json:"lyric" gorm:"column:lyric"` // 歌曲歌词 71 | // TLyric string `json:"tlyric" gorm:"column:tlyric"` // 翻译歌词,没有可为 null 72 | // RLyric string `json:"rlyric" gorm:"column:rlyric"` // 罗马音歌词,没有可为 null 73 | // LxLyric string `json:"lxlyric" gorm:"column:lxlyric"` // lx 逐字歌词,没有可为 null 74 | // // 歌词格式为 [分钟:秒.毫秒]<开始时间(基于该句),持续时间>歌词文字 75 | // // 例如: [00:00.000]<0,36>测<36,36>试<50,60>歌<80,75>词 76 | // } 77 | // // 视频 78 | // // XMovieItem struct { 79 | // // ID string `json:"id" gorm:"primaryKey"` 80 | // // Name string `json:"name" gorm:"column:name"` 81 | // // } 82 | // // 链接 83 | // XLinkItem struct { 84 | // ID string `json:"id" gorm:"primaryKey"` 85 | // } 86 | // ) 87 | 88 | // const ( 89 | // T_artist = `artist` 90 | // T_detail = `detail` 91 | // T_lyric = `lyric` 92 | // T_music = `music` 93 | // ) 94 | 95 | // 分源表 96 | // type ( 97 | // // Music 98 | // WyMusic MusicItem 99 | // MgMusic MusicItem 100 | // KwMusic MusicItem 101 | // KgMusic MusicItem 102 | // TxMusic MusicItem 103 | // LxMusic MusicItem 104 | // // Artist 105 | // WyArtist ArtistItem 106 | // MgArtist ArtistItem 107 | // KwArtist ArtistItem 108 | // KgArtist ArtistItem 109 | // TxArtist ArtistItem 110 | // LxArtist ArtistItem 111 | // // Lyric 112 | // WyLyric LyricItem 113 | // MgLyric LyricItem 114 | // KwLyric LyricItem 115 | // KgLyric LyricItem 116 | // TxLyric LyricItem 117 | // LxLyric LyricItem 118 | // ) 119 | -------------------------------------------------------------------------------- /src/middleware/auth/auth.go: -------------------------------------------------------------------------------- 1 | // 全局验证 2 | package auth 3 | 4 | import ( 5 | "encoding/gob" 6 | "lx-source/src/env" 7 | "lx-source/src/middleware/resp" 8 | "sync/atomic" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type ( 15 | RateLimit struct { 16 | Tim int64 // 创建时间 (注:原子操作64位数据需放在结构体第一位或保证8字节对齐,否则不兼容32位平台 https://pkg.go.dev/sync/atomic#pkg-note-BUG) 17 | Num uint32 // 请求次数 18 | } 19 | ) 20 | 21 | func init() { 22 | gob.Register(RateLimit{}) 23 | } 24 | 25 | func InitHandler(h gin.HandlerFunc) (out []gin.HandlerFunc) { 26 | loger := env.Loger.NewGroup(`AuthHandler`) 27 | // RateLimit 速率限制 28 | /* 29 | 逻辑: 30 | 记录访问者ip,到内存(缓存)中查找"块",没有则新建, 31 | 检测"块"是否过期,否则新建, 32 | // 判断ip是否在白名单内,是则直接放行 33 | 判断请求数+1是否大于限制,True: 429 请求过快,请稍后重试 34 | // 判断是否超出容忍限度,是则封禁ip (暂未实现) 35 | // 超过容忍限度每次请求增加一个Block的时间 36 | 继续执行后续Handler 37 | */ 38 | if env.Config.Auth.RateLimit_Enable { 39 | loger.Debug(`RateLimit Enabled`) 40 | loger.Info(`已启用速率限制,当前配置 %v/%v`, env.Config.Auth.RateLimit_Single, env.Config.Auth.RateLimit_Block) 41 | newRateLimit := func() *RateLimit { return &RateLimit{Tim: time.Now().Unix(), Num: 1} } 42 | block_int64 := int64(env.Config.Auth.RateLimit_Block) 43 | block_mem := int(env.Config.Auth.RateLimit_Block * env.Config.Auth.RateLimit_BanNum) 44 | bannum := env.Config.Auth.RateLimit_Single + env.Config.Auth.RateLimit_BanNum 45 | bantim := int64(env.Config.Auth.RateLimit_BanTim) 46 | var getIp func(c *gin.Context) string 47 | if env.Config.Main.NgProxy { 48 | loger.Info(`已开启反向代理兼容模式`) 49 | getIp = func(c *gin.Context) string { return c.ClientIP() } 50 | } else { 51 | getIp = func(c *gin.Context) string { return c.RemoteIP() } 52 | } 53 | out = append(out, func(c *gin.Context) { 54 | resp.Wrap(c, func() *resp.Resp { 55 | rip := getIp(c) 56 | if rip == `` { 57 | rip = `0.0.0.0` 58 | } 59 | cip, ok := env.Cache.Get(rip) 60 | loger.Debug(`GetMemRip: %v`, rip) 61 | if ok { 62 | if oip, ok := cip.(*RateLimit); ok { 63 | loger.Debug(`GetMemOut: %+v`, oip) 64 | if oip.Tim+block_int64 > time.Now().Unix() { 65 | oi := atomic.AddUint32(&oip.Num, 1) 66 | if oi > env.Config.Auth.RateLimit_Single { 67 | if oi > bannum { 68 | atomic.AddInt64(&oip.Tim, bantim) 69 | } 70 | return &resp.Resp{Code: 5, Msg: `请求过快,请稍后重试`} 71 | } 72 | return nil 73 | } 74 | } 75 | } 76 | val := newRateLimit() 77 | if err := env.Cache.Set(rip, val, block_mem); err != nil { 78 | loger.Error(`写入内存: %s`, err) 79 | return &resp.Resp{Code: 4, Msg: `速率限制内部异常,请联系网站管理员`} 80 | } 81 | loger.Debug(`SetMemVal: %+v`, val) 82 | return nil 83 | }) 84 | }) 85 | } 86 | // ApiKey 请求头验证 87 | if env.Config.Auth.ApiKey_Enable { 88 | loger.Debug(`ApiKeyAuth Enabled`) 89 | out = append(out, func(c *gin.Context) { 90 | resp.Wrap(c, func() *resp.Resp { 91 | var auth string 92 | if key, ok := c.GetQuery(`key`); ok { 93 | auth = key 94 | } else { 95 | auth = c.Request.Header.Get(`X-LxM-Auth`) 96 | } 97 | if auth != env.Config.Auth.ApiKey_Value { 98 | loger.Debug(`验证失败: %q`, auth) 99 | return &resp.Resp{Code: 3, Msg: `验证Key失败, 请联系网站管理员`} 100 | } 101 | return nil 102 | }) 103 | }) 104 | } 105 | return append(out, h) 106 | } 107 | -------------------------------------------------------------------------------- /src/middleware/auth/ratelimit.go: -------------------------------------------------------------------------------- 1 | package auth 2 | -------------------------------------------------------------------------------- /src/middleware/dynlink/dynlink.go: -------------------------------------------------------------------------------- 1 | package dynlink 2 | 3 | import ( 4 | "lx-source/src/caches" 5 | "lx-source/src/caches/localcache" 6 | "lx-source/src/env" 7 | "lx-source/src/middleware/util" 8 | "net/http" 9 | 10 | "github.com/ZxwyWebSite/ztool" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type DynLink struct { 15 | Mode uint8 16 | Link string 17 | } 18 | 19 | // var ExLink func(string) string = func(s string) string { return s } 20 | 21 | // func localInit(c string) func(string) string { 22 | // return func(l string) string { 23 | // return ztool.Str_FastConcat(c, `/`, l) 24 | // } 25 | // } 26 | 27 | // Doc 动态链 28 | /* 29 | 0. 链接格式 30 | - (Mode: 链接模式 0:本地/1:远程, Link: 真实链接), id(uint32 4294967295) 31 | - yyyymmdd/unixsecond/hex(:s/:id/:q).format(flac24bit->fl24) 32 | 1. 传入参数 (得到音乐链接后生成随机链并写入缓存) 33 | + Data1 查询缓存 34 | - key: "lx/0000000001/320k" 35 | - val: "`{cache.Path}/file/`20231221/1703176257/6c782f303030303030303030312f3332306b.mp3" 36 | + Data2 直链缓存 37 | - key: "20231221/1703176257/6c782f303030303030303030312f3332306b.mp3" 38 | - val: "&DynLink{Mode: 0, Link: 'cache/lx/0000000001/320k'}" 39 | + 返回链接: "http://127.0.0.1/file/lx/0000000001/320k.mp3" 40 | 2. 查询缓存 41 | - key: "20231221/1703176257/6c782f303030303030303030312f3332306b.mp3" 42 | - val: "&DynLink{Mode: 0, Link: 'cache/lx/0000000001/320k'}" 43 | - va2: "&DynLink{Mode: 1, Link: 'http://127.0.0.1/file/lx/0000000001/320k.mp3'}" 44 | 3. 实际数据 (访问 /file/:t/:x/:f) 45 | + if Mode==0 本地数据直接发送 46 | - c.File(Link) 47 | + if Mode==1 远程数据302跳转 48 | - c.Redirect(Link) 49 | 50 | 0. 实现思路 51 | 52 | */ 53 | 54 | func LoadHandler(r *gin.Engine) { 55 | loger := env.Loger.NewGroup(`DynLink`) 56 | cache, cok := caches.UseCache.(*localcache.Cache) 57 | // env.Cache.Set(`date/second/fname.mp3`, DynLink{Mode: 0, Link: `wy/3203127/320k.mp3`}, 0) 58 | // env.Cache.Set(`date/second/lname.mp3`, DynLink{Mode: 1, Link: `https://r2eu.zxwy.link/gh/lx-source/static/error.mp3`}, 0) 59 | // 动态链已完成(beta)... 60 | if env.Config.Cache.LinkMode == `dynamic` || env.Config.Cache.LinkMode == `2` /*|| true*/ { 61 | loger.Debug(`UseDynamic`) 62 | r.GET(`/file/:t/:x/:f`, func(c *gin.Context) { 63 | parms := util.ParaMap(c) 64 | t, x, f := parms[`t`], parms[`x`], parms[`f`] 65 | if clink, ok := env.Cache.Get(ztool.Str_FastConcat(t, `/`, x, `/`, f)); ok { 66 | if dyn, ok := clink.(DynLink); ok { 67 | if dyn.Mode == 0 && cok { 68 | c.File(ztool.Str_FastConcat(cache.Path, `/`, dyn.Link)) 69 | return 70 | } 71 | c.Redirect(http.StatusFound, dyn.Link) 72 | return 73 | } 74 | } 75 | c.AbortWithStatus(http.StatusNotFound) 76 | }) 77 | } else if cok { 78 | loger.Debug(`UseStatic`) 79 | // ExLink = localInit(cache.Path) 80 | r.Static(`/file`, cache.Path) 81 | } 82 | } 83 | 84 | // func FileHandler() gin.HandlerFunc { 85 | // loger := env.Loger.NewGroup(`DynLink`) 86 | // // 为了兼容原静态链,必须设置3个参数 87 | // // file/:{time.unix}/:{md5(cquery)}/:{fname} 1703006183//77792f3434343730363834382f3332306b.mp3 88 | // // file/:date/:second/:fname 20231219/1703006183/77792f3434343730363834382f3332306b.mp3 89 | // env.Cache.Set(`20211008/hello/test.mp3`, DynLink{Link: `/www/wwwroot/lx-source/data/cache/wy/3203127/320k.mp3`}, 0) 90 | 91 | // if env.Config.Cache.LinkMode == `dynamic` || env.Config.Cache.LinkMode == `2` /*|| true*/ { 92 | // loger.Debug(`UseDynamic`) 93 | // return func(c *gin.Context) { 94 | // parms := util.ParaMap(c) 95 | // t, x, f := parms[`t`], parms[`x`], parms[`f`] 96 | // if clink, ok := env.Cache.Get(ztool.Str_FastConcat(t, `/`, x, `/`, f)); ok { 97 | // if dyn, ok := clink.(DynLink); ok { 98 | // if dyn.Mode == 0 { 99 | // c.File(ztool.Str_FastConcat(dyn.Link)) 100 | // return 101 | // } 102 | // c.Redirect(http.StatusFound, dyn.Link) 103 | // return 104 | // } 105 | // } 106 | // c.AbortWithStatus(http.StatusNotFound) 107 | // } 108 | // } 109 | // if cache, ok := caches.UseCache.(*localcache.Cache); ok { 110 | // loger.Debug(`UseStatic`) 111 | // return func(c *gin.Context) { 112 | // parms := util.ParaMap(c) 113 | // t, x, f := parms[`t`], parms[`x`], parms[`f`] 114 | // c.File(ztool.Str_FastConcat(cache.Path, `/`, t, `/`, x, `/`, f)) 115 | // } 116 | // } 117 | // return func(c *gin.Context) { 118 | // c.AbortWithStatus(http.StatusNotFound) 119 | // } 120 | // } 121 | -------------------------------------------------------------------------------- /src/middleware/resp/resp.go: -------------------------------------------------------------------------------- 1 | // 返回值处理 2 | package resp 3 | 4 | import ( 5 | "lx-source/src/env" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // 统一输出 12 | /* 13 | 返回码对应表 (参考Python版): 14 | 0: http.StatusOK, // [200] 成功 15 | 1: http.StatusForbidden, // [403] IP被封禁 16 | 2: http.StatusServiceUnavailable, // [503] 获取失败 17 | 3: http.StatusUnauthorized, // [401] 验证失败 18 | 4: http.StatusInternalServerError, // [500] 服务器内部错误 19 | 5: http.StatusTooManyRequests, // [429] 请求过于频繁 20 | 6: http.StatusBadRequest, // [400] 参数错误 21 | */ 22 | type Resp struct { 23 | Code int `json:"code"` // 状态码 为兼容内置源设置 暂无实际作用 (1.0.2后已兼容Python版定义) 24 | Msg string `json:"msg"` // 提示or报错信息 25 | Data any `json:"data"` // 音乐URL 26 | Ext any `json:"ext,omitempty"` // 其它信息 27 | } 28 | 29 | // 获取失败默认音频 30 | // var ErrMp3 = `https://r2eu.zxwy.link/gh/lx-source/static/error.mp3` 31 | 32 | // 返回请求 33 | /* 34 | 注:Code不为0时调用c.Abort()终止Handler 35 | */ 36 | func (o *Resp) Execute(c *gin.Context) { 37 | // StatusCode转换 (小分支switch快, 大分支map快) 38 | var status int 39 | switch o.Code { 40 | case 0: 41 | status = http.StatusOK 42 | case 1: 43 | status = http.StatusForbidden 44 | case 2: 45 | status = http.StatusServiceUnavailable 46 | if o.Data == nil || o.Data == `` { 47 | o.Data = env.Config.Main.ErrMp3 //ErrMp3 48 | } 49 | case 3: 50 | status = http.StatusUnauthorized 51 | case 4: 52 | status = http.StatusInternalServerError 53 | case 5: 54 | status = http.StatusTooManyRequests 55 | case 6: 56 | status = http.StatusBadRequest 57 | default: 58 | status = http.StatusOK 59 | } 60 | if o.Code != 0 { 61 | // if o.Code == 2 /*&& o.Data == ``*/ { 62 | // o.Data = ErrMp3 63 | // } 64 | c.Abort() 65 | } 66 | c.JSON(status, o) 67 | } 68 | 69 | // 包装请求并自动处理 70 | /* 71 | 注:返回nil以继续执行下一个Handler 72 | */ 73 | func Wrap(c *gin.Context, f func() *Resp) { 74 | if r := f(); r != nil { 75 | r.Execute(c) 76 | } 77 | } 78 | 79 | // func Wrap2(c *gin.Context, p []string, f func([]string) *Resp) { 80 | // if r := f(util.ParaArr(c)); r != nil { 81 | // r.Execute(c) 82 | // } 83 | // } 84 | -------------------------------------------------------------------------------- /src/middleware/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "lx-source/src/env" 5 | "net/http" 6 | 7 | "github.com/ZxwyWebSite/ztool" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // 将路由参数转为Map 12 | /* 13 | `:s/:id/:q` -> { 14 | `s`: `source`, 15 | `id`: `musicId`, 16 | `q`: `quality`, 17 | } 18 | */ 19 | func ParaMap(c *gin.Context) map[string]string { 20 | parmlen := len(c.Params) 21 | parms := make(map[string]string, parmlen) 22 | for i := 0; i < parmlen; i++ { 23 | parms[c.Params[i].Key] = c.Params[i].Value 24 | } 25 | return parms 26 | } 27 | 28 | // 将路由参数转为Array 29 | /* 30 | ParaArr(c, `id`, `s`, `xxx`) => [ 31 | `musicId`, 32 | `source`, 33 | ``, 34 | ] 35 | */ 36 | func ParaArr(c *gin.Context, s ...string) []string { 37 | parmlen := len(c.Params) 38 | parslen := len(s) 39 | out := make([]string, parslen) 40 | for im := 0; im < parmlen; im++ { 41 | obj := c.Params[im] 42 | for is := 0; is < parslen; is++ { 43 | if s[is] == obj.Key { 44 | out[is] = obj.Value 45 | } 46 | } 47 | } 48 | return out 49 | } 50 | 51 | var pathCache string 52 | 53 | func init() { 54 | env.Inits.Add(func() { 55 | if !env.Config.Main.NgProxy { 56 | pathCache = `/` 57 | } 58 | }) 59 | } 60 | 61 | // 动态获取相对路径 <路径> 62 | /* 63 | HOST = `192.168.10.22:1011` 64 | URI = `/path/to/lxs/link/wy/2049512697/flac` 65 | sub = `link/` 66 | -> http://192.168.10.22:1011/path/to/lxs/ 67 | */ 68 | func GetPath(c *http.Request, sub string) string { 69 | // 从缓存读取相对路径 `/path/to/lxs/` or `/` 70 | if pathCache == `` { 71 | pathCache = ztool.Str_Before(c.RequestURI, sub) 72 | } 73 | return ztool.Str_FastConcat(`http://`, c.Host, pathCache) 74 | } 75 | -------------------------------------------------------------------------------- /src/server/api_music.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "lx-source/src/caches" 5 | "lx-source/src/env" 6 | "lx-source/src/middleware/auth" 7 | "lx-source/src/middleware/resp" 8 | "lx-source/src/middleware/util" 9 | "lx-source/src/sources" 10 | "lx-source/src/sources/custom" 11 | "strings" 12 | "sync/atomic" 13 | 14 | "github.com/ZxwyWebSite/ztool" 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | // type ( 19 | // Context struct { 20 | // Parms map[string]string 21 | // } 22 | // Source interface { 23 | // Link(c *Context) (string, error) 24 | // Lyric(c *Context) 25 | // } 26 | // ) 27 | 28 | func loadMusic(api gin.IRouter) { 29 | // /{method}/{source}/{musicId}/{?quality} 30 | api.GET(`/:m/:s/:id/*q`, auth.InitHandler(musicHandler)...) 31 | } 32 | 33 | func musicHandler(c *gin.Context) { 34 | resp.Wrap(c, func() *resp.Resp { 35 | // 获取请求参数 (测试用Array会不会提升性能) 36 | arr := util.ParaArr(c, `s`, `m`, `id`, `q`) 37 | ps, pm, pid, pq := arr[0], arr[1], arr[2], strings.TrimPrefix(arr[3], `/`) 38 | out := &resp.Resp{Code: 0} // 默认Code:6 (参数错误) 39 | loger := env.Loger.NewGroup(`MusicHandler`) 40 | defer loger.Free() 41 | loger.Debug(`s:'%v', m:'%v', id:'%v', q:'%v'`, ps, pm, pid, pq) 42 | // 定位音乐源 43 | var source custom.Source 44 | var active bool // 是否激活(自定义账号) 45 | switch ps { 46 | case sources.S_wy: 47 | active = env.Config.Custom.Wy_Enable 48 | source = custom.WySource 49 | case sources.S_mg: 50 | active = env.Config.Custom.Mg_Enable 51 | source = custom.MgSource 52 | case sources.S_kw: 53 | active = env.Config.Custom.Kw_Enable 54 | source = custom.KwSource 55 | case sources.S_kg: 56 | active = env.Config.Custom.Kg_Enable 57 | source = custom.KgSource 58 | case sources.S_tx: 59 | active = env.Config.Custom.Tx_Enable 60 | source = custom.TxSource 61 | case sources.S_lx: 62 | source = custom.LxSource 63 | default: 64 | out.Code = 6 65 | out.Msg = ztool.Str_FastConcat(`无效源参数:'`, ps, `'`) 66 | return out 67 | } 68 | if source == nil { 69 | out.Code = 6 70 | out.Msg = sources.ErrDisable 71 | return out 72 | } 73 | if !source.Vef(&pid) { 74 | out.Code = 6 75 | out.Msg = sources.E_VefMusicId 76 | return out 77 | } 78 | // 查询内存缓存 79 | atomic.AddInt64(&accnum, 1) 80 | cquery := strings.Join([]string{pm, ps, pid, pq}, `/`) 81 | loger.Debug(`MemoGet: %v`, cquery) 82 | if cdata, ok := env.Cache.Get(cquery); ok { 83 | loger.Debug(`MemoHIT: %q`, cdata) 84 | if cdata == `` { 85 | out.Code = 2 86 | out.Msg = memRej 87 | } else { 88 | out.Msg = memHIT 89 | out.Data = cdata 90 | } 91 | return out 92 | } 93 | // 定位源方法 94 | switch pm { 95 | case `url`, `link`: 96 | // if !active && pq != sources.Q_128k { 97 | // out.Msg = `未激活源仅可试听128k音质` 98 | // return out 99 | // } 100 | // 查询文件缓存 101 | var cstat bool 102 | if caches.UseCache != nil { 103 | cstat = caches.UseCache.Stat() 104 | } 105 | uquery := caches.NewQuery(ps, pid, pq) 106 | uquery.Request = c.Request 107 | defer uquery.Free() 108 | if cstat { 109 | loger.Debug(`FileGet: %v`, uquery.Query()) 110 | if olink := caches.UseCache.Get(uquery); olink != `` { 111 | env.Cache.Set(cquery, olink, sources.C_lx) 112 | out.Msg = cacheHIT 113 | out.Data = olink 114 | return out 115 | } 116 | } 117 | // 解析歌曲外链 118 | atomic.AddInt64(&reqnum, 1) 119 | out.Data, out.Msg = source.Url(pid, pq) 120 | if out.Data != `` { 121 | // 缓存并获取直链 122 | atomic.AddInt64(&secnum, 1) 123 | if out.Msg == `` { 124 | if cstat && active { 125 | loger.Debug(`FileSet: %v`, out.Data) 126 | if link := caches.UseCache.Set(uquery, out.Data.(string)); link != `` { 127 | env.Cache.Set(cquery, link, sources.C_lx) 128 | out.Msg = cacheSet 129 | out.Data = link 130 | return out 131 | } 132 | out.Msg = cacheFAIL 133 | } else { 134 | out.Msg = cacheMISS 135 | } 136 | } else { 137 | loger.Warn(`发生错误: %s`, out.Msg) 138 | } 139 | // 无法获取直链 直接返回原链接 140 | env.Cache.Set(cquery, out.Data, source.Exp()-300) 141 | return out 142 | } 143 | case `lrc`, `lyric`: 144 | out.Data, out.Msg = source.Lrc(pid) 145 | case `pic`, `cover`: 146 | out.Data, out.Msg = source.Pic(pid) 147 | default: 148 | out.Code = 6 149 | out.Msg = ztool.Str_FastConcat(`无效源方法:'`, pm, `'`) 150 | return out 151 | } 152 | if out.Msg != `` { 153 | out.Code = 2 154 | env.Cache.Set(cquery, out.Data, 600) 155 | } 156 | return out 157 | }) 158 | } 159 | -------------------------------------------------------------------------------- /src/server/app_lxmusic.go: -------------------------------------------------------------------------------- 1 | //go:build extapp 2 | 3 | package server 4 | 5 | import ( 6 | "lx-source/src/middleware/resp" 7 | 8 | "github.com/ZxwyWebSite/ztool" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type queryObj struct { 13 | Name string `json:"name"` // 歌名 14 | Singer string `json:"singer"` // 歌手 15 | Source string `json:"source"` // 平台 16 | Songmid string `json:"songmid"` // 音乐ID 17 | Interval string `json:"interval"` // 时长 18 | AlbumName string `json:"albumName"` // 专辑 19 | Img string `json:"img"` // 封面 20 | // TypeURL struct { 21 | // } `json:"typeUrl"` // 未知 22 | AlbumID string `json:"albumId"` // 专辑ID 23 | // 支持音质 24 | Types []struct { 25 | Type string `json:"type"` // 音质 26 | Size string `json:"size"` // 大小 27 | Hash string `json:"hash"` // 哈希(kg only) 28 | } `json:"types"` 29 | // tx 30 | StrMediaMid string `json:"strMediaMid"` // 当前文件ID 31 | AlbumMid string `json:"albumMid"` // 专辑ID 32 | SongID int `json:"songId"` // 音乐ID 33 | // mg 34 | CopyrightID string `json:"copyrightId"` // 音乐ID 35 | LrcURL string `json:"lrcUrl"` // lrc歌词 36 | MrcURL string `json:"mrcUrl"` // mrc歌词 37 | TrcURL string `json:"trcUrl"` // trc歌词 38 | // kg 39 | Hash string `json:"hash"` // 文件哈希(kg only) 40 | } 41 | 42 | func loadLxMusic(lx *gin.RouterGroup) { 43 | // 获取链接 44 | lx.POST(`/link/:q`, func(c *gin.Context) { 45 | resp.Wrap(c, func() *resp.Resp { 46 | var obj queryObj 47 | if err := c.ShouldBindJSON(&obj); err != nil { 48 | return &resp.Resp{Code: 6, Msg: `解析错误: ` + err.Error()} 49 | } 50 | pams := map[string]string{ 51 | `s`: obj.Source, 52 | `id`: ztool.Str_Select(obj.Hash, obj.CopyrightID, obj.Songmid), 53 | } 54 | for k, v := range pams { 55 | c.Params = append(c.Params, gin.Param{Key: k, Value: v}) 56 | } 57 | return nil 58 | }) 59 | }, linkHandler) 60 | // lx.GET(`/info`) 61 | } 62 | -------------------------------------------------------------------------------- /src/server/app_musicfree.go: -------------------------------------------------------------------------------- 1 | //go:build extapp 2 | 3 | package server 4 | 5 | import ( 6 | "lx-source/src/middleware/util" 7 | "lx-source/src/sources" 8 | "net/http" 9 | 10 | "github.com/ZxwyWebSite/ztool" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | func loadMusicFree(mf *gin.RouterGroup) { 15 | // 插件订阅 16 | mf.GET(`/subscribe`, func(c *gin.Context) { 17 | slist := sources.S_al 18 | type plugins struct { 19 | Name string `json:"name"` 20 | Url string `json:"url"` 21 | Version string `json:"version"` 22 | } 23 | length := len(slist) 24 | plgs := make([]plugins, length) 25 | url := ztool.Str_FastConcat(util.GetPath(c.Request, `app/`), `public/musicfree/`) 26 | for i := 0; i < length; i++ { 27 | name := `lxs-` + slist[i] 28 | plgs[i] = plugins{ 29 | Name: name, 30 | Url: ztool.Str_FastConcat(url, name, `.js`), 31 | Version: `0.0.0`, 32 | } 33 | } 34 | c.JSON(http.StatusOK, gin.H{`plugins`: plgs}) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/server/loadpublic.go: -------------------------------------------------------------------------------- 1 | // 静态资源 2 | package server 3 | 4 | import ( 5 | "bytes" 6 | "embed" 7 | "fmt" 8 | "io" 9 | "io/fs" 10 | "lx-source/src/env" 11 | "net/http" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/ZxwyWebSite/ztool" 16 | "github.com/ZxwyWebSite/ztool/x/bytesconv" 17 | "github.com/gin-gonic/gin" 18 | "github.com/gin-gonic/gin/render" 19 | ) 20 | 21 | //go:embed public 22 | var publicEM embed.FS // 打包默认Public目录 src/server/public 23 | 24 | // 载入Public目录并设置路由 25 | func loadPublic(r *gin.Engine) { 26 | pf := env.Loger.NewGroup(`PublicFS`) 27 | dir := ztool.Str_FastConcat(env.RunPath, `/data/public`) 28 | publicFS, err := fs.Sub(publicEM, `public`) 29 | var httpFS http.FileSystem = http.FS(publicFS) 30 | if err != nil { 31 | pf.Fatal(`内置Public目录载入错误: %s, 请尝试重新编译`, err) 32 | } 33 | if !ztool.Fbj_IsExists(dir) { 34 | pf.Info(`不存在Public目录, 释放默认静态文件`) 35 | walk := func(relPath string, d fs.DirEntry, err error) error { 36 | if err != nil { 37 | return fmt.Errorf(`无法获取[%q]的信息: %s`, relPath, err) 38 | } 39 | if !d.IsDir() { 40 | out, err := ztool.Fbj_CreatFile(filepath.Join(dir, relPath)) 41 | if err != nil { 42 | return fmt.Errorf(`无法创建文件[%q]: %s`, relPath, err) 43 | } 44 | defer out.Close() 45 | pf.Debug(`导出 [%q]...`, relPath) 46 | obj, err := publicFS.Open(relPath) 47 | if err != nil { 48 | return fmt.Errorf(`无法打开文件[%q]: %s`, relPath, err) 49 | } 50 | if _, err := io.Copy(out, obj); err != nil { 51 | return fmt.Errorf(`无法写入文件[%q]: %s`, relPath, err) 52 | } 53 | } 54 | return nil 55 | } 56 | if err := fs.WalkDir(publicFS, `.`, walk); err != nil { 57 | pf.Error(`无法释放静态文件: %s`, err) 58 | // pf.Warn(`正在使用内置Public目录, 将无法自定义静态文件`) 59 | // httpFS = http.FS(publicFS) 60 | } else { 61 | pf.Info(`全部静态资源导出完成, 祝你使用愉快 ^_^`) 62 | } 63 | } 64 | pf.Free() 65 | // 使用本地public目录 66 | // httpFS = gin.Dir(dir, false) 67 | // r.GET(`/:file`, func(c *gin.Context) { 68 | // file := c.Param(`file`) 69 | // switch file { 70 | // case `favicon.ico`: 71 | // c.FileFromFS(`icon.ico`, httpFS) 72 | // // case `lx-custom-source.js`: 73 | // // c.FileFromFS(`lx-custom-source.js`, http.FS(publicFS)) 74 | // default: 75 | // c.FileFromFS(file, httpFS) 76 | // } 77 | // }) 78 | // 自动填写源脚本参数 79 | if env.Config.Script.Auto > 0 { 80 | file, _ := publicFS.Open(`lx-custom-source.js`) 81 | data, _ := io.ReadAll(file) 82 | file.Close() 83 | data = bytes.Replace(data, 84 | bytesconv.StringToBytes(`http://127.0.0.1:1011/`), 85 | bytesconv.StringToBytes(env.Config.Cache.Local_Bind), 1, 86 | ) 87 | if env.Config.Auth.ApiKey_Enable && env.Config.Script.Auto >= 2 { 88 | data = bytes.Replace(data, 89 | bytesconv.StringToBytes(`apipass = ''`), 90 | bytesconv.StringToBytes(ztool.Str_FastConcat( 91 | `apipass = '`, env.Config.Auth.ApiKey_Value, `'`, 92 | )), 1, 93 | ) 94 | } 95 | r.GET(`/lx-custom-source.js`, func(c *gin.Context) { 96 | var mime string 97 | if _, ok := c.GetQuery(`raw`); ok { 98 | mime = `application/octet-stream` 99 | } else { 100 | mime = `text/javascript; charset=utf-8` 101 | } 102 | c.Render(http.StatusOK, render.Data{ 103 | ContentType: mime, 104 | Data: data, 105 | }) 106 | }) 107 | } else { 108 | r.StaticFileFS(`/lx-custom-source.js`, `lx-custom-source.js`, httpFS) 109 | } 110 | // 新版源脚本 111 | { 112 | // 构建文件头 113 | var b strings.Builder 114 | b.Grow(75 + 115 | len(env.Config.Script.Name) + 116 | len(env.Config.Script.Descript) + 117 | len(env.Config.Script.Version) + 118 | len(env.Config.Script.Author) + 119 | len(env.Config.Script.Homepage), 120 | ) 121 | b.WriteString("/*!\n * @name ") 122 | b.WriteString(env.Config.Script.Name) 123 | b.WriteString("\n * @description ") 124 | b.WriteString(env.Config.Script.Descript) 125 | b.WriteString("\n * @version v") 126 | b.WriteString(env.Config.Script.Version) 127 | b.WriteString("\n * @author ") 128 | b.WriteString(env.Config.Script.Author) 129 | b.WriteString("\n * @homepage ") 130 | b.WriteString(env.Config.Script.Homepage) 131 | b.WriteString("\n */\n") 132 | // 构建文件体 133 | file, _ := publicFS.Open(`lx-source-script.js`) 134 | data, _ := io.ReadAll(file) 135 | file.Close() 136 | r.GET(`/lx-source-script.js`, func(c *gin.Context) { 137 | var mime string 138 | if _, ok := c.GetQuery(`raw`); ok { 139 | mime = `application/octet-stream` 140 | } else { 141 | mime = `text/javascript; charset=utf-8` 142 | } 143 | // 构建文件尾 144 | var d strings.Builder 145 | d.WriteString(`globalThis.ls={api:{addr:'`) 146 | d.WriteString(env.Config.Cache.Local_Bind) 147 | d.WriteString(`',pass:'`) 148 | if env.Config.Auth.ApiKey_Enable { 149 | if env.Config.Script.Auto >= 2 { 150 | d.WriteString(env.Config.Auth.ApiKey_Value) 151 | } else { 152 | if key, ok := c.GetQuery(`key`); ok { 153 | d.WriteString(key) 154 | } 155 | } 156 | } 157 | d.WriteString(`'}};`) 158 | d.WriteByte('\n') 159 | // Render 160 | c.Status(http.StatusOK) 161 | c.Writer.Header()[`Content-Type`] = []string{mime} 162 | c.Writer.WriteString(b.String()) 163 | c.Writer.WriteString(d.String()) 164 | c.Writer.Write(data) 165 | }) 166 | } 167 | r.StaticFileFS(`/favicon.ico`, `lx-icon.ico`, httpFS) 168 | r.StaticFileFS(`/status`, `status.html`, httpFS) 169 | r.StaticFS(`/public`, httpFS) 170 | } 171 | -------------------------------------------------------------------------------- /src/server/loadquality.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "lx-source/src/env" 5 | "lx-source/src/sources" 6 | ) 7 | 8 | var ( 9 | // 默认音质 10 | defQuality = []string{`128k`, `320k`, `flac`, `flac24bit`} 11 | // 试听音质 12 | tstQuality = []string{`128k`} 13 | // 标准音质 14 | stdQuality = []string{`128k`, `320k`, `flac`} 15 | ) 16 | 17 | // 自动生成支持的音质表 18 | func loadQMap() [][]string { 19 | m := make([][]string, 6) 20 | // 0.wy 21 | if env.Config.Source.Enable_Wy { 22 | if env.Config.Custom.Wy_Enable { 23 | m[sources.I_wy] = defQuality 24 | } else { 25 | m[sources.I_wy] = tstQuality 26 | } 27 | } 28 | // 1.mg 29 | if env.Config.Source.Enable_Mg { 30 | if env.Config.Custom.Mg_Enable { 31 | m[sources.I_mg] = defQuality 32 | } else { 33 | m[sources.I_mg] = tstQuality 34 | } 35 | } 36 | // 2.kw 37 | if env.Config.Source.Enable_Kw { 38 | if env.Config.Custom.Kw_Enable { 39 | m[sources.I_kw] = stdQuality 40 | } 41 | } 42 | // 3.kg 43 | if env.Config.Source.Enable_Kg { 44 | if env.Config.Custom.Kg_Enable { 45 | m[sources.I_kg] = defQuality 46 | } else { 47 | m[sources.I_kg] = tstQuality 48 | } 49 | } 50 | // 4.tx 51 | if env.Config.Source.Enable_Tx { 52 | if env.Config.Custom.Tx_Enable { 53 | m[sources.I_tx] = stdQuality 54 | } else { 55 | m[sources.I_tx] = tstQuality 56 | } 57 | } 58 | // 5.lx 59 | if env.Config.Source.Enable_Lx { 60 | m[sources.I_lx] = defQuality 61 | } 62 | return m 63 | } 64 | -------------------------------------------------------------------------------- /src/server/public/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZxwyWebSite/lx-source/9155f341051d2570e06bf9acaa38476705a3bc4d/src/server/public/icon.ico -------------------------------------------------------------------------------- /src/server/public/lx-custom-source.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Lx-Custom-Source 3 | * @description Client 4 | * version 1.0.1 5 | * @author Zxwy 6 | * homepage null 7 | */ 8 | 9 | // 脚本配置 10 | const version = '1.0.3' // 脚本版本 11 | const apiaddr = 'http://127.0.0.1:1011/' // 服务端地址,末尾加斜杠 12 | const apipass = '' // 验证密钥,填在单引号内 13 | const devmode = true // 调试模式 14 | // const timeout = 60 * 1000 // 请求超时(ms) 15 | 16 | // 常量 & 默认值 17 | const { EVENT_NAMES, request, on, send } = globalThis.lx 18 | const defs = { type: 'music', actions: ['musicUrl'] } 19 | const defheaders = { 20 | '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', 21 | 'Accept': 'application/json, text/plain, */*', 22 | 'X-LxM-Auth': apipass, 23 | } 24 | const conf = { 25 | api: { 26 | addr: apiaddr, // 服务端地址,末尾加斜杠 27 | pass: apipass, // 验证密钥,由服务端自动生成 '${apipass}' 28 | glbv: 'v1' // 大版本号 29 | }, 30 | info: { 31 | version: version, // 脚本版本 32 | devmode: devmode, // 调试模式 33 | }, 34 | } 35 | 36 | const httpRequest = (url, options) => new Promise((resolve, reject) => { 37 | options.headers = { ...defheaders, ...options.headers } // 添加默认请求头 38 | // options.timeout ? options.timeout : timeout // 添加默认请求超时 39 | request(url, options, (err, resp) => { 40 | if (err) return reject(err) 41 | resolve(resp.body) 42 | }) 43 | }) 44 | 45 | const musicUrl = async (source, info, quality) => { 46 | const start = new Date().getTime(); 47 | const id = info.hash ?? info.copyrightId ?? info.songmid // 音乐id kg源为hash, mg源为copyrightId 48 | //const ext = source == 'kg' ? info.albumId : '' //source == 'tx' ? info.strMediaMid 49 | const query = `${source}/${id}/${quality}` //${(ext != '' && ext != void 0) ? '-' + ext : ''} 50 | console.log('创建任务: %s, 音乐信息: %O', query, info) 51 | const body = await httpRequest(`${apiaddr}link/${query}`, { method: 'get' }); 52 | console.log('返回数据: %O', body, `, 耗时 ${new Date().getTime() - start} ms`) 53 | return body.data != '' ? body.data : Promise.reject(body.msg) // 没有获取到链接则将msg作为错误抛出 54 | } 55 | 56 | // 注册应用API请求事件 57 | // source 音乐源,可能的值取决于初始化时传入的sources对象的源key值 58 | // info 请求附加信息,内容根据action变化 59 | // action 请求操作类型,目前只有musicUrl,即获取音乐URL链接, 60 | // 当action为musicUrl时info的结构:{type, musicInfo}, 61 | // info.type:音乐质量,可能的值有128k / 320k / flac / flac24bit(取决于初始化时对应源传入的qualitys值中的一个), 62 | // info.musicInfo:音乐信息对象,里面有音乐ID、名字等信息 63 | on(EVENT_NAMES.request, ({ source, action, info }) => { 64 | // 回调必须返回 Promise 对象 65 | switch (action) { 66 | // action 为 musicUrl 时需要在 Promise 返回歌曲 url 67 | case 'musicUrl': 68 | return musicUrl(source, info.musicInfo, info.type).catch(err => { 69 | console.log('发生错误: %o', err) 70 | return Promise.reject(err) 71 | }) 72 | } 73 | }) 74 | 75 | // 脚本初始化 (目前只有检查更新) 76 | const init = () => { 77 | 'use strict'; 78 | console.log('初始化脚本, 版本: %s, 服务端地址: %s', version, apiaddr) 79 | var stat = false; var msg = ''; var updUrl = ''; var sourcess = {} 80 | httpRequest(apiaddr, { method: 'get', timeout: 1000 * 10 }) 81 | .then((body) => { 82 | if (!body) { msg = '初始化失败:' + '无返回数据'; return } 83 | console.log('获取服务端数据成功: %o, 版本: %s', body, body.version) 84 | // 检查Api大版本 85 | if (body.msg != `Hello~::^-^::~${conf.api.glbv}~`) { 86 | msg = 'Api大版本不匹配,请检查服务端与脚本是否兼容!'; return 87 | } 88 | // 检查脚本更新 89 | const script = body.script // 定位到Script部分 90 | const lv = version.split('.'); const rv = script.ver.split('.') // 分别对主次小版本检查更新 91 | for (var i = 0; i < 3; i++) { 92 | if (lv[i] < rv[i]) { 93 | console.log('发现更新, 版本: %s, 信息: %s, 地址: %s, 强制推送: %o', script.ver, script.log, script.url, script.force) 94 | msg = `${script.force ? '强制' : '发现'}更新:` + script.log; updUrl = script.url; if (script.force) return; break 95 | } 96 | } 97 | // 激活可用源 98 | const source = body.source // 定位到Source部分 99 | Object.keys(source).forEach(v => { 100 | if (source[v] != null /*== true*/) { 101 | sourcess[v] = { 102 | name: v, 103 | ...defs, 104 | qualitys: source[v], // 支持返回音质时启用 使用后端音质表 105 | } 106 | } 107 | }) 108 | // 完成初始化 109 | stat = true 110 | }) 111 | .catch((err) => { msg = '初始化失败: ' + err ?? '连接服务端超时'; console.log(msg) }) 112 | .finally(() => { 113 | // 脚本初始化完成后需要发送inited事件告知应用 114 | send(EVENT_NAMES.inited, { 115 | status: stat, // 初始化成功 or 失败 (初始化失败不打开控制台, 使用更新提示接口返回信息) 116 | openDevTools: stat ? devmode : false, // 是否打开开发者工具,方便用于调试脚本 'devmode' or 'stat ? devmode : false' 117 | sources: sourcess, // 使用服务端源列表 118 | }) 119 | // 发送更新提示 120 | if (msg) send(EVENT_NAMES.updateAlert, { log: '提示:' + msg, updateUrl: updUrl ? apiaddr + updUrl : '' }) 121 | }) 122 | } 123 | 124 | console.log('\n __ __ __ ______ ______ __ __ ____ ______ ______\n / / / / / / / ____/ / __ / / / / / / __ \\ / ____/ / ____/\n / / / /_/ / __ / /___ / / / / / / / / / /_/ / / / / /___\n / / \\_\\ \\ /_/ /___ / / / / / / / / / / ___/ / / / ____/\n / /___ / / / / ____/ / / /_/ / / /_/ / / / \\ / /___ / /___\n/_____/ /_/ /_/ /_____/ /_____/ /_____/ /_/ \\_\\ /_____/ /_____/\n=======================================================================\n') 125 | init() // 启动!!! -------------------------------------------------------------------------------- /src/server/public/lx-icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZxwyWebSite/lx-source/9155f341051d2570e06bf9acaa38476705a3bc4d/src/server/public/lx-icon.ico -------------------------------------------------------------------------------- /src/server/public/lx-source-script.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see lx-source-script.js.LICENSE.txt */ 2 | (()=>{"use strict";var t,r,e,o,n={75:(t,r,e)=>{e.d(r,{EK:()=>n,R3:()=>h,an:()=>u,bD:()=>c,bH:()=>l,bl:()=>_,cY:()=>p,lm:()=>m,n9:()=>i,yN:()=>s,zf:()=>d});let o=!1;function n(){o=!0}const s=1,a={Bold:22,Faint:22,Italic:23,Underline:24,BlinkSlow:25,BlinkRapid:25,ReverseVideo:27,Concealed:28,CrossedOut:29},i=31,l=32,p=33,u=34,c=35,_=36,h=37,d=41;function m(...t){return new g(...t)}function f(t){return`[${t}m`}class g{params=[];noColor=null;constructor(...t){o&&(this.noColor=!0),this.Add(...t)}Add(...t){return this.params.push(...t),this}sequence(){return this.params.map((t=>String(t))).join(";")}Wrap(t){return this.isNoColorSet()?t:this.format()+t+this.unformat()}format(){return f(this.sequence())}unformat(){return f(this.params.map((t=>a[t]||"0")).join(";"))}DisableColor(){this.noColor=!0}EnableColor(){this.noColor=!1}isNoColorSet(){return null!==this.noColor?this.noColor:o}}},44:(t,r,e)=>{e.a(t,(async(t,r)=>{try{var o=e(510),n=e(537),s=e(75),a=e(330);const t=(t,r)=>new Promise(((e,n)=>{r.headers={"User-Agent":`Mozilla/5.0 (compatible; lx-music-${o._K?o._K:"request"}/${o.rE})`,Accept:"application/json, text/plain, */*","X-LxM-Auth":o.xf.api.pass,...r.headers},r.timeout||(r.timeout=15e3),(0,o.Em)(t,r,((t,r)=>{if(t)return n(t);e(r.body)}))})),i=async(r,e,s,a)=>{const i=(new Date).getTime(),l=`${e}/${s.hash??s.copyrightId??s.songmid}/${a}`;n.Ay.Info("创建任务: %s, 音乐信息: %O",l,s);const p=await t(`${o.xf.api.addr}${r}/${l}`,{method:"GET"});return n.Ay.Info("返回数据: %O",p,`, 耗时 ${(new Date).getTime()-i} ms`),0!==p.code&&n.Ay.Warn(`${p.code}:`,p.msg),""!==p.data?p.data:Promise.reject(p.msg)},l={musicUrl:"url",lyric:"lrc",pic:"pic"};(0,o.on)(o.MN.request,(({source:t,action:r,info:e})=>i(l[r],t,e.musicInfo,e.type))),"desktop"===o._K?console.log(` _ ____ \n | | __ __ / ___| ___ _ _ _ __ ___ ___ \n | | \\ \\/ / _ \\___ \\ / _ \\| | | | '__/ __/ _ \\\n | |___ > < (_) ___) | (_) | |_| | | | (_| __/\n |_____/_/\\_\\ |____/ \\___/ \\__,_|_| \\___\\___| Beta\n${"".padEnd(56,"=")}`):(0,s.EK)(),n.Ay.SetLevel(n.J7).SetName("").Info("欢迎使用 Lx-Script 洛雪音乐自定义源脚本 ^_^");const p={};await(async()=>{const r=n.Ay.NewGroup("Init");r.Info("初始化脚本, 版本: %s, 服务端地址: %s",a.rE,o.xf.api.addr);const e=await t(o.xf.api.addr,{method:"GET",timeout:1e4});if(!e)throw"无返回数据";r.Info("获取服务端数据成功: %o, 版本: %s",e,e.version);const s=new Date-new Date(1e3*e.summary.StartAt);if(r.Info("已持续运行",String(Math.floor(s/864e5)).padStart(2,"0"),"天",String(Math.floor(s%864e5/36e5)).padStart(2,"0"),"时",String(Math.floor(s%36e5/6e4)).padStart(2,"0"),"分",String(Math.floor(s%6e4/1e3)).padStart(2,"0"),"秒,","调用:",e.summary.Accessn,"/解析:",e.summary.Request,"/成功:",e.summary.Success),e.msg!==`Hello~::^-^::~${o.xf.api.glbv}~`)throw"Api大版本不匹配,请检查服务端与脚本是否兼容!";const i=a.rE.split("."),l=e.script.ver.split(".");for(var u=0;u<3;u++){if(i[u]l[u])break}if(e.auth.apikey&&""===o.xf.api.pass)throw"未填写请求密钥";for(const[t,r]of Object.entries(e.source))null!==r&&(p[t]={name:t,type:"music",actions:["musicUrl","pic","lyric"],qualitys:r})})(),(0,o.tN)(o.MN.inited,{status:!0,openDevTools:!0,sources:p}),r()}catch(t){r(t)}}),1)},537:(t,r,e)=>{e.d(r,{Ay:()=>i,J7:()=>n});var o=e(75);const n=3;let s=2;class a{level=null;name="";group=[];deferr=null;constructor(t,r){this.name=t,this.group=[r]}SetName(t){return this.name=t,this}SetLevel(t){return this.level=t,this}getLevel(){return null!==this.level?this.level:s}Clone(){const t=new a(this.name,this.group);return t.level=this.level,t.deferr=this.deferr,t}NewGroup(t){return this.Clone().SetGroup(t)}AddGroup(t){return this.group.push(t),this}AppGroup(t){return this.Clone().AddGroup(t)}SetGroup(t){return this.group=[t],this}println(t,...r){let e=[];var n;this.name&&e.push((0,o.lm)(o.bH,o.yN).Wrap(`[${this.name}]`)),e.push(t,(0,o.lm)(o.an,o.yN).Wrap(`${(n=new Date).getFullYear()}-${(n.getMonth()+1).toString().padStart(2,"0")}-${n.getDate().toString().padStart(2,"0")} ${n.getHours().toString().padStart(2,"0")}:${n.getMinutes().toString().padStart(2,"0")}:${n.getSeconds().toString().padStart(2,"0")}`),(0,o.lm)(o.bD,o.yN).Wrap(this.group.map((t=>`[${t}]`)).join(" "))),r[0]&&e.push(r[0]);let s=[e.join(" ")];const a=r.slice(1);a&&s.push(...a),console.log(...s)}Info(...t){return this.getLevel()>=1&&this.println((0,o.lm)(o.bl,o.yN).Wrap("[Info]")+" ",...t),this}Warn(...t){return this.getLevel()>=2&&this.println((0,o.lm)(o.cY,o.yN).Wrap("[Warn]")+" ",...t),this}Error(...t){return this.getLevel()>=0&&this.println((0,o.lm)(o.n9,o.yN).Wrap("[Error]"),...t),this}Fatal(...t){throw this.getLevel()>=0&&this.println((0,o.lm)(o.zf,o.yN).Wrap("[Fatal]"),...t),null!=this.deferr&&this.deferr(),new Error(t.toString())}Debug(...t){return this.getLevel()>=n&&this.println((0,o.lm)(o.R3,o.yN).Wrap("[Debug]"),...t),this}Panic(...t){throw this.getLevel()>=0&&this.println((0,o.lm)(o.zf,o.yN).Wrap("[Panic]"),...t),new Error(t.toString())}}const i=new a("Logs","Main")},510:(t,r,e)=>{e.d(r,{Em:()=>a,MN:()=>o,_K:()=>l,on:()=>n,rE:()=>i,tN:()=>s,xf:()=>u});const{EVENT_NAMES:o,on:n,send:s,request:a,version:i,env:l}=globalThis.lx,{api:p}=globalThis.ls,u={api:{addr:p.addr,pass:p.pass,glbv:"v1"},etc:{devmode:!0}}},330:t=>{t.exports={rE:"1.1.0"}}},s={};function a(t){var r=s[t];if(void 0!==r)return r.exports;var e=s[t]={exports:{}};return n[t](e,e.exports,a),e.exports}t="function"==typeof Symbol?Symbol("webpack queues"):"__webpack_queues__",r="function"==typeof Symbol?Symbol("webpack exports"):"__webpack_exports__",e="function"==typeof Symbol?Symbol("webpack error"):"__webpack_error__",o=t=>{t&&t.d<1&&(t.d=1,t.forEach((t=>t.r--)),t.forEach((t=>t.r--?t.r++:t())))},a.a=(n,s,a)=>{var i;a&&((i=[]).d=-1);var l,p,u,c=new Set,_=n.exports,h=new Promise(((t,r)=>{u=r,p=t}));h[r]=_,h[t]=t=>(i&&t(i),c.forEach(t),h.catch((t=>{}))),n.exports=h,s((n=>{var s;l=(n=>n.map((n=>{if(null!==n&&"object"==typeof n){if(n[t])return n;if(n.then){var s=[];s.d=0,n.then((t=>{a[r]=t,o(s)}),(t=>{a[e]=t,o(s)}));var a={};return a[t]=t=>t(s),a}}var i={};return i[t]=t=>{},i[r]=n,i})))(n);var a=()=>l.map((t=>{if(t[e])throw t[e];return t[r]})),p=new Promise((r=>{(s=()=>r(a)).r=0;var e=t=>t!==i&&!c.has(t)&&(c.add(t),t&&!t.d&&(s.r++,t.push(s)));l.map((r=>r[t](e)))}));return s.r?p:a()}),(t=>(t?u(h[e]=t):p(_),o(i)))),i&&i.d<0&&(i.d=0)},a.d=(t,r)=>{for(var e in r)a.o(r,e)&&!a.o(t,e)&&Object.defineProperty(t,e,{enumerable:!0,get:r[e]})},a.o=(t,r)=>Object.prototype.hasOwnProperty.call(t,r);a(44)})(); -------------------------------------------------------------------------------- /src/server/public/status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | LX Source API 9 | 10 | 11 | 12 |

LX Source API

13 | 当你看到这个页面时,服务已经成功跑起来了~ 14 |

示例:

15 | 31 | 39 |

状态:

40 | 62 | 87 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/server/public/test.txt: -------------------------------------------------------------------------------- 1 | :) -------------------------------------------------------------------------------- /src/server/router.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "lx-source/src/env" 5 | "lx-source/src/middleware/dynlink" 6 | "lx-source/src/sources" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/gin-contrib/cors" 11 | "github.com/gin-contrib/gzip" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | var ( 16 | accnum int64 17 | reqnum int64 18 | secnum int64 19 | ) 20 | 21 | // 载入路由 22 | func InitRouter() *gin.Engine { 23 | r := gin.Default() 24 | qmap := loadQMap() 25 | // Gzip压缩 26 | if env.Config.Main.Gzip { 27 | r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{"/file/"}))) 28 | } 29 | // Cors跨域 30 | if env.Config.Main.Cors { 31 | r.Use(cors.Default()) 32 | } 33 | startime := time.Now().Unix() 34 | // 源信息 35 | r.GET(`/`, func(c *gin.Context) { 36 | c.JSON(http.StatusOK, gin.H{ 37 | `version`: env.Version, // 服务端程序版本 38 | `name`: `lx-music-source`, // 名称 39 | `msg`: `Hello~::^-^::~v1~`, // Api大版本 40 | // `developer`: []string{`Zxwy`}, // 开发者列表, 可在保留原作者的基础上添加你自己的名字? 41 | // 仓库地址 42 | // `github`: `https://github.com/ZxwyWebSite/lx-source`, 43 | // 可用平台 44 | `source`: gin.H{ 45 | sources.S_wy: qmap[sources.I_wy], 46 | sources.S_mg: qmap[sources.I_mg], 47 | sources.S_kw: qmap[sources.I_kw], 48 | sources.S_kg: qmap[sources.I_kg], 49 | sources.S_tx: qmap[sources.I_tx], 50 | sources.S_lx: qmap[sources.I_lx], 51 | }, 52 | // 自定义源脚本更新 53 | `script`: env.Config.Script.Update, //env.Config.Script, 54 | // 数据统计 55 | `summary`: gin.H{ 56 | `StartAt`: startime, // 启动时间 57 | `Accessn`: accnum, // 访问次数 58 | `Request`: reqnum, // 解析次数 59 | `Success`: secnum, // 成功次数 60 | }, 61 | // 验证方式 62 | `auth`: gin.H{ 63 | `apikey`: env.Config.Auth.ApiKey_Enable, 64 | }, 65 | }) 66 | }) 67 | // 静态文件 68 | loadPublic(r) 69 | // r.StaticFile(`/favicon.ico`, `public/icon.ico`) 70 | // r.StaticFile(`/lx-custom-source.js`, `public/lx-custom-source.js`) 71 | // 解析接口 72 | loadMusic(r) 73 | // r.GET(`/link/:s/:id/:q`, auth.InitHandler(linkHandler)...) 74 | dynlink.LoadHandler(r) 75 | // 动态链? 76 | // r.GET(`/file/:t/:x/:f`, dynlink.FileHandler()) 77 | // if cache, ok := caches.UseCache.(*localcache.Cache); ok { 78 | // r.Static(`/file`, cache.Path) 79 | // } 80 | // if env.Config.Cache.Mode == `local` { 81 | // r.Static(`/file`, env.Config.Cache.Local_Path) 82 | // } 83 | // 功能接口 84 | // api := r.Group(`/api`) 85 | // { 86 | // api.GET(`/:s/:m/:q`) // {source}/{method}/{query} 87 | // } 88 | // 软件接口 89 | // app := r.Group(`/app`) 90 | // { 91 | // loadLxMusic(app.Group(`/lxmusic`)) 92 | // loadMusicFree(app.Group(`/musicfree`)) 93 | // } 94 | // 数据接口 95 | // r.GET(`/file/:t/:hq/:n`, func(c *gin.Context) { 96 | // c.String(http.StatusOK, time.Now().Format(`20060102150405`)) 97 | // }) 98 | // 暂不对文件接口进行验证 脚本返回链接无法附加请求头 只可在Get添加Query 99 | // g := r.Group(``) 100 | // { 101 | // g.Use(authHandler) 102 | // g.GET(`/link/:s/:id/:q`, linkHandler) 103 | // g.Static(`/file`, LocalCachePath) 104 | // } 105 | return r 106 | } 107 | 108 | // 数据返回格式 109 | const ( 110 | cacheHIT = `Cache HIT` // 缓存已命中 111 | cacheMISS = `Cache MISS` // 缓存未命中 112 | cacheSet = `Cache Seted` // 缓存已设置 113 | cacheFAIL = `Cache FAIL` // 缓存未成功 114 | 115 | memHIT = `Memory HIT` // 内存已命中 116 | memRej = `Memory Reject` // 内存已拒绝 117 | ) 118 | 119 | // 外链解析 120 | // func linkHandler(c *gin.Context) { 121 | // resp.Wrap(c, func() *resp.Resp { 122 | // // 获取传入参数 检查合法性 123 | // arr := util.ParaArr(c, `s`, `id`, `q`) 124 | // s, id, q := arr[0], arr[1], arr[2] 125 | // // parms := util.ParaMap(c) 126 | // // getParam := func(p string) string { return strings.TrimSuffix(strings.TrimPrefix(c.Param(p), `/`), `/`) } //strings.Trim(c.Param(p), `/`) 127 | // // s := parms[`s`] //c.Param(`s`) //getParam(`s`) // source 平台 wy, mg, kw 128 | // // id := parms[`id`] //c.Param(`id`) //getParam(`id`) // sid 音乐ID wy: songmid, mg: copyrightId 129 | // // q := parms[`q`] //c.Param(`q`) //getParam(`q`) // quality 音质 128k / 320k / flac / flac24bit 130 | // env.Loger.NewGroup(`LinkQuery`).Debug(`s: %v, id: %v, q: %v`, s, id, q).Free() 131 | // if ztool.Chk_IsNilStr(s, q, id) { 132 | // return &resp.Resp{Code: 6, Msg: `参数不全`} // http.StatusBadRequest 133 | // } 134 | // cquery := caches.NewQuery(s, id, q) 135 | // cquery.Request = c.Request 136 | // // fmt.Printf("%+v\n", cquery) 137 | // defer cquery.Free() 138 | // // _, ok := sources.UseSource.Verify(cquery) // 获取请求音质 同时检测是否支持(如kw源没有flac24bit) qualitys[q][s]rquery 139 | // // if !ok { 140 | // // return &resp.Resp{Code: 6, Msg: `不支持的平台或音质`} 141 | // // } 142 | 143 | // // 查询内存 144 | // if clink, ok := env.Cache.Get(cquery.Query()); ok { 145 | // if str, ok := clink.(string); ok { 146 | // env.Loger.NewGroup(`MemCache`).Debug(`MemHIT [%q]=>[%q]`, cquery.Query(), str).Free() 147 | // if str == `` { 148 | // return &resp.Resp{Code: 2, Msg: memRej} // 拒绝请求,当前一段时间内解析出错 `MemCache Reject` 149 | // } 150 | // return &resp.Resp{Msg: memHIT, Data: str} // `MemCache HIT` 151 | // } 152 | // } 153 | // // 查询缓存 154 | // var cstat bool 155 | // if caches.UseCache != nil { 156 | // cstat = caches.UseCache.Stat() 157 | // } 158 | // sc := env.Loger.NewGroup(`StatCache`) 159 | // defer sc.Free() 160 | // if cstat { 161 | // sc.Debug(`Method: Get, Query: %v`, cquery.Query()) 162 | // if link := caches.UseCache.Get(cquery); link != `` { 163 | // env.Cache.Set(cquery.Query(), link, 3600) 164 | // return &resp.Resp{Msg: cacheHIT, Data: link} 165 | // } 166 | // } else { 167 | // sc.Debug(`Disabled`) 168 | // } 169 | // atomic.AddInt64(&reqnum, 1) 170 | // // 解析歌曲外链 171 | // outlink, emsg := sources.UseSource.GetLink(cquery) 172 | // if emsg != `` { 173 | // if emsg == sources.Err_Verify { // Verify Failed: 不支持的平台或音质 174 | // return &resp.Resp{Code: 6, Msg: ztool.Str_FastConcat(emsg, `: 不支持的平台或音质`)} 175 | // } 176 | // env.Cache.Set(cquery.Query(), outlink, 600) // 发生错误的10分钟内禁止再次查询 177 | // return &resp.Resp{Code: 2, Msg: emsg, Data: outlink} 178 | // } 179 | // atomic.AddInt64(&secnum, 1) 180 | // // 缓存并获取直链 !(s == `kg` || (s == `tx` && !tx_en)) => (s != `kg` && (s != `tx` || tx_en)) 181 | // if outlink != `` && cstat && cquery.Source != sources.S_kg && (cquery.Source != sources.S_tx || env.Config.Custom.Tx_Enable) { 182 | // sc.Debug(`Method: Set, Link: %v`, outlink) 183 | // if link := caches.UseCache.Set(cquery, outlink); link != `` { 184 | // env.Cache.Set(cquery.Query(), link, 3600) 185 | // return &resp.Resp{Msg: cacheSet, Data: link} 186 | // } 187 | // } 188 | // // 无法获取直链 直接返回原链接 189 | // env.Cache.Set(cquery.Query(), outlink, 1200) 190 | // return &resp.Resp{Msg: cacheMISS, Data: outlink} 191 | // }) 192 | // } 193 | -------------------------------------------------------------------------------- /src/sources/builtin/driver.go: -------------------------------------------------------------------------------- 1 | // 内置解析源 2 | package builtin 3 | 4 | // import ( 5 | // "lx-source/src/caches" 6 | // "lx-source/src/env" 7 | // "lx-source/src/sources" 8 | // "lx-source/src/sources/custom/kg" 9 | // "lx-source/src/sources/custom/kw" 10 | // "lx-source/src/sources/custom/mg" 11 | // "lx-source/src/sources/custom/tx" 12 | // "lx-source/src/sources/custom/wy" 13 | // wm "lx-source/src/sources/custom/wy/modules" 14 | // "lx-source/src/sources/example" 15 | // "net/http" 16 | // "strconv" 17 | // "sync" 18 | // "time" 19 | 20 | // "github.com/ZxwyWebSite/ztool" 21 | // ) 22 | 23 | // type Source struct{} 24 | 25 | // // 预检 (兼容旧接口) 26 | // func (s *Source) Verify(c *caches.Query) (rquery string, ok bool) { 27 | // rquery, ok = qualitys[c.Quality][c.Source] 28 | // return 29 | // } 30 | 31 | // var ( 32 | // // 并发对象池 (用户限制在Router处实现) 33 | // wy_pool *sync.Pool 34 | // mg_pool = &sync.Pool{New: func() any { return new(MgApi_Song) }} 35 | // // kw_pool = &sync.Pool{New: func() any { return new(KwApi_Song) }} 36 | // // kg_pool = &sync.Pool{New: func() any { return new(KgApi_Song) }} 37 | // // tx_pool = &sync.Pool{New: func() any { return new(res_tx) }} 38 | // wv_pool *sync.Pool 39 | // ) 40 | 41 | // func init() { 42 | // env.Inits.Add(func() { 43 | // if env.Config.Source.Enable_Wy { 44 | // wy_pool = &sync.Pool{New: func() any { return new(wm.PlayInfo) }} 45 | // if env.Config.Source.MusicIdVerify { 46 | // wv_pool = &sync.Pool{New: func() any { return new(wm.VerifyInfo) }} 47 | // } 48 | // } 49 | // }) 50 | // } 51 | 52 | // // 查询 53 | // func (s *Source) GetLink(c *caches.Query) (outlink string, msg string) { 54 | // rquery, ok := s.Verify(c) 55 | // if !ok /*&& c.Source != `tx`*/ { 56 | // msg = sources.Err_Verify //`Verify Failed` 57 | // return 58 | // } 59 | // // var outlink string 60 | // jx := env.Loger.NewGroup(`Sources`) //sources.Loger.AppGroup(`builtin`) //env.Loger.NewGroup(`JieXiApis`) 61 | // defer jx.Free() 62 | // switch c.Source { 63 | // case sources.S_wy: 64 | // if !env.Config.Source.Enable_Wy { 65 | // msg = sources.ErrDisable 66 | // return 67 | // } 68 | // if wy.Url != nil { 69 | // outlink, msg = wy.Url(c.MusicID, c.Quality) 70 | // break 71 | // } 72 | // // 可用性验证 73 | // if env.Config.Source.MusicIdVerify { 74 | // vef := wv_pool.Get().(*wm.VerifyInfo) 75 | // defer wv_pool.Put(vef) 76 | // vurl := ztool.Str_FastConcat(`https://`, example.Vef_wy, `&id=`, c.MusicID) 77 | // _, err := ztool.Net_HttpReq(http.MethodGet, vurl, nil, example.Header_wy, &vef) 78 | // if err != nil { 79 | // jx.Error(`Wy, VefReq: %s`, err) 80 | // msg = sources.ErrHttpReq 81 | // return 82 | // } 83 | // jx.Debug(`Wy, Vef: %+v`, vef) 84 | // if vef.Code != 200 || !vef.Success { 85 | // msg = ztool.Str_FastConcat(`暂不可用:`, vef.Message) 86 | // return 87 | // } 88 | // } 89 | // // 获取外链 90 | // resp := wy_pool.Get().(*wm.PlayInfo) 91 | // defer wy_pool.Put(resp) 92 | // // 分流逻辑 (暂无其它节点) 93 | // // urls := [...]string{ 94 | // // ztool.Str_FastConcat(`http://`, api_wy, `?id=`, c.MusicID, `&level=`, rquery, `&noCookie=true`), 95 | // // ztool.Str_FastConcat(`https://`, api_wy, `&id=`, c.MusicID, `&level=`, rquery, `&encodeType=`, c.Extname), 96 | // // } 97 | // // url := urls[rand.Intn(len(urls))] 98 | // url := ztool.Str_FastConcat( 99 | // `https://`, example.Api_wy, `&id=`, c.MusicID, `&level=`, rquery, 100 | // `×tamp=`, strconv.FormatInt(time.Now().UnixMilli(), 10), 101 | // ) 102 | // // jx.Debug(`Wy, Url: %v`, url) 103 | // // wy源增加后端重试 默认3次 104 | // for i := 0; true; i++ { 105 | // _, err := ztool.Net_HttpReq(http.MethodGet, url, nil, example.Header_wy, &resp) 106 | // if err != nil { 107 | // jx.Error(`HttpReq, Err: %s, ReTry: %v`, err, i) 108 | // if i > 3 { 109 | // jx.Error(`Wy, HttpReq: %s`, err) 110 | // msg = sources.ErrHttpReq 111 | // return 112 | // } 113 | // time.Sleep(time.Second) 114 | // continue 115 | // } 116 | // break 117 | // } 118 | // jx.Debug(`Wy, Resp: %+v`, resp) 119 | // if len(resp.Data) == 0 { 120 | // msg = `No Data:Api接口忙,请稍后重试` 121 | // return 122 | // } 123 | // var data = resp.Data[0] 124 | // if data.Code != 200 || data.FreeTrialInfo != nil { 125 | // // jx.Error("发生错误, 返回数据:\n%#v", resp) 126 | // msg = `触发风控或专辑单独收费: ` + strconv.Itoa(data.Code) 127 | // return 128 | // } 129 | // if data.Level != rquery { 130 | // msg = ztool.Str_FastConcat(`实际音质不匹配: `, rquery, ` <= `, data.Level) // 实际音质不匹配: exhigh <= standard 131 | // if !env.Config.Source.ForceFallback { 132 | // return 133 | // } 134 | // } 135 | // // jx.Info(`WyLink, RealQuality: %v`, data.Level) 136 | // outlink = data.URL 137 | // case sources.S_mg: 138 | // if !env.Config.Source.Enable_Mg { 139 | // msg = sources.ErrDisable 140 | // return 141 | // } 142 | // if len(c.MusicID) != 11 { 143 | // msg = sources.E_VefMusicId 144 | // return 145 | // } 146 | // if mg.Url != nil { 147 | // outlink, msg = mg.Url(c.MusicID, c.Quality) 148 | // break 149 | // } 150 | // resp := mg_pool.Get().(*MgApi_Song) 151 | // defer mg_pool.Put(resp) 152 | 153 | // url := ztool.Str_FastConcat(`https://`, example.Api_mg, `?copyrightId=`, c.MusicID, `&type=`, rquery) 154 | // // jx.Debug(`Mg, Url: %v`, url) 155 | // _, err := ztool.Net_HttpReq(http.MethodGet, url, nil, example.Header_mg, &resp) 156 | // if err != nil { 157 | // jx.Error(`Mg, HttpReq: %s`, err) 158 | // msg = sources.ErrHttpReq 159 | // return 160 | // } 161 | // jx.Debug(`Mg, Resp: %+v`, resp) 162 | // if link := resp.Data.PlayURL; link != `` { 163 | // outlink = `https:` + link 164 | // } else { 165 | // msg = ztool.Str_FastConcat(resp.Code, `: `, resp.Msg) 166 | // } 167 | // case sources.S_kw: 168 | // if !env.Config.Source.Enable_Kw { 169 | // msg = sources.ErrDisable 170 | // return 171 | // } 172 | // outlink, msg = kw.Url(c.MusicID, c.Quality) 173 | // // if emsg != `` { 174 | // // msg = emsg 175 | // // return 176 | // // } 177 | // // outlink = ourl 178 | // case sources.S_kg: 179 | // if !env.Config.Source.Enable_Kg { 180 | // msg = sources.ErrDisable 181 | // return 182 | // } 183 | // if len(c.MusicID) != 32 { 184 | // msg = sources.E_VefMusicId 185 | // return 186 | // } 187 | // outlink, msg = kg.Url(c.MusicID, c.Quality) 188 | // // if emsg != `` { 189 | // // msg = emsg 190 | // // return 191 | // // } 192 | // // outlink = ourl 193 | // // case sources.S_kg: 194 | // // if !env.Config.Custom.Kg_Enable { 195 | // // msg = sources.ErrDisable 196 | // // return 197 | // // } 198 | // // resp := kg_pool.Get().(*KgApi_Song) 199 | // // defer kg_pool.Put(resp) 200 | 201 | // // // sep := strings.Split(c.MusicID, `-`) // 分割 Hash-Album 如 6DC276334F56E22BE2A0E8254D332B45-13097991 202 | // // // alb := func() string { 203 | // // // if len(sep) >= 2 { 204 | // // // return sep[1] 205 | // // // } 206 | // // // return `` 207 | // // // }() 208 | // // sep := c.Split() 209 | // // url := ztool.Str_FastConcat(api_kg, `&hash=`, sep[0], `&album_id=`, sep[1], `&_=`, strconv.FormatInt(time.Now().UnixMilli(), 10)) 210 | // // // jx.Debug(`Kg, Url: %s`, url) 211 | // // _, err := ztool.Net_HttpReq(http.MethodGet, url, nil, nil, &resp) 212 | // // if err != nil { 213 | // // jx.Error(`Kg, HttpReq: %s`, err) 214 | // // msg = sources.ErrHttpReq 215 | // // return 216 | // // } 217 | // // jx.Debug(`Kg, Resp: %+v`, resp) 218 | // // if resp.ErrCode != 0 { 219 | // // msg = ztool.Str_FastConcat(`Error: `, strconv.Itoa(resp.ErrCode)) 220 | // // return 221 | // // } 222 | // // var data KgApi_Data 223 | // // err = ztool.Val_MapToStruct(resp.Data, &data) 224 | // // if err != nil { 225 | // // msg = err.Error() 226 | // // return 227 | // // } 228 | // // if data.PlayBackupURL == `` { 229 | // // if data.PlayURL == `` { 230 | // // msg = sources.ErrNoLink 231 | // // return 232 | // // } 233 | // // outlink = data.PlayURL 234 | // // } 235 | // // outlink = data.PlayBackupURL 236 | // case sources.S_tx: 237 | // if !env.Config.Source.Enable_Tx { 238 | // msg = sources.ErrDisable 239 | // return 240 | // } 241 | // if len(c.MusicID) != 14 { 242 | // msg = sources.E_VefMusicId 243 | // return 244 | // } 245 | // outlink, msg = tx.Url(c.MusicID, c.Quality) 246 | // // if emsg != `` { 247 | // // msg = emsg 248 | // // return 249 | // // } 250 | // // outlink = ourl 251 | // // case `otx`: 252 | // // resp := tx_pool.Get().(*res_tx) 253 | // // defer tx_pool.Put(resp) 254 | 255 | // // sep := c.Split() 256 | // // url := ztool.Str_FastConcat(api_tx, 257 | // // `{"comm":{"ct":24,"cv":0,"format":"json","uin":"10086"},"req":{"method":"GetCdnDispatch","module":"CDN.SrfCdnDispatchServer","param":{"calltype":0,"guid":"1535153710","userip":""}},"req_0":{"method":"CgiGetVkey","module":"vkey.GetVkeyServer","param":{`, 258 | // // func(s string) string { 259 | // // if s == `` { 260 | // // return `` 261 | // // } 262 | // // return ztool.Str_FastConcat(`"filename":["`, rquery, s, `.`, c.Extname, `"],`) 263 | // // }(sep[1]), 264 | // // `"guid":"1535153710","loginflag":1,"platform":"20","songmid":["`, sep[0], `"],"songtype":[0],"uin":"10086"}}}`, 265 | // // ) 266 | // // // jx.Debug(`Tx, Url: %s`, url) 267 | // // out, err := ztool.Net_HttpReq(http.MethodGet, url, nil, header_tx, &resp) 268 | // // if err != nil { 269 | // // jx.Error(`Tx, HttpReq: %s`, err) 270 | // // msg = errHttpReq 271 | // // return 272 | // // } 273 | // // jx.Debug(`Tx, Resp: %s`, out) 274 | // // if resp.Code != 0 { 275 | // // msg = ztool.Str_FastConcat(`Error: `, strconv.Itoa(resp.Code)) 276 | // // return 277 | // // } 278 | // // if resp.Req0.Data.Midurlinfo[0].Purl == `` { 279 | // // msg = errNoLink 280 | // // return 281 | // // } 282 | // // outlink = ztool.Str_FastConcat(`https://dl.stream.qqmusic.qq.com/`, resp.Req0.Data.Midurlinfo[0].Purl) 283 | // default: 284 | // msg = `不支持的平台` 285 | // return 286 | // } 287 | // return 288 | // } 289 | -------------------------------------------------------------------------------- /src/sources/custom/custom.go: -------------------------------------------------------------------------------- 1 | package custom 2 | 3 | // type CSource struct{} 4 | -------------------------------------------------------------------------------- /src/sources/custom/driver.go: -------------------------------------------------------------------------------- 1 | // 账号解析源 2 | package custom 3 | 4 | import ( 5 | "lx-source/src/env" 6 | "lx-source/src/sources" 7 | "lx-source/src/sources/custom/kg" 8 | "lx-source/src/sources/custom/kw" 9 | "lx-source/src/sources/custom/mg" 10 | "lx-source/src/sources/custom/tx" 11 | "lx-source/src/sources/custom/wy" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | type ( 17 | // 源定义 18 | UrlFunc func(string, string) (string, string) 19 | LrcFunc func(string) (string, string) 20 | PicFunc func(string) (string, string) 21 | VefFunc func(*string) bool 22 | // 源接口 23 | Source interface { 24 | Url(string, string) (string, string) // 外链 25 | Lrc(string) (string, string) // 歌词 26 | Pic(string) (string, string) // 封面 27 | Vef(*string) bool // 验证 28 | Exp() int // 缓存 29 | } 30 | ) 31 | 32 | func notSupport(string) (string, string) { return ``, `不支持的平台或功能` } 33 | 34 | // 接口封装 35 | type WrapSource struct { 36 | UrlFunc 37 | LrcFunc 38 | PicFunc 39 | VefFunc 40 | ExpData int 41 | } 42 | 43 | func (ws *WrapSource) Url(songMid, quality string) (string, string) { 44 | return ws.UrlFunc(songMid, quality) 45 | } 46 | func (ws *WrapSource) Lrc(songMid string) (string, string) { 47 | return ws.LrcFunc(songMid) 48 | } 49 | func (ws *WrapSource) Pic(songMid string) (string, string) { 50 | return ws.PicFunc(songMid) 51 | } 52 | func (ws *WrapSource) Vef(songMid *string) bool { 53 | return ws.VefFunc(songMid) 54 | } 55 | func (ws *WrapSource) Exp() int { 56 | return ws.ExpData 57 | } 58 | 59 | var ( 60 | WySource Source 61 | MgSource Source 62 | KwSource Source 63 | KgSource Source 64 | TxSource Source 65 | LxSource Source 66 | ) 67 | 68 | func init() { 69 | env.Inits.Add(func() { 70 | if env.Config.Source.Enable_Wy { 71 | WySource = &WrapSource{ 72 | UrlFunc: wy.Url, 73 | LrcFunc: notSupport, 74 | PicFunc: notSupport, 75 | VefFunc: func(songMid *string) bool { 76 | _, err := strconv.ParseUint(*songMid, 10, 0) 77 | return err == nil 78 | }, 79 | ExpData: sources.C_wy, 80 | } 81 | } 82 | if env.Config.Source.Enable_Mg { 83 | MgSource = &WrapSource{ 84 | UrlFunc: mg.Url, 85 | LrcFunc: notSupport, 86 | PicFunc: notSupport, 87 | VefFunc: func(songMid *string) bool { 88 | return len(*songMid) == 11 89 | }, 90 | ExpData: sources.C_mg, 91 | } 92 | } 93 | if env.Config.Source.Enable_Kw { 94 | KwSource = &WrapSource{ 95 | UrlFunc: kw.Url, 96 | LrcFunc: notSupport, 97 | PicFunc: notSupport, 98 | VefFunc: func(songMid *string) bool { 99 | _, err := strconv.ParseUint(*songMid, 10, 0) 100 | return err == nil 101 | }, 102 | ExpData: sources.C_kw, 103 | } 104 | } 105 | if env.Config.Source.Enable_Kg { 106 | KgSource = &WrapSource{ 107 | UrlFunc: kg.Url, 108 | LrcFunc: notSupport, 109 | PicFunc: notSupport, 110 | VefFunc: func(songMid *string) (ok bool) { 111 | if ok = len(*songMid) == 32; ok { 112 | *songMid = strings.ToUpper(*songMid) 113 | } 114 | return 115 | }, 116 | ExpData: sources.C_kg, 117 | } 118 | } 119 | if env.Config.Source.Enable_Tx { 120 | TxSource = &WrapSource{ 121 | UrlFunc: tx.Url, 122 | LrcFunc: notSupport, 123 | PicFunc: notSupport, 124 | VefFunc: func(songMid *string) bool { 125 | return len(*songMid) == 14 126 | }, 127 | ExpData: sources.C_tx, 128 | } 129 | } 130 | if env.Config.Source.Enable_Lx { 131 | LxSource = nil 132 | } 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /src/sources/custom/kg/musicinfo.go: -------------------------------------------------------------------------------- 1 | package kg 2 | 3 | import ( 4 | "encoding/gob" 5 | "lx-source/src/env" 6 | "lx-source/src/sources" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/ZxwyWebSite/ztool" 13 | ) 14 | 15 | func init() { 16 | gob.Register(musicInfo{}) 17 | } 18 | 19 | func getMusicInfo(hash_ string) (info musicInfo, emsg string) { 20 | cquery := strings.Join([]string{`kg`, hash_, `info`}, `/`) 21 | if cdata, ok := env.Cache.Get(cquery); ok { 22 | if cinfo, ok := cdata.(musicInfo); ok { 23 | info = cinfo 24 | return 25 | } 26 | } 27 | body := ztool.Str_FastConcat( 28 | `{"area_code":"1","show_privilege":"1","show_album_info":"1","is_publish":"","appid":1005,"clientver":11451,"mid":"211008","dfid":"-","clienttime":"`, 29 | strconv.FormatInt(time.Now().Unix(), 10), 30 | `","key":"OIlwlieks28dk2k092lksi2UIkp","data":[{"hash":"`, 31 | hash_, 32 | `"}]}`, 33 | ) 34 | var infoResp struct { 35 | Status int `json:"status"` 36 | ErrorCode int `json:"error_code"` 37 | Errmsg string `json:"errmsg"` 38 | Data [][]musicInfo `json:"data"` 39 | } 40 | err := ztool.Net_Request( 41 | http.MethodPost, 42 | `http://gateway.kugou.com/v3/album_audio/audio`, 43 | strings.NewReader(body), 44 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeader(map[string]string{ 45 | `KG-THash`: `13a3164`, 46 | `KG-RC`: `1`, 47 | `KG-Fake`: `0`, 48 | `KG-RF`: `00869891`, 49 | `User-Agent`: `Android712-AndroidPhone-11451-376-0-FeeCacheUpdate-wifi`, 50 | `x-router`: `kmr.service.kugou.com`, 51 | })}, 52 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&infoResp)}, 53 | ) 54 | if err != nil { 55 | emsg = err.Error() 56 | return 57 | } 58 | if len(infoResp.Data) == 0 { 59 | if infoResp.Errmsg != `` { 60 | emsg = infoResp.Errmsg 61 | } else { 62 | emsg = `No Data` 63 | } 64 | return 65 | } 66 | info = infoResp.Data[0][0] 67 | emsg = infoResp.Errmsg 68 | env.Cache.Set(cquery, info, sources.C_lx) 69 | return 70 | } 71 | -------------------------------------------------------------------------------- /src/sources/custom/kg/player.go: -------------------------------------------------------------------------------- 1 | package kg 2 | 3 | import ( 4 | "lx-source/src/env" 5 | "lx-source/src/sources" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func Url(songMid, quality string) (ourl, msg string) { 13 | loger := env.Loger.NewGroup(`Kg`) 14 | defer loger.Free() 15 | rquality, ok := qualityMap[quality] 16 | if !ok { 17 | msg = sources.E_QNotSupport 18 | return 19 | } 20 | info, emsg := getMusicInfo(strings.ToLower(songMid)) 21 | if emsg != `` { 22 | loger.Error(`GetInfo: %v`, emsg) 23 | msg = emsg 24 | return 25 | } 26 | loger.Debug(`Info: %+v`, info) 27 | var tHash string 28 | switch quality { 29 | case sources.Q_128k: 30 | tHash = info.AudioInfo.Hash128 31 | case sources.Q_320k: 32 | tHash = info.AudioInfo.Hash320 33 | case sources.Q_flac: 34 | tHash = info.AudioInfo.HashFlac 35 | case sources.Q_fl24: 36 | tHash = info.AudioInfo.HashHigh 37 | } 38 | if tHash == `` { 39 | msg = sources.E_QNotMatch 40 | return 41 | } 42 | tHash = strings.ToLower(tHash) 43 | now := time.Now() 44 | params := map[string]string{ 45 | `album_id`: info.AlbumInfo.AlbumID, 46 | `userid`: env.Config.Custom.Kg_userId, 47 | `area_code`: `1`, 48 | `hash`: tHash, 49 | `module`: ``, 50 | `mid`: mid, 51 | `appid`: env.Config.Custom.Kg_Client_AppId, 52 | `ssa_flag`: `is_fromtrack`, 53 | `clientver`: env.Config.Custom.Kg_Client_Version, 54 | `open_time`: now.Format(`20060102`), 55 | `vipType`: `6`, 56 | `ptype`: `0`, 57 | `token`: env.Config.Custom.Kg_token, 58 | `auth`: ``, 59 | `mtype`: `0`, 60 | `album_audio_id`: info.AlbumAudioID, 61 | `behavior`: `play`, 62 | `clienttime`: strconv.FormatInt(now.Unix(), 10), 63 | `pid`: `2`, 64 | `key`: getKey(tHash), 65 | `dfid`: `-`, 66 | `pidversion`: `3001`, 67 | 68 | `quality`: rquality, 69 | // `IsFreePart`: `1`, 70 | } 71 | if !env.Config.Custom.Kg_Enable { 72 | params[`IsFreePart`] = `1` // 仅游客登录时允许获取试听 73 | } 74 | headers := map[string]string{ 75 | `User-Agent`: `Android712-AndroidPhone-8983-18-0-NetMusic-wifi`, 76 | `KG-THash`: `3e5ec6b`, 77 | `KG-Rec`: `1`, 78 | `KG-RC`: `1`, 79 | 80 | `x-router`: `tracker.kugou.com`, 81 | } 82 | var resp playInfo 83 | err := signRequest(http.MethodGet, url, ``, params, headers, &resp) 84 | if err != nil { 85 | loger.Error(`Request: %s`, err) 86 | msg = sources.ErrHttpReq 87 | return 88 | } 89 | loger.Debug(`Resp: %+v`, resp) 90 | switch resp.Status { 91 | case 3: 92 | msg = `该歌曲在酷狗没有版权,请换源播放` 93 | case 2: 94 | msg = `链接获取失败:请检查账号是否有会员或数字专辑是否购买` 95 | } 96 | if resp.Status != 1 { 97 | if msg == `` { 98 | msg = `链接获取失败,可能是数字专辑或者api失效,Status: ` + strconv.Itoa(resp.Status) 99 | } 100 | return 101 | } 102 | ourl = resp.URL[len(resp.URL)-1] 103 | return 104 | } 105 | -------------------------------------------------------------------------------- /src/sources/custom/kg/refresh.go: -------------------------------------------------------------------------------- 1 | package kg 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "errors" 7 | "lx-source/src/env" 8 | "math/rand" 9 | "net/http" 10 | "strconv" 11 | "time" 12 | 13 | "github.com/ZxwyWebSite/ztool" 14 | "github.com/ZxwyWebSite/ztool/logs" 15 | "github.com/ZxwyWebSite/ztool/x/bytesconv" 16 | "github.com/ZxwyWebSite/ztool/x/json" 17 | "github.com/ZxwyWebSite/ztool/zcypt" 18 | ) 19 | 20 | // 通过TOP500榜单获取随机歌曲的mixsongmid 21 | func randomMixSongMid() (mid string, err error) { 22 | // 声明榜单url 23 | const rankUrl = `http://mobilecdnbj.kugou.com/api/v3/rank/song?version=9108&ranktype=1&plat=0&pagesize=100&area_code=1&page=1&rankid=8888&with_res_tag=0&show_portrait_mv=1` 24 | // 请求 25 | var res rankInfo 26 | err = ztool.Net_Request( 27 | http.MethodGet, 28 | rankUrl, nil, 29 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeaders()}, 30 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&res)}, 31 | ) 32 | if err != nil { 33 | return 34 | } 35 | // fmt.Printf("%#v\n", res) 36 | if res.Status != 1 { 37 | err = errors.New(res.Error) 38 | return 39 | } 40 | 41 | // 随机选择一首歌曲 42 | randomSong := res.Data.Info[rand.Intn(len(res.Data.Info))] 43 | // fmt.Printf("%#v\n", randomSong) 44 | 45 | // 因为排行榜api不会返回mixsongmid 46 | // 所以需要进行一次搜索接口来获取 47 | var body searchInfo 48 | err = ztool.Net_Request( 49 | http.MethodGet, 50 | ztool.Str_FastConcat( 51 | `https://songsearch.kugou.com/song_search_v2?`, 52 | ztool.Net_Values(map[string]string{ 53 | `keyword`: randomSong.Filename, 54 | `area_code`: `1`, 55 | `page`: `1`, 56 | `pagesize`: `1`, 57 | `userid`: `0`, 58 | `clientver`: ``, 59 | `platform`: `WebFilter`, 60 | `filter`: `2`, 61 | `iscorrection`: `1`, 62 | `privilege_filter`: `0`, 63 | }), 64 | ), nil, 65 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeaders(map[string]string{ 66 | `Referer`: `https://www.kugou.com`, 67 | })}, 68 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&body)}, 69 | ) 70 | if err != nil { 71 | return 72 | } 73 | // fmt.Printf("%#v\n", body) 74 | if body.Status != 1 { 75 | err = errors.New(body.ErrorMsg) 76 | return 77 | } 78 | if body.Data.Total == 0 || len(body.Data.Lists) == 0 { 79 | err = errors.New(`歌曲搜索失败`) 80 | return 81 | } 82 | mid = body.Data.Lists[0].MixSongID 83 | return 84 | } 85 | 86 | // 签到主函数,传入userinfo,响应None就是成功,报错即为不成功 87 | func do_account_signin(loger *logs.Logger, now int64) (err error) { 88 | // 时间检测 89 | if now < env.Config.Custom.Kg_Lite_Interval { 90 | loger.Debug(`Key未过期,跳过...`) 91 | return nil 92 | } 93 | // 检查用户配置文件,获取mixsongmid 94 | mixid := env.Config.Custom.Kg_Lite_MixId //`582534238` 95 | if mixid == `auto` || mixid == `` { 96 | for i := 0; true; i++ { 97 | mixid, err = randomMixSongMid() 98 | if err != nil { 99 | loger.Error(`ReTry: %v, Err: %s`, i, err) 100 | if i >= 2 { 101 | return 102 | } 103 | time.Sleep(time.Second) 104 | continue 105 | } 106 | break 107 | } 108 | // mixid, err = randomMixSongMid() 109 | // if err != nil { 110 | // return 111 | // } 112 | loger.Info(`成功获取MixSongMid: ` + mixid) 113 | } else { 114 | loger.Info(`使用固定MixSongMid: ` + mixid) 115 | } 116 | 117 | // 声明变量 118 | headers := map[string]string{ 119 | `User-Agent`: ztool.Str_FastConcat( 120 | `Android712-AndroidPhone-`, 121 | env.Config.Custom.Kg_Client_Version, 122 | `-18-0-NetMusic-wifi`, 123 | ), 124 | `KG-THash`: `3e5ec6b`, 125 | `KG-Rec`: `1`, 126 | `KG-RC`: `1`, 127 | `x-router`: `youth.kugou.com`, 128 | } 129 | body := ztool.Str_FastConcat( 130 | `{"mixsongid":"`, mixid, `"}`, 131 | ) 132 | tnow := time.Now() 133 | params := map[string]string{ 134 | `userid`: env.Config.Custom.Kg_userId, 135 | `token`: env.Config.Custom.Kg_token, 136 | `appid`: env.Config.Custom.Kg_Client_AppId, 137 | `clientver`: env.Config.Custom.Kg_Client_Version, 138 | `clienttime`: strconv.FormatInt(tnow.Unix(), 10), 139 | `mid`: mid, 140 | `uuid`: zcypt.HexToString(zcypt.RandomBytes(16)), 141 | `dfid`: `-`, 142 | } 143 | 144 | // 发送请求 145 | var out refreshInfo 146 | err = signRequest( 147 | http.MethodPost, 148 | `https://gateway.kugou.com/v2/report/listen_song`, 149 | body, params, headers, &out, 150 | ) 151 | if err != nil { 152 | return err 153 | } 154 | loger.Debug(`Resp: %+v`, out) 155 | if out.Status != 1 || out.ErrorCode != 0 { 156 | switch out.ErrorCode { 157 | case 130012: 158 | loger.Info(`今日已签到过,明天再来吧`) 159 | case 51002: 160 | panic(`登录过期啦!请重新获取账号Token`) 161 | default: 162 | return errors.New(strconv.Itoa(out.ErrorCode) + `: ` + out.ErrorMsg) 163 | } 164 | } else { 165 | loger.Info(`Lite签到成功`) 166 | } 167 | tomorrow := time.Date(tnow.Year(), tnow.Month(), tnow.Day()+1, 0, 0, 0, 0, tnow.Location()) 168 | env.Config.Custom.Kg_Lite_Interval = tomorrow.Unix() 169 | 170 | return env.Cfg.Save(``) 171 | } 172 | 173 | // 刷新Token 174 | func login_by_token(loger *logs.Logger, now int64) (err error) { 175 | // 前置到期检测 176 | if now < env.Config.Custom.Kg_Refresh_Interval { 177 | loger.Debug(`Key未过期,跳过...`) 178 | return 179 | } 180 | // 获取加密参数 181 | var aeskey []byte 182 | switch env.Config.Custom.Kg_Client_AppId { 183 | case `1005`: 184 | aeskey = []byte(`90b8382a1bb4ccdcf063102053fd75b8`) 185 | case `3116`: 186 | aeskey = []byte(`c24f74ca2820225badc01946dba4fdf7`) 187 | default: 188 | panic(`当前应用AppId暂不支持此功能`) 189 | } 190 | // 生成请求数据 191 | tnow := time.Now() 192 | pbyte, _ := json.Marshal(map[string]any{ 193 | `clienttime`: tnow.Unix(), 194 | `token`: env.Config.Custom.Kg_token, 195 | }) 196 | block, _ := aes.NewCipher(aeskey) 197 | encrypter := cipher.NewCBCEncrypter(block, aeskey[block.BlockSize():]) 198 | padata := zcypt.PKCS7Padding(pbyte, block.BlockSize()) 199 | encrypted := make([]byte, len(padata)) 200 | encrypter.CryptBlocks(encrypted, padata) 201 | encstr := zcypt.HexToString(encrypted) 202 | bodys, _ := json.Marshal(map[string]any{ 203 | `t1`: 0, 204 | `t2`: 0, 205 | `p3`: encstr, 206 | `userid`: env.Config.Custom.Kg_userId, 207 | `clienttime_ms`: tnow.UnixMilli(), 208 | }) 209 | params := map[string]string{ 210 | `dfid`: `-`, 211 | `mid`: `20211008`, 212 | `clientver`: env.Config.Custom.Kg_Client_Version, 213 | `clienttime`: strconv.FormatInt(tnow.Unix(), 10), 214 | `appid`: env.Config.Custom.Kg_Client_AppId, 215 | } 216 | headers := map[string]string{ 217 | `User-Agent`: `Android711-1070-10860-14-0-LOGIN-wifi`, 218 | `KG-THash`: `7af653c`, 219 | `KG-Rec`: `1`, 220 | `KG-RC`: `1`, 221 | } 222 | // 请求对应接口 223 | var res loginInfo 224 | err = signRequest( 225 | http.MethodPost, 226 | `http://login.user.kugou.com/v4/login_by_token`, 227 | bytesconv.BytesToString(bodys), 228 | params, headers, &res, 229 | ) 230 | if err != nil { 231 | return errors.New(`接口请求失败: ` + err.Error()) 232 | } 233 | loger.Info(`获取数据成功`) 234 | loger.Debug(`Resp: %+v`, res) 235 | if res.ErrorCode != 0 { 236 | return errors.New(`刷新登录失败: ` + strconv.Itoa(res.ErrorCode)) 237 | } 238 | env.Config.Custom.Kg_token = res.Data.Token 239 | env.Config.Custom.Kg_userId = strconv.Itoa(res.Data.Userid) 240 | next := time.Date(tnow.Year(), tnow.Month(), tnow.Day()+25, 0, 0, 0, 0, tnow.Location()) 241 | env.Config.Custom.Kg_Refresh_Interval = next.Unix() 242 | loger.Info(`刷新登录成功`) 243 | return env.Cfg.Save(``) 244 | } 245 | 246 | func init() { 247 | env.Inits.Add(func() { 248 | if env.Config.Custom.Kg_token != `` { 249 | if env.Config.Custom.Kg_Lite_Enable && env.Config.Custom.Kg_Client_AppId == `3116` { 250 | env.Tasker.Add(`kg_litsign`, do_account_signin, 86000, true) 251 | } 252 | if env.Config.Custom.Kg_Refresh_Enable { 253 | env.Tasker.Add(`kg_refresh`, login_by_token, 86000, true) 254 | } 255 | } 256 | /*if env.Config.Custom.Kg_Lite_Enable { 257 | if env.Config.Custom.Kg_Client_AppId == `3116` && env.Config.Custom.Kg_token != `` { 258 | env.Tasker.Add(`kg_litsign`, do_account_signin, 86000, true) 259 | } 260 | } 261 | if env.Config.Custom.Kg_Refresh_Enable && env.Config.Custom.Kg_token != `` { 262 | env.Tasker.Add(`kg_refresh`, login_by_token, 86000, true) 263 | }*/ 264 | }) 265 | } 266 | -------------------------------------------------------------------------------- /src/sources/custom/kg/utils.go: -------------------------------------------------------------------------------- 1 | package kg 2 | 3 | import ( 4 | "lx-source/src/env" 5 | "lx-source/src/sources" 6 | "strings" 7 | 8 | "github.com/ZxwyWebSite/ztool" 9 | "github.com/ZxwyWebSite/ztool/x/slices" 10 | "github.com/ZxwyWebSite/ztool/zcypt" 11 | ) 12 | 13 | var ( 14 | // qualityHashMap = map[string]string{ 15 | // sources.Q_128k: `hash_128`, 16 | // sources.Q_320k: `hash_320`, 17 | // sources.Q_flac: `hash_flac`, 18 | // sources.Q_fl24: `hash_high`, 19 | // } 20 | qualityMap = map[string]string{ 21 | sources.Q_128k: `128`, 22 | sources.Q_320k: `320`, 23 | sources.Q_flac: sources.Q_flac, 24 | sources.Q_fl24: `high`, 25 | 26 | sources.Q_master: `viper_atmos`, 27 | } 28 | ) 29 | 30 | const ( 31 | // signkey = `OIlwieks28dk2k092lksi2UIkp` 32 | // pidversec = `57ae12eb6890223e355ccfcb74edf70d` 33 | // clientver = `12029` 34 | url = `https://gateway.kugou.com/v5/url` 35 | // appid = `1005` 36 | mid = `211008` 37 | ) 38 | 39 | func sortDict(dictionary map[string]string) ([]string, int) { 40 | length := len(dictionary) 41 | var keys = make([]string, 0, length) 42 | for k := range dictionary { 43 | keys = append(keys, k) 44 | } 45 | slices.Sort(keys) 46 | return keys, length 47 | } 48 | 49 | // func sign(params map[string]string, body string) string { 50 | // keys, lens := sortDict(params) 51 | // var b strings.Builder 52 | // for i := 0; i < lens; i++ { 53 | // b.WriteString(keys[i]) 54 | // b.WriteByte('=') 55 | // b.WriteString(params[keys[i]]) 56 | // } 57 | // // b.WriteString(body) 58 | // return zcypt.MD5EncStr(ztool.Str_FastConcat(signkey, b.String(), signkey)) 59 | // } 60 | 61 | func signRequest(method string, url string, body string, params, headers map[string]string, out any) error { 62 | keys, lens := sortDict(params) 63 | // buildSignatureParams 64 | var b strings.Builder 65 | for i := 0; i < lens; i++ { 66 | b.WriteString(keys[i]) 67 | b.WriteByte('=') 68 | b.WriteString(params[keys[i]]) 69 | } 70 | b.WriteString(body) 71 | // buildRequestParams 72 | var c strings.Builder 73 | for j := 0; j < lens; j++ { 74 | c.WriteString(keys[j]) 75 | c.WriteByte('=') 76 | c.WriteString(params[keys[j]]) 77 | c.WriteByte('&') 78 | } 79 | c.WriteString(`signature`) 80 | c.WriteByte('=') 81 | c.WriteString(zcypt.MD5EncStr(ztool.Str_FastConcat( 82 | env.Config.Custom.Kg_Client_SignKey, 83 | b.String(), env.Config.Custom.Kg_Client_SignKey, 84 | ))) 85 | 86 | url = ztool.Str_FastConcat(url, `?`, c.String()) 87 | // ztool.Cmd_FastPrintln(url) 88 | return ztool.Net_Request( 89 | method, url, strings.NewReader(body), 90 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeader(headers)}, 91 | []ztool.Net_ResHandlerFunc{ //func(res *http.Response) error { 92 | // body, err := io.ReadAll(res.Body) 93 | // fmt.Printf("%s, %s, %s\n", body, err, res.Status) 94 | // return ztool.Err_EsContinue 95 | // }, 96 | ztool.Net_ResToStruct(out), 97 | }, 98 | ) 99 | } 100 | 101 | func getKey(hash_ string) string { 102 | return zcypt.MD5EncStr(ztool.Str_FastConcat( 103 | strings.ToLower(hash_), env.Config.Custom.Kg_Client_PidVerSec, 104 | env.Config.Custom.Kg_Client_AppId, mid, env.Config.Custom.Kg_userId, 105 | )) 106 | } 107 | 108 | // 解析版权字段 (Copilot) 109 | /* 110 | // 定义一个函数,接受一个整数作为参数,返回一个布尔值的切片,表示每一位的状态 111 | 依次为: 下载是否付费 \ 下载是否禁止 \ 播放是否付费 \ 播放是否禁止 112 | https://open.kugou.com/docs/open-player/#/android-sdk?v=1&id=%e6%ad%8c%e6%9b%b2%e5%ad%97%e6%ae%b5%e8%a7%a3%e6%9e%90 113 | */ 114 | // func parsePrivilege(privilege int) []bool { 115 | // result := make([]bool, 4) // 创建一个长度为4的切片 116 | // for i := 0; i < 4; i++ { 117 | // // 用位运算符&来判断每一位是否为1,如果是则将对应的切片元素设为true 118 | // if privilege&(1<> 32 162 | for i := 0; i < 16; i++ { 163 | R := pSource[1] 164 | R = bit_transform(arrayE, 64, R) 165 | R ^= longs[i] 166 | for j := 0; j < 8; j++ { 167 | pR[j] = 255 & (R >> (j * 8)) 168 | } 169 | var SOut int64 170 | for sbi := 7; sbi >= 0; sbi-- { 171 | SOut <<= 4 172 | SOut |= matrixNSBox[sbi][pR[sbi]] 173 | } 174 | 175 | R = bit_transform(arrayP, 32, SOut) 176 | L := pSource[0] 177 | pSource[0] = pSource[1] 178 | pSource[1] = L ^ R 179 | } 180 | pSource = []int64{pSource[1], pSource[0]} 181 | out = (-4294967296 & (pSource[1] << 32)) | (0xFFFFFFFF & pSource[0]) 182 | out = bit_transform(arrayIP_1, 64, out) 183 | return //out 184 | } 185 | 186 | // 生成子密钥函数 187 | func sub_keys(l int64, longs []int64, n int) { 188 | l2 := bit_transform(arrayPC_1, 56, l) 189 | for i := 0; i < 16; i++ { 190 | l2 = ((l2 & arrayLsMask[arrayLs[i]]) << (28 - arrayLs[i])) | ((l2 & ^arrayLsMask[arrayLs[i]]) >> arrayLs[i]) 191 | longs[i] = bit_transform(arrayPC_2, 64, l2) 192 | } 193 | j := 0 194 | for n == 1 && j < 8 { 195 | // l3 := longs[j] 196 | longs[j], longs[15-j] = longs[15-j], longs[j] 197 | j++ 198 | } 199 | } 200 | 201 | // 加密函数 202 | func encrypt(msg []byte, key []byte) []byte { 203 | // if len(key) != 8 { 204 | // panic("key length must be 8 bytes") 205 | // } 206 | 207 | // 处理密钥块 208 | var l int64 209 | for i := 0; i < 8; i++ { 210 | l = l | int64(key[i])<<(i*8) 211 | } 212 | 213 | j := len(msg) / 8 214 | // arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了 215 | arrLong1 := make([]int64, 16) 216 | sub_keys(l, arrLong1, 0) 217 | // arrLong2 存放的是前部分的明文 218 | arrLong2 := make([]int64, j) 219 | for m := 0; m < j; m++ { 220 | for n := 0; n < 8; n++ { 221 | arrLong2[m] |= int64(msg[n+m*8]) << (n * 8) 222 | } 223 | } 224 | 225 | // 用于存放密文 226 | arrLong3 := make([]int64, (1+8*(j+1))/8) 227 | // 计算前部的数据块(除了最后一部分) 228 | for i1 := 0; i1 < j; i1++ { 229 | arrLong3[i1] = _DES64(arrLong1, arrLong2[i1]) 230 | } 231 | 232 | // 保存多出来的字节 233 | arrByte1 := msg[j*8:] 234 | var l2 int64 235 | for i1 := 0; i1 < len(msg)%8; i1++ { 236 | l2 |= int64(arrByte1[i1]) << (i1 * 8) 237 | } 238 | // 计算多出的那一位(最后一位) 239 | arrLong3[j] = _DES64(arrLong1, l2) 240 | 241 | // 将密文转为字节型 242 | arrByte2 := make([]byte, 8*len(arrLong3)) 243 | i4 := 0 244 | for _, l3 := range arrLong3 { 245 | for i6 := 0; i6 < 8; i6++ { 246 | arrByte2[i4] = byte(255 & (l3 >> (i6 * 8))) 247 | i4++ 248 | } 249 | } 250 | return arrByte2 251 | } 252 | 253 | // base64编码函数 254 | func base64_encrypt(msg string) string { 255 | b1 := encrypt(bytesconv.StringToBytes(msg), SECRET_KEY) 256 | return zcypt.Base64ToString(base64.StdEncoding, b1) 257 | // s := base64.StdEncoding.EncodeToString(b1) 258 | // return s //strings.ReplaceAll(s, "\n", ``) 259 | } 260 | -------------------------------------------------------------------------------- /src/sources/custom/kw/types.go: -------------------------------------------------------------------------------- 1 | package kw 2 | 3 | type ( 4 | // KwDES json 5 | playInfo struct { 6 | Code int `json:"code"` 7 | Locationid string `json:"locationid"` 8 | Data struct { 9 | Bitrate int `json:"bitrate"` 10 | User string `json:"user"` 11 | Sig string `json:"sig"` 12 | Type string `json:"type"` 13 | Format string `json:"format"` 14 | P2PAudiosourceid string `json:"p2p_audiosourceid"` 15 | Rid int `json:"rid"` 16 | // Source string `json:"source"` 17 | URL string `json:"url"` 18 | } `json:"data"` 19 | Msg string `json:"msg"` 20 | } 21 | // 酷我音乐接口 (波点) 22 | kwApi_Song struct { 23 | Code int `json:"code"` 24 | Msg string `json:"msg"` 25 | ReqID string `json:"reqId"` 26 | Data struct { 27 | Duration int `json:"duration"` 28 | AudioInfo struct { 29 | Bitrate string `json:"bitrate"` 30 | Format string `json:"format"` 31 | Level string `json:"level"` 32 | Size string `json:"size"` 33 | } `json:"audioInfo"` 34 | URL string `json:"url"` 35 | } `json:"data"` 36 | ProfileID string `json:"profileId"` 37 | CurTime int64 `json:"curTime"` 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /src/sources/custom/kw/utils.go: -------------------------------------------------------------------------------- 1 | package kw 2 | 3 | import ( 4 | "bytes" 5 | "lx-source/src/sources" 6 | 7 | "github.com/ZxwyWebSite/ztool/x/bytesconv" 8 | ) 9 | 10 | var ( 11 | fileInfo = map[string]struct { 12 | E string // 扩展名 13 | H string // 专用音质 14 | }{ 15 | sources.Q_128k: { 16 | E: sources.X_mp3, 17 | H: sources.Q_128k, 18 | }, 19 | sources.Q_320k: { 20 | E: sources.X_mp3, 21 | H: sources.Q_320k, 22 | }, 23 | sources.Q_flac: { 24 | E: sources.Q_flac, 25 | H: `2000k`, 26 | }, 27 | sources.Q_fl24: { 28 | E: sources.Q_flac, 29 | H: `4000k`, 30 | }, 31 | } 32 | // 注:这个还是有规律的,加上或去掉k即可直接比较 33 | // qualityMapReverse = map[string]string{ 34 | // `128`: sources.Q_128k, 35 | // `320`: sources.Q_320k, 36 | // `2000`: sources.Q_flac, 37 | // `4000`: sources.Q_fl24, 38 | // } 39 | desheader = map[string]string{ 40 | // `User-Agent`: `okhttp/3.10.0`, 41 | } 42 | bdheader = map[string]string{ 43 | `channel`: `guanfang`, 44 | `plat`: `ar`, 45 | `net`: `wifi`, 46 | `ver`: `3.1.4`, 47 | `api-ver`: `application/json`, 48 | 49 | `user-agent`: `Dart/2.18 (dart:io)`, //`Dalvik/2.1.0 (Linux; U; Android 7.1.1; OPPO R9sk Build/NMF26F)`, 50 | } 51 | // bdsreg = regexp.MustCompile(`[^a-zA-Z0-9]`) 52 | ) 53 | 54 | func mkMap(data []byte) map[string]string { 55 | out := make(map[string]string) 56 | sep := bytes.Split(data, []byte{13, 10}) 57 | for i, r := 0, len(sep); i < r; i++ { 58 | var s = sep[i] 59 | if p := bytes.IndexByte(s, '='); p != -1 { 60 | out[bytesconv.BytesToString(s[:p])] = bytesconv.BytesToString(s[p+1:]) 61 | continue 62 | } else { 63 | out[`_`] += bytesconv.BytesToString(s) + `;` 64 | } 65 | /*pat := bytes.Split(sep[i], []byte{61}) 66 | if len(pat) >= 2 { 67 | out[bytesconv.BytesToString(pat[0])] = bytesconv.BytesToString(pat[1]) 68 | continue 69 | } 70 | out[`_`] += bytesconv.BytesToString(pat[0]) + `;`*/ 71 | } 72 | return out 73 | } 74 | 75 | // 波点签名算法 76 | /*func Bdsign(str string, m, m2 map[string]string) *strings.Builder { 77 | var b strings.Builder 78 | b.WriteString(`uid=`) 79 | b.WriteString(env.Config.Custom.Kw_Bd_Uid) 80 | b.WriteByte('&') 81 | b.WriteString(`token=`) 82 | b.WriteString(env.Config.Custom.Kw_Bd_Token) 83 | b.WriteByte('&') 84 | b.WriteString(`timestamp=`) 85 | b.WriteString(strconv.FormatInt(time.Now().UnixMilli(), 10)) 86 | for k, v := range m2 { 87 | b.WriteByte('&') 88 | b.WriteString(k) 89 | b.WriteByte('=') 90 | b.WriteString(url.QueryEscape(v)) 91 | } 92 | // 取 strings.Builder.buf []byte 地址 93 | pb := (*[]byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + unsafe.Sizeof((*strings.Builder)(nil)))) 94 | charArray := bdsreg.ReplaceAll(*pb, []byte{}) 95 | slices.Sort(charArray) 96 | str3 := string(charArray) 97 | fmt.Println(str3) 98 | lowerCase := zcypt.MD5EncStr(`kuwotest` + str3 + `/api/play/music/v2/audioUrl`) 99 | b.WriteByte('&') 100 | b.WriteString(`sign=`) 101 | b.WriteString(lowerCase) 102 | return &b 103 | }*/ 104 | -------------------------------------------------------------------------------- /src/sources/custom/mg/albuminfo.go: -------------------------------------------------------------------------------- 1 | package mg 2 | 3 | import ( 4 | "encoding/gob" 5 | "errors" 6 | "lx-source/src/env" 7 | "lx-source/src/sources" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/ZxwyWebSite/ztool" 12 | ) 13 | 14 | func init() { 15 | gob.Register(albumInfo{}) 16 | } 17 | 18 | func getAlbumInfo(aid string) (info albumInfo, err error) { 19 | cquery := strings.Join([]string{`mg`, aid, `ainfo`}, `/`) 20 | if cdata, ok := env.Cache.Get(cquery); ok { 21 | if cinfo, ok := cdata.(albumInfo); ok { 22 | info = cinfo 23 | return 24 | } 25 | } 26 | err = ztool.Net_Request( 27 | http.MethodGet, ztool.Str_FastConcat( 28 | `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?needSimple=01&resourceId=`, aid, `&resourceType=2003`, 29 | ), nil, 30 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeaders()}, 31 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&info)}, 32 | ) 33 | if err == nil { 34 | if len(info.Resource) == 0 { 35 | err = errors.New(`no Album Resource`) 36 | } else { 37 | env.Cache.Set(cquery, info, sources.C_lx) 38 | } 39 | } 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /src/sources/custom/mg/musicinfo.go: -------------------------------------------------------------------------------- 1 | package mg 2 | 3 | import ( 4 | "encoding/gob" 5 | "errors" 6 | "lx-source/src/env" 7 | "lx-source/src/sources" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/ZxwyWebSite/ztool" 12 | ) 13 | 14 | func init() { 15 | gob.Register(musicInfo{}) 16 | } 17 | 18 | func getMusicInfo(cid string) (info musicInfo, err error) { 19 | cquery := strings.Join([]string{`mg`, cid, `minfo`}, `/`) 20 | if cdata, ok := env.Cache.Get(cquery); ok { 21 | if cinfo, ok := cdata.(musicInfo); ok { 22 | info = cinfo 23 | return 24 | } 25 | } 26 | err = ztool.Net_Request( 27 | http.MethodGet, ztool.Str_FastConcat( 28 | `https://c.musicapp.migu.cn/MIGUM2.0/v1.0/content/resourceinfo.do?copyrightId=`, cid, `&resourceType=2`, 29 | ), nil, 30 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeaders()}, 31 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&info)}, 32 | ) 33 | if err == nil { 34 | if len(info.Resource) == 0 { 35 | err = errors.New(`no Music Resource`) 36 | } else { 37 | env.Cache.Set(cquery, info, sources.C_lx) 38 | } 39 | } 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /src/sources/custom/mg/player.go: -------------------------------------------------------------------------------- 1 | package mg 2 | 3 | import ( 4 | "lx-source/src/env" 5 | "lx-source/src/sources" 6 | "lx-source/src/sources/custom/utils" 7 | "lx-source/src/sources/example" 8 | "net/http" 9 | "sync" 10 | 11 | "github.com/ZxwyWebSite/ztool" 12 | ) 13 | 14 | var ( 15 | Url func(string, string) (string, string) 16 | 17 | mg_pool *sync.Pool 18 | ) 19 | 20 | func init() { 21 | env.Inits.Add(func() { 22 | loger := env.Loger.NewGroup(`MgInit`) 23 | switch env.Config.Custom.Mg_Mode { 24 | case `0`, `builtin`: 25 | loger.Debug(`use builtin`) 26 | mg_pool = &sync.Pool{New: func() any { return new(mgApi_Song) }} 27 | Url = builtin 28 | case `1`, `custom`: 29 | loger.Debug(`use custom`) 30 | if ztool.Chk_IsNilStr( 31 | // env.Config.Custom.Mg_Usr_VerId, 32 | // env.Config.Custom.Mg_Usr_Token, 33 | env.Config.Custom.Mg_Usr_OSVer, 34 | env.Config.Custom.Mg_Usr_ReqUA, 35 | ) { 36 | loger.Fatal(`使用自定义账号且用户参数为空`) 37 | } 38 | mg_pool = &sync.Pool{New: func() any { return new(playInfo) }} 39 | Url = mcustom 40 | case `2`, `malbum`: 41 | loger.Debug(`use malbum`) 42 | Url = malbum 43 | default: 44 | loger.Fatal(`未定义的接口模式,请检查配置 [Custom].Mg_Mode`) 45 | } 46 | loger.Free() 47 | }) 48 | } 49 | 50 | func builtin(songMid, quality string) (ourl, msg string) { 51 | loger := env.Loger.NewGroup(`Mg`) 52 | defer loger.Free() 53 | rquality, ok := qualitys[quality] 54 | if !ok { 55 | msg = sources.E_QNotSupport 56 | return 57 | } 58 | resp := mg_pool.Get().(*mgApi_Song) 59 | defer mg_pool.Put(resp) 60 | url := ztool.Str_FastConcat(`https://`, example.Api_mg, `?copyrightId=`, songMid, `&type=`, rquality) 61 | err := ztool.Net_Request( 62 | http.MethodGet, url, nil, 63 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeaders(example.Header_mg)}, 64 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&resp)}, 65 | ) 66 | if err != nil { 67 | loger.Error(`HttpReq: %s`, err) 68 | msg = sources.ErrHttpReq 69 | return 70 | } 71 | loger.Debug(`Resp: %+v`, resp) 72 | // ourl, msg = resp.Url(rquality) 73 | if resp.Data.PlayURL != `` { 74 | ourl = `https:` + utils.DelQuery(resp.Data.PlayURL) 75 | } else { 76 | msg = ztool.Str_FastConcat(resp.Code, `: `, resp.Msg) 77 | } 78 | return 79 | } 80 | 81 | func mcustom(songMid, quality string) (ourl, msg string) { 82 | loger := env.Loger.NewGroup(`Mg`) 83 | defer loger.Free() 84 | rquality, ok := qualityMap[quality] 85 | if !ok { 86 | msg = sources.E_QNotSupport 87 | return 88 | } 89 | url := ztool.Str_FastConcat( 90 | `https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.4?toneFlag=`, rquality, 91 | `&songId=`, songMid, 92 | `&resourceType=2`, 93 | ) 94 | resp := mg_pool.Get().(*playInfo) 95 | defer mg_pool.Put(resp) 96 | err := ztool.Net_Request( 97 | http.MethodGet, url, nil, 98 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeaders(map[string]string{ 99 | `User-Agent`: env.Config.Custom.Mg_Usr_ReqUA, 100 | `aversionid`: env.Config.Custom.Mg_Usr_VerId, 101 | `token`: env.Config.Custom.Mg_Usr_Token, 102 | `channel`: `0146832`, 103 | `language`: `Chinese`, 104 | `ua`: `Android_migu`, 105 | `mode`: `android`, 106 | `os`: `Android ` + env.Config.Custom.Mg_Usr_OSVer, 107 | })}, 108 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&resp)}, 109 | ) 110 | if err != nil { 111 | loger.Error(`Request: %s`, err) 112 | msg = sources.ErrHttpReq 113 | return 114 | } 115 | loger.Debug(`Resp: %+v`, resp) 116 | if resp.Code != `000000` { 117 | msg = resp.Info 118 | return 119 | } 120 | if resp.Data.URL == `` { 121 | msg = `No Data: 无返回链接` 122 | return 123 | } 124 | if resp.Data.AudioFormatType != rquality { 125 | msg = ztool.Str_FastConcat(`实际音质不匹配: `, rquality, ` <= `, resp.Data.AudioFormatType) 126 | if !env.Config.Source.ForceFallback { 127 | return 128 | } 129 | } 130 | ourl = utils.DelQuery(resp.Data.URL) 131 | return 132 | } 133 | 134 | func malbum(songMid, quality string) (ourl, msg string) { 135 | loger := env.Loger.NewGroup(`Mg`) 136 | defer loger.Free() 137 | rquality, ok := qualityMap[quality] 138 | if !ok { 139 | msg = sources.E_QNotSupport 140 | return 141 | } 142 | minfo, err := getMusicInfo(songMid) 143 | if err != nil { 144 | msg = err.Error() 145 | return 146 | } 147 | loger.Debug(`mInfo: %+v`, minfo) 148 | var hasQuality bool 149 | for _, v := range minfo.Resource[0].NewRateFormats { 150 | if hasQuality = v.FormatType == rquality; hasQuality { 151 | break 152 | } 153 | } 154 | if !hasQuality { 155 | msg = sources.E_QNotMatch 156 | return 157 | } 158 | ainfo, err := getAlbumInfo(minfo.Resource[0].AlbumID) 159 | if err != nil { 160 | msg = err.Error() 161 | return 162 | } 163 | loger.Debug(`aInfo: %+v`, ainfo) 164 | for _, v := range ainfo.Resource[0].SongItems { 165 | if v.CopyrightID == songMid { 166 | for _, w := range v.NewRateFormats { 167 | if w.FormatType == rquality { 168 | if rquality == `PQ` || rquality == `HQ` { 169 | ourl = w.URL 170 | } else { 171 | ourl = w.AndroidURL 172 | } 173 | break 174 | } 175 | } 176 | break 177 | } 178 | } 179 | if ourl == `` { 180 | msg = sources.E_NoLink 181 | } else { 182 | ourl = `https://freetyst.nf.migu.cn` + ourl[24:] 183 | } 184 | return 185 | } 186 | -------------------------------------------------------------------------------- /src/sources/custom/mg/refresh.go: -------------------------------------------------------------------------------- 1 | package mg 2 | 3 | import ( 4 | "errors" 5 | "lx-source/src/env" 6 | "net/http" 7 | 8 | "github.com/ZxwyWebSite/ztool" 9 | "github.com/ZxwyWebSite/ztool/logs" 10 | ) 11 | 12 | func refresh(loger *logs.Logger, now int64) error { 13 | var out map[string]any 14 | err := ztool.Net_Request( 15 | http.MethodPost, 16 | `https://m.music.migu.cn/migumusic/h5/user/auth/userActiveNotice`, 17 | nil, 18 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeaders(mgheader)}, 19 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&out)}, 20 | ) 21 | if err == nil { 22 | if out[`code`].(int) != http.StatusOK { 23 | return errors.New(out[`msg`].(string)) 24 | } else { 25 | loger.Info(`咪咕session保活成功`) 26 | } 27 | } 28 | return err 29 | } 30 | 31 | func init() { 32 | env.Inits.Add(func() { 33 | if env.Config.Custom.Mg_Refresh_Enable && false { 34 | env.Tasker.Add(`mg_refresh`, refresh, 86000, true) 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/sources/custom/mg/utils.go: -------------------------------------------------------------------------------- 1 | package mg 2 | 3 | import "lx-source/src/sources" 4 | 5 | // const ( 6 | // q_128k = `PQ` 7 | // q_320k = `HQ` 8 | // q_flac = `SQ` 9 | // q_fl24 = `ZQ` 10 | // ) 11 | 12 | var ( 13 | qualityMap = map[string]string{ 14 | sources.Q_128k: `PQ`, 15 | sources.Q_320k: `HQ`, 16 | sources.Q_flac: `SQ`, 17 | sources.Q_fl24: `ZQ`, 18 | } 19 | // qualityMapReverse = map[string]string{ 20 | // q_128k: sources.Q_128k, 21 | // q_320k: sources.Q_320k, 22 | // q_flac: sources.Q_flac, 23 | // q_fl24: sources.Q_fl24, 24 | // } 25 | qualitys = map[string]string{ 26 | sources.Q_128k: `1`, 27 | sources.Q_320k: `2`, 28 | sources.Q_flac: `3`, 29 | sources.Q_fl24: `4`, 30 | 31 | sources.Q_master: `5`, 32 | } 33 | // qualitysReverse = map[string]string { 34 | // `000009`: sources.Q_128k, 35 | // `020010`: sources.Q_320k, 36 | // `011002`: sources.Q_flac, 37 | // `011005`: sources.Q_fl24, 38 | // } 39 | mgheader = map[string]string{ 40 | `Origin`: `https://m.music.migu.cn`, 41 | `Referer`: `https://m.music.migu.cn/v4/`, 42 | `By`: ``, 43 | `channel`: ``, 44 | `Cookie`: ``, 45 | } 46 | ) 47 | -------------------------------------------------------------------------------- /src/sources/custom/tx/encrypt.go: -------------------------------------------------------------------------------- 1 | package tx 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/ZxwyWebSite/ztool" 8 | "github.com/ZxwyWebSite/ztool/x/bytesconv" 9 | "github.com/ZxwyWebSite/ztool/zcypt" 10 | ) 11 | 12 | func v(b string) string { 13 | res := []byte{} 14 | p := [...]int{21, 4, 9, 26, 16, 20, 27, 30} 15 | for _, x := range p { 16 | res = append(res, b[x]) 17 | } 18 | return bytesconv.BytesToString(res) //string(res) 19 | } 20 | 21 | func c(b string) string { 22 | res := []byte{} 23 | p := [...]int{18, 11, 3, 2, 1, 7, 6, 25} 24 | for _, x := range p { 25 | res = append(res, b[x]) 26 | } 27 | return bytesconv.BytesToString(res) //string(res) 28 | } 29 | 30 | func y(a, b, c int) (e []int) { 31 | // e := []int{} 32 | r25 := a >> 2 33 | if b != 0 && c != 0 { 34 | r26 := a & 3 35 | r26_2 := r26 << 4 36 | r26_3 := b >> 4 37 | r26_4 := r26_2 | r26_3 38 | r27 := b & 15 39 | r27_2 := r27 << 2 40 | r27_3 := r27_2 | (c >> 6) 41 | r28 := c & 63 42 | e = append(e, r25) 43 | e = append(e, r26_4) 44 | e = append(e, r27_3) 45 | e = append(e, r28) 46 | } else { 47 | r10 := a >> 2 48 | r11 := a & 3 49 | r11_2 := r11 << 4 50 | e = append(e, r10) 51 | e = append(e, r11_2) 52 | } 53 | return //e 54 | } 55 | 56 | func n(ls []int) string { 57 | e := []int{} 58 | for i, r := 0, len(ls); i < r; i += 3 { 59 | if i < r-2 { 60 | e = append(e, y(ls[i], ls[i+1], ls[i+2])...) 61 | } else { 62 | e = append(e, y(ls[i], 0, 0)...) 63 | } 64 | } 65 | res := []byte{} 66 | b64all := `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=` 67 | for _, i := range e { 68 | res = append(res, b64all[i]) 69 | } 70 | return bytesconv.BytesToString(res) 71 | } 72 | 73 | func t(b string) (res []int) { 74 | zd := map[string]int{ 75 | `0`: 0, 76 | `1`: 1, 77 | `2`: 2, 78 | `3`: 3, 79 | `4`: 4, 80 | `5`: 5, 81 | `6`: 6, 82 | `7`: 7, 83 | `8`: 8, 84 | `9`: 9, 85 | `A`: 10, 86 | `B`: 11, 87 | `C`: 12, 88 | `D`: 13, 89 | `E`: 14, 90 | `F`: 15, 91 | } 92 | ol := [...]int{212, 45, 80, 68, 195, 163, 163, 203, 157, 220, 254, 91, 204, 79, 104, 6} 93 | // res := []int{} 94 | j := 0 95 | for i, r := 0, len(b); i < r; i += 2 { 96 | one := zd[string(b[i])] 97 | two := zd[string(b[i+1])] 98 | r := one*16 ^ two 99 | // if j >= 16 { 100 | // break 101 | // } 102 | res = append(res, r^ol[j]) 103 | j++ 104 | } 105 | return //res 106 | } 107 | 108 | // func createMD5(s []byte) string { 109 | // hash := md5.New() 110 | // hash.Write(s) 111 | // return hex.EncodeToString(hash.Sum(nil)) 112 | // } 113 | 114 | func sign(params []byte) string { 115 | md5Str := strings.ToUpper(zcypt.CreateMD5(params)) 116 | h := v(md5Str) 117 | e := c(md5Str) 118 | ls := t(md5Str) 119 | m := n(ls) 120 | res := ztool.Str_FastConcat(`zzb`, h, m, e) //`zzb` + h + m + e 121 | res = strings.ToLower(res) 122 | r := regexp.MustCompile(`[\/+]`) 123 | res = r.ReplaceAllString(res, ``) 124 | return res 125 | } 126 | -------------------------------------------------------------------------------- /src/sources/custom/tx/info.go: -------------------------------------------------------------------------------- 1 | package tx 2 | 3 | // func Info(songMid string) (info any, msg string) { 4 | // req, emsg := getMusicInfo(songMid) 5 | // if emsg != `` { 6 | // msg = emsg 7 | // return 8 | // } 9 | // var singerList []any 10 | // for _, s := range req.TrackInfo.Singer { 11 | // // item := new(struct{ 12 | // // ID int `json:"id"` 13 | // // Str string 14 | // // }) 15 | // // item.ID = s.ID 16 | // singerList = append(singerList, s) 17 | // } 18 | // var file_info map[string]struct{ 19 | // Size interface{} 20 | // } 21 | // if req.TrackInfo.File.Size128Mp3 != 0 { 22 | // file_info[`128k`].Size = 23 | // } 24 | // return 25 | // } 26 | -------------------------------------------------------------------------------- /src/sources/custom/tx/musicinfo.go: -------------------------------------------------------------------------------- 1 | package tx 2 | 3 | import ( 4 | "encoding/gob" 5 | "lx-source/src/env" 6 | "lx-source/src/sources" 7 | "strings" 8 | 9 | "github.com/ZxwyWebSite/ztool" 10 | "github.com/ZxwyWebSite/ztool/x/bytesconv" 11 | ) 12 | 13 | func init() { 14 | gob.Register(musicInfo{}) 15 | } 16 | 17 | func getMusicInfo(songMid string) (infoBody musicInfo, emsg string) { 18 | cquery := strings.Join([]string{`tx`, songMid, `info`}, `/`) 19 | if cdata, ok := env.Cache.Get(cquery); ok { 20 | if cinfo, ok := cdata.(musicInfo); ok { 21 | infoBody = cinfo 22 | return 23 | } 24 | } 25 | infoReqBody := ztool.Str_FastConcat(`{"comm":{"ct":"19","cv":"1859","uin":"0"},"req":{"method":"get_song_detail_yqq","module":"music.pf_song_detail_svr","param":{"song_mid":"`, songMid, `","song_type":0}}}`) 26 | var infoResp struct { 27 | Code int `json:"code"` 28 | // Ts int64 `json:"ts"` 29 | // StartTs int64 `json:"start_ts"` 30 | // Traceid string `json:"traceid"` 31 | Req struct { 32 | Code int `json:"code"` 33 | Data musicInfo `json:"data"` 34 | } `json:"req"` 35 | } 36 | err := signRequest(bytesconv.StringToBytes(infoReqBody), &infoResp) 37 | if err != nil { 38 | emsg = err.Error() 39 | return //nil, err.Error() 40 | } 41 | if infoResp.Code != 0 || infoResp.Req.Code != 0 { 42 | emsg = `获取音乐信息失败` 43 | return //nil, `获取音乐信息失败` 44 | } 45 | infoBody = infoResp.Req.Data 46 | env.Cache.Set(cquery, infoBody, sources.C_lx) 47 | return //infoBody.Req.Data, `` 48 | } 49 | -------------------------------------------------------------------------------- /src/sources/custom/tx/player.go: -------------------------------------------------------------------------------- 1 | package tx 2 | 3 | import ( 4 | "lx-source/src/env" 5 | "lx-source/src/sources" 6 | 7 | "github.com/ZxwyWebSite/ztool" 8 | "github.com/ZxwyWebSite/ztool/x/bytesconv" 9 | ) 10 | 11 | /* 12 | 音乐URL获取逻辑: 13 | if需要付费播放and无账号信息: 14 | 试听获取 15 | el有账号信息or无需付费: 16 | 正常获取 17 | if没有链接: 18 | 尝试获取试听 19 | if还没有链接: 20 | 报错 21 | 返回结果 22 | 注: 23 | 以上逻辑暂时没想好怎么改, 24 | 当前根据配置文件 [Custom].Tx_Enable 是否开启判断, 25 | if需要付费播放and未开启账号解析: 26 | 试听获取 27 | el无需付费or有账号信息: 28 | 正常获取 29 | if没有链接: 30 | 报错 31 | 返回结果 32 | 更新: 33 | 可通过 goto loop 实现,但可能会导致逻辑混乱 (想使用账号获取正常链接却返回试听链接) 34 | 2024-03-16: 35 | 正常获取->128k获取->试听获取 36 | */ 37 | 38 | func Url(songMid, quality string) (ourl, msg string) { 39 | loger := env.Loger.NewGroup(`Tx`) 40 | defer loger.Free() 41 | infoFile, ok := fileInfo[quality] 42 | if !ok || (!env.Config.Custom.Tx_Enable && quality != sources.Q_128k) { 43 | msg = sources.E_QNotSupport //`不支持的音质` 44 | return 45 | } 46 | infoBody, emsg := getMusicInfo(songMid) 47 | if emsg != `` { 48 | loger.Error(`GetInfo: %v`, emsg) 49 | msg = emsg 50 | return 51 | } 52 | loger.Debug(`infoBody: %+v`, infoBody) 53 | var uauthst, uuin string = env.Config.Custom.Tx_Ukey, env.Config.Custom.Tx_Uuin 54 | if uuin == `` || !env.Config.Custom.Tx_Enable { 55 | uuin = `1535153710` 56 | } 57 | var strFileName string 58 | tryLink := infoBody.TrackInfo.Pay.PayPlay == 1 && /*uauthst == ``&&*/ !env.Config.Custom.Tx_Enable 59 | Loop: 60 | if tryLink { 61 | if infoBody.TrackInfo.Vs[0] == `` { 62 | msg = sources.ErrNoLink 63 | return 64 | } 65 | strFileName = ztool.Str_FastConcat(`RS02`, infoBody.TrackInfo.Vs[0], `.`, sources.X_mp3) 66 | } else { 67 | strFileName = ztool.Str_FastConcat(infoFile.H, infoBody.TrackInfo.File.MediaMid, `.`, infoFile.E) 68 | } 69 | requestBody := ztool.Str_FastConcat( 70 | `{"comm":{"authst":"`, 71 | uauthst, 72 | `","ct":"26","cv":"2010101","qq":"`, 73 | uuin, 74 | `","v":"2010101"},"req_0":{"method":"CgiGetVkey","module":"vkey.GetVkeyServer","param":{"filename":["`, 75 | strFileName, 76 | `"],"guid":"20211008","loginflag":1,"platform":"20","songmid":["`, 77 | songMid, 78 | `"],"songtype":[0],"uin":"`, 79 | uuin, 80 | `"}}}`, 81 | ) 82 | var infoResp struct { 83 | Code int `json:"code"` 84 | // Ts int64 `json:"ts"` 85 | // StartTs int64 `json:"start_ts"` 86 | // Traceid string `json:"traceid"` 87 | Req0 playInfo `json:"req_0"` 88 | } 89 | err := signRequest(bytesconv.StringToBytes(requestBody), &infoResp) 90 | if err != nil { 91 | loger.Error(`Request: %s`, err) 92 | msg = err.Error() 93 | return 94 | } 95 | loger.Debug(`infoResp: %+v`, infoResp) 96 | if len(infoResp.Req0.Data.Midurlinfo) == 0 { 97 | msg = `No Data: 无返回数据` 98 | return 99 | } 100 | infoData := infoResp.Req0.Data.Midurlinfo[0] 101 | if infoData.Purl == `` { 102 | if env.Config.Source.ForceFallback && !tryLink { 103 | if quality != sources.Q_128k && infoBody.TrackInfo.Pay.PayPlay == 0 { 104 | msg = `Fallback to 128k` 105 | infoFile = fileInfo[sources.Q_128k] 106 | quality = sources.Q_128k 107 | } else { 108 | msg = `Fallbacked` 109 | tryLink = true 110 | } 111 | goto Loop 112 | } 113 | msg = sources.E_NoLink //`无法获取音乐链接` 114 | return 115 | } 116 | realQuality := ztool.Str_Before(infoData.Filename, `.`)[:4] //strings.Split(infoData.Filename, `.`)[0][:4] 117 | // if qualityMapReverse[realQuality] != quality && /*infoBody.TrackInfo.Pay.PayPlay == 0*/ !tryLink { 118 | // msg = sources.E_QNotMatch //`实际音质不匹配` 119 | // return 120 | // } 121 | if realQuality != infoFile.H && !tryLink { 122 | msg = sources.E_QNotMatch 123 | if !env.Config.Source.ForceFallback { 124 | return 125 | } 126 | } 127 | ourl = env.Config.Custom.Tx_CDNUrl + infoData.Purl 128 | return 129 | } 130 | -------------------------------------------------------------------------------- /src/sources/custom/tx/refresh.go: -------------------------------------------------------------------------------- 1 | package tx 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "lx-source/src/env" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "github.com/ZxwyWebSite/ztool" 12 | "github.com/ZxwyWebSite/ztool/logs" 13 | "github.com/ZxwyWebSite/ztool/x/bytesconv" 14 | ) 15 | 16 | /*type AutoGenerated struct { 17 | Code int `json:"code"` 18 | Ts int64 `json:"ts"` 19 | StartTs int64 `json:"start_ts"` 20 | Traceid string `json:"traceid"` 21 | Req1 struct { 22 | Code int `json:"code"` 23 | Data struct { 24 | Openid string `json:"openid"` 25 | RefreshToken string `json:"refresh_token"` 26 | AccessToken string `json:"access_token"` 27 | ExpiredAt int `json:"expired_at"` 28 | Musicid int `json:"musicid"` 29 | MusicKey string `json:"musickey"` 30 | MusickeyCreateTime int `json:"musickeyCreateTime"` 31 | FirstLogin int `json:"first_login"` 32 | ErrMsg string `json:"errMsg"` 33 | SessionKey string `json:"sessionKey"` 34 | Unionid string `json:"unionid"` 35 | StrMusicId string `json:"str_musicid"` 36 | Errtip string `json:"errtip"` 37 | Nick string `json:"nick"` 38 | Logo string `json:"logo"` 39 | FeedbackURL string `json:"feedbackURL"` 40 | EncryptUin string `json:"encryptUin"` 41 | Userip string `json:"userip"` 42 | LastLoginTime int `json:"lastLoginTime"` 43 | KeyExpiresIn int `json:"keyExpiresIn"` 44 | RefreshKey string `json:"refresh_key"` 45 | LoginType int `json:"loginType"` 46 | Prompt2Bind int `json:"prompt2bind"` 47 | LogoffStatus int `json:"logoffStatus"` 48 | OtherAccounts []interface{} `json:"otherAccounts"` 49 | OtherPhoneNo string `json:"otherPhoneNo"` 50 | Token string `json:"token"` 51 | IsPrized int `json:"isPrized"` 52 | IsShowDevManage int `json:"isShowDevManage"` 53 | ErrTip2 string `json:"errTip2"` 54 | Tip3 string `json:"tip3"` 55 | EncryptedPhoneNo string `json:"encryptedPhoneNo"` 56 | PhoneNo string `json:"phoneNo"` 57 | } `json:"data"` 58 | } `json:"req1"` 59 | }*/ 60 | 61 | type refreshData struct { 62 | Req1 struct { 63 | Code int `json:"code"` 64 | Data struct { 65 | ExpiredAt int64 `json:"expired_at"` // 过期时间 (Unix) 66 | // MusicId int `json:"musicid"` // 数字uid 67 | MusicKey string `json:"musickey"` // 账号Key 68 | StrMusicId string `json:"str_musicid"` // 字符串uid 69 | // KeyExpiresIn int `json:"keyExpiresIn"` // 过期时间 (秒) 70 | } `json:"data"` 71 | } `json:"req1"` 72 | } 73 | 74 | /* 75 | 刷新登录模块 (移植自Python版) 76 | 逻辑: 77 | 1. 使用内存缓存设置过期时间,每次获取链接时取值检查,若没有设置或已过期则尝试刷新Key 78 | 2. 以计划任务方式运行,每隔一段时间自动执行 79 | 注: 80 | 第一次载入时会刷新一次测试可用性&同步过期时间 (默认7天) 81 | */ 82 | 83 | func refresh(loger *logs.Logger, now int64) error { 84 | // 前置检测 85 | if now < env.Config.Custom.Tx_Refresh_Interval { 86 | loger.Debug(`Key未过期,跳过...`) 87 | return nil 88 | } 89 | // 刷新逻辑 (注:QQ登录最常用所以先判断QHL开头) 90 | var body, surl string 91 | if strings.HasPrefix(env.Config.Custom.Tx_Ukey, `Q_H_L`) { 92 | body = ztool.Str_FastConcat( 93 | `{"req1":{"method":"QQLogin","module":"QQConnectLogin.LoginServer","param":{"expired_in":7776000,"musicid":`, 94 | env.Config.Custom.Tx_Uuin, 95 | `,"musickey":"`, 96 | env.Config.Custom.Tx_Ukey, 97 | `"}}}`, 98 | ) 99 | surl = `6` 100 | } else if strings.HasPrefix(env.Config.Custom.Tx_Ukey, `W_X`) { 101 | body = ztool.Str_FastConcat( 102 | `{"comm":{"authst":"","ct":"11","cv":"12080008","fPersonality":"0","qq":"","tmeAppID":"qqmusic","tmeLoginMethod":"1","tmeLoginType":"1","v":"12080008"},"req1":{"method":"Login","module":"music.login.LoginServer","param":{"code":"","loginMode":2,"musickey":"`, 103 | env.Config.Custom.Tx_Ukey, 104 | `","openid":"","refresh_key":"","refresh_token":"","str_musicid":"`, 105 | env.Config.Custom.Tx_Uuin, 106 | `","unionid":""}}}`, 107 | ) 108 | } else { 109 | // 致命错误(删除任务) 110 | panic(`未知的 qqmusic_key 格式, 请检查配置 [Custom].Tx_Ukey`) 111 | } 112 | loger.Debug(`Body: %v`, body) 113 | var resp refreshData 114 | signature := sign(bytesconv.StringToBytes(body)) 115 | err := ztool.Net_Request( 116 | http.MethodPost, 117 | ztool.Str_FastConcat(`https://u`, surl, `.y.qq.com/cgi-bin/musics.fcg?sign=`, signature), 118 | strings.NewReader(body), 119 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeaders(header)}, 120 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&resp)}, 121 | ) 122 | if err != nil { 123 | // loger.Error(`请求Api失败: %s`, err) 124 | return errors.New(`请求Api失败: ` + err.Error()) 125 | } 126 | loger.Debug(`Resp: %+v`, resp) 127 | if resp.Req1.Code != 0 { 128 | switch resp.Req1.Code { 129 | case 1000: 130 | return fmt.Errorf(`%v: Token无效或已过期`, resp.Req1.Code) 131 | case 2000: 132 | return fmt.Errorf(`%v: 该Token不支持刷新`, resp.Req1.Code) 133 | default: 134 | return fmt.Errorf(`%v: 刷新登录失败`, resp.Req1.Code) 135 | } 136 | // loger.Warn("刷新登录失败, code: %v\n响应体: %+v", resp.Req1.Code, resp) 137 | // return fmt.Errorf("刷新登录失败, code: %v\n响应体: %+v", resp.Req1.Code, resp) 138 | } 139 | loger.Info(`刷新登录成功`) 140 | env.Config.Custom.Tx_Uuin = resp.Req1.Data.StrMusicId 141 | env.Config.Custom.Tx_Ukey = resp.Req1.Data.MusicKey 142 | tnow := time.Now() 143 | env.Config.Custom.Tx_Refresh_Interval = time.Date(tnow.Year(), tnow.Month(), tnow.Day()+5, 0, 0, 0, 0, tnow.Location()).Unix() 144 | // env.Config.Custom.Tx_Refresh_Interval = now + 432000 //(每5天刷新一次) //1209600 - 86000 // 14天提前一天 145 | loger.Debug(`Resp: %+v`, resp) 146 | loger.Debug(`Uuin: %v, Ukey: %v`, resp.Req1.Data.StrMusicId, resp.Req1.Data.MusicKey) 147 | loger.Debug(`ExpiresAt: %v, Real: %v`, resp.Req1.Data.ExpiredAt, env.Config.Custom.Tx_Refresh_Interval) 148 | err = env.Cfg.Save(``) 149 | // if err != nil { 150 | // loger.Error(`%s`, err) 151 | // return 152 | // } 153 | if err == nil { 154 | loger.Info(`数据更新成功`) // 已通过相应数据更新uin和qqmusic_key 155 | } 156 | return err 157 | } 158 | 159 | func init() { 160 | env.Inits.Add(func() { 161 | if env.Config.Custom.Tx_Refresh_Enable && env.Config.Custom.Tx_Ukey != `` && env.Config.Custom.Tx_Uuin != `` { 162 | env.Tasker.Add(`tx_refresh`, refresh, 86000, true) 163 | } 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /src/sources/custom/tx/utils.go: -------------------------------------------------------------------------------- 1 | package tx 2 | 3 | import ( 4 | "bytes" 5 | "lx-source/src/sources" 6 | "net/http" 7 | 8 | "github.com/ZxwyWebSite/ztool" 9 | ) 10 | 11 | var ( 12 | fileInfo = map[string]struct { 13 | E string // 扩展名 14 | H string // 专用音质 15 | }{ 16 | sources.Q_128k: { 17 | E: sources.X_mp3, 18 | H: `M500`, 19 | }, 20 | sources.Q_320k: { 21 | E: sources.X_mp3, 22 | H: `M800`, 23 | }, 24 | sources.Q_flac: { 25 | E: sources.Q_flac, 26 | H: `F000`, 27 | }, 28 | sources.Q_fl24: { 29 | E: sources.Q_flac, 30 | H: `RS01`, 31 | }, 32 | sources.Q_dolby: { 33 | E: sources.Q_flac, 34 | H: `Q000`, 35 | }, 36 | sources.Q_master: { 37 | E: sources.Q_flac, 38 | H: `AI00`, // (~~母带音质大部分都是AI提上去的~~) 39 | }, 40 | } 41 | // qualityMapReverse = map[string]string{ 42 | // `M500`: sources.Q_128k, 43 | // `M800`: sources.Q_320k, 44 | // `F000`: sources.Q_flac, 45 | // `RS01`: sources.Q_fl24, 46 | // `Q000`: `dolby`, 47 | // `AI00`: `master`, 48 | // } 49 | header = map[string]string{ 50 | `Referer`: `https://y.qq.com/`, 51 | } 52 | ) 53 | 54 | func signRequest(data []byte, out any) error { 55 | s := sign(data) 56 | return ztool.Net_Request(http.MethodPost, 57 | ztool.Str_FastConcat(`https://u.y.qq.com/cgi-bin/musics.fcg?format=json&sign=`, s), 58 | bytes.NewReader(data), 59 | []ztool.Net_ReqHandlerFunc{ 60 | ztool.Net_ReqAddHeaders(header), 61 | }, 62 | []ztool.Net_ResHandlerFunc{ 63 | ztool.Net_ResToStruct(out), 64 | }, 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /src/sources/custom/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/ZxwyWebSite/ztool" 5 | ) 6 | 7 | // func SizeFormat(size int) string { 8 | // if size < 1024 { 9 | // return ztool.Str_FastConcat(strconv.Itoa(size), `B`) 10 | // } 11 | // size64 := float64(size) 12 | // if size64 < math.Pow(size64, 2) { 13 | 14 | // } 15 | // return `` 16 | // } 17 | 18 | // 删除?号后尾随内容 19 | func DelQuery(str string) string { 20 | return ztool.Str_Before(str, `?`) 21 | } 22 | -------------------------------------------------------------------------------- /src/sources/custom/wy/modules/core_crypto.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rsa" 8 | "crypto/x509" 9 | "encoding/base64" 10 | "encoding/pem" 11 | "math/rand" 12 | _ "unsafe" 13 | 14 | "github.com/ZxwyWebSite/ztool" 15 | "github.com/ZxwyWebSite/ztool/x/bytesconv" 16 | "github.com/ZxwyWebSite/ztool/x/json" 17 | "github.com/ZxwyWebSite/ztool/zcypt" 18 | ) 19 | 20 | // crypto.js 21 | 22 | var ( 23 | ivKey = bytesconv.StringToBytes(`0102030405060708`) 24 | presetKey = bytesconv.StringToBytes(`0CoJUm6Qyw8W8jud`) 25 | linuxapiKey = bytesconv.StringToBytes(`rFgB&h#%2?^eDg:Q`) 26 | base62 = bytesconv.StringToBytes(`abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`) 27 | publicKey = bytesconv.StringToBytes(`-----BEGIN PUBLIC KEY----- 28 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB 29 | -----END PUBLIC KEY-----`) 30 | eapiKey = bytesconv.StringToBytes(`e82ckenh8dichen8`) 31 | ) 32 | 33 | func aesEncrypt(text, key []byte, iv bool) []byte { 34 | pad := 16 - len(text)%16 35 | text = append(text, bytes.Repeat([]byte{byte(pad)}, pad)...) 36 | block, _ := aes.NewCipher(key) 37 | // if err != nil { 38 | // panic(err) 39 | // } 40 | var encryptor cipher.BlockMode 41 | if iv { 42 | encryptor = cipher.NewCBCEncrypter(block, ivKey) 43 | } else { 44 | encryptor = zcypt.NewECBEncrypter(block) 45 | } 46 | ciphertext := make([]byte, len(text)) 47 | encryptor.CryptBlocks(ciphertext, text) 48 | if iv { 49 | return zcypt.Base64Encode(base64.StdEncoding, ciphertext) 50 | } 51 | return bytes.ToUpper(zcypt.HexEncode(ciphertext)) 52 | } 53 | 54 | //go:linkname rsaEncryptNone crypto/rsa.encrypt 55 | func rsaEncryptNone(*rsa.PublicKey, []byte) ([]byte, error) 56 | 57 | func rsaEncrypt(data []byte) string { 58 | pblock, _ := pem.Decode(publicKey) 59 | pubKey, _ := x509.ParsePKIXPublicKey(pblock.Bytes) 60 | // 注:为实现NONE加密手动导出了标准库里的encrypt方法,若编译不过添加以下代码 61 | // /usr/local/go/src/crypto/rsa/rsa.go:478 62 | // ``` 63 | // var Encrypt = encrypt // export 64 | // ``` 65 | // 第二种方式:linkname调用,不用改库 https://www.jianshu.com/p/7b3638b47845 66 | encData, err := rsaEncryptNone(pubKey.(*rsa.PublicKey), data) 67 | if err != nil { 68 | panic(err) 69 | } 70 | return zcypt.HexToString(encData) 71 | } 72 | 73 | func weapi(object map[string]any) map[string][]string { 74 | text, err := json.Marshal(object) 75 | if err != nil { 76 | panic(err) 77 | } 78 | secretKey := make([]byte, 16) 79 | for i := 0; i < 16; i++ { 80 | secretKey[i] = base62[rand.Intn(62)] 81 | } 82 | return map[string][]string{ 83 | `params`: {bytesconv.BytesToString(aesEncrypt( 84 | aesEncrypt(text, presetKey, true), 85 | secretKey, 86 | true, 87 | ))}, 88 | `encSecKey`: {rsaEncrypt(ztool.Sort_ReverseNew(secretKey))}, 89 | } 90 | } 91 | 92 | func linuxapi(object map[string]any) map[string][]string { 93 | text, err := json.Marshal(object) 94 | if err != nil { 95 | panic(err) 96 | } 97 | return map[string][]string{ 98 | `eparams`: {bytesconv.BytesToString(aesEncrypt(text, linuxapiKey, false))}, 99 | } 100 | } 101 | 102 | func eapi(url string, object map[string]any) map[string][]string { 103 | text, err := json.Marshal(object) 104 | if err != nil { 105 | panic(err) 106 | } 107 | message := ztool.Str_FastConcat( 108 | `nobody`, url, `use`, bytesconv.BytesToString(text), `md5forencrypt`, 109 | ) 110 | digest := zcypt.CreateMD5(bytesconv.StringToBytes(message)) 111 | data := bytes.Join( 112 | [][]byte{ 113 | bytesconv.StringToBytes(url), 114 | text, 115 | bytesconv.StringToBytes(digest), 116 | }, 117 | []byte{45, 51, 54, 99, 100, 52, 55, 57, 98, 54, 98, 53, 45}, 118 | ) 119 | return map[string][]string{ 120 | `params`: {bytesconv.BytesToString(aesEncrypt(data, eapiKey, false))}, 121 | } 122 | } 123 | 124 | func decrypt(data []byte) (out []byte) { 125 | dec, err := zcypt.HexDecode(data) 126 | if err == nil { 127 | out, err = zcypt.AesDecrypt(dec, eapiKey) 128 | } 129 | if err != nil { 130 | panic(err) 131 | } 132 | return out 133 | } 134 | -------------------------------------------------------------------------------- /src/sources/custom/wy/modules/core_request.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "math/rand" 7 | "net/http" 8 | stdurl "net/url" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/ZxwyWebSite/ztool" 15 | "github.com/ZxwyWebSite/ztool/x/json" 16 | "github.com/ZxwyWebSite/ztool/zcypt" 17 | ) 18 | 19 | // request.js 20 | 21 | const anonymous_token = `1f5fa7b6a6a9f81a11886e5186fde7fb98e25cf0036d7afd055b980b2261f5464b7f5273fc3921d1262bfec66a19a30c41d8da00c3685f5ace96f0d5a48b6db334d974731083682e3324751bcc9aaf44c3061cd1` 22 | 23 | var ( 24 | wapiReg = regexp.MustCompile(`\w*api`) 25 | csrfReg = regexp.MustCompile(`_csrf=([^(;|$)]+)`) 26 | domaReg = regexp.MustCompile(`\s*Domain=[^(;|$)]+;*`) 27 | ) 28 | 29 | var userAgentMap = map[string]string{ 30 | `mobile`: `Mozilla/5.0 (iPhone; CPU iPhone OS 17_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1`, 31 | `pc`: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0`, 32 | } 33 | 34 | type ( 35 | ReqQuery struct { 36 | Cookie map[string]string 37 | RealIP string 38 | Ids string 39 | Br string 40 | Level string 41 | } 42 | reqOptions struct { 43 | Headers map[string]string 44 | UA string 45 | RealIP string 46 | IP string 47 | Crypto string 48 | Url string 49 | Cookie interface{} 50 | } 51 | ReqAnswer struct { 52 | Status int 53 | Body map[string]any 54 | Cookie []string 55 | } 56 | ) 57 | 58 | func createRequest(method, url string, data map[string]any, options reqOptions) (*ReqAnswer, error) { 59 | if options.Headers == nil { 60 | options.Headers = make(map[string]string) 61 | } 62 | options.Headers[`User-Agent`] = userAgentMap[options.UA] 63 | if method == http.MethodPost { 64 | options.Headers[`Content-Type`] = `application/x-www-form-urlencoded` 65 | } 66 | if strings.Contains(url, `music.163.com`) { 67 | options.Headers[`Referer`] = `https://music.163.com` 68 | } 69 | ip := ztool.Str_Select(options.RealIP, options.IP, ``) 70 | if ip != `` { 71 | options.Headers[`X-Real-IP`] = ip 72 | options.Headers[`X-Forwarded-For`] = ip 73 | } 74 | if obj, ok := options.Cookie.(map[string]string); ok { 75 | obj[`__remember_me`] = `true` 76 | obj[`_ntes_nuid`] = zcypt.HexToString(zcypt.RandomBytes(16)) 77 | if !strings.Contains(url, `login`) { 78 | obj[`NMTID`] = zcypt.HexToString(zcypt.RandomBytes(16)) 79 | } 80 | if _, ok := obj[`MUSIC_U`]; !ok { 81 | // 游客 82 | if _, ok := obj[`MUSIC_A`]; !ok { 83 | obj[`MUSIC_A`] = anonymous_token 84 | if obj[`os`] == `` { 85 | obj[`os`] = `ios` 86 | } 87 | if obj[`appver`] == `` { 88 | obj[`appver`] = `8.20.21` 89 | } 90 | } 91 | } 92 | keys, i := make([]string, len(obj)), 0 93 | for k, v := range obj { 94 | keys[i] = ztool.Str_FastConcat( 95 | stdurl.QueryEscape(k), `=`, stdurl.QueryEscape(v), 96 | ) 97 | i++ 98 | } 99 | options.Headers[`Cookie`] = strings.Join(keys, `; `) 100 | options.Cookie = obj 101 | } else if str, ok := options.Cookie.(string); ok && str != `` { 102 | options.Headers[`Cookie`] = str 103 | } else { 104 | options.Headers[`Cookie`] = `__remember_me=true; NMTID=xxx` 105 | } 106 | var form stdurl.Values 107 | switch options.Crypto { 108 | case `weapi`: 109 | options.Headers[`User-Agent`] = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.69` 110 | csrfToken := csrfReg.FindStringSubmatch(options.Headers[`Cookie`]) 111 | if len(csrfToken) > 1 { 112 | data[`csrf_token`] = csrfToken[1] 113 | } else { 114 | data[`csrf_token`] = `` 115 | } 116 | form = weapi(data) 117 | // fmt.Println(form.Encode()) 118 | url = wapiReg.ReplaceAllString(url, `weapi`) 119 | case `linuxapi`: 120 | form = linuxapi(map[string]any{ 121 | `method`: method, 122 | `url`: wapiReg.ReplaceAllString(url, `weapi`), 123 | `params`: data, 124 | }) 125 | options.Headers[`User-Agent`] = `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36` 126 | url = `https://music.163.com/api/linux/forward` 127 | case `eapi`: 128 | cookie, ok := options.Cookie.(map[string]string) 129 | if !ok { 130 | cookie = make(map[string]string) 131 | } 132 | // csrfToken := cookie[`__csrf`] 133 | now := time.Now() 134 | reqid := strconv.Itoa(rand.Intn(1000)) 135 | header := map[string]string{ 136 | `osver`: ztool.Str_Select(cookie[`osver`], `17,1,2`), //系统版本 137 | `deviceId`: cookie[`deviceId`], //zcypt.Base64ToString(base64.StdEncoding, bytesconv.StringToBytes(imei+"'\t02:00:00:00:00:00\t5106025eb79a5247\t70ffbaac7'")) 138 | `appver`: ztool.Str_Select(cookie[`appver`], `8.9.70`), // app版本 139 | `versioncode`: ztool.Str_Select(cookie[`versioncode`], `140`), //版本号 140 | `mobilename`: cookie[`mobilename`], //设备model 141 | `buildver`: ztool.Str_Select(cookie[`buildver`], strconv.FormatInt(now.Unix(), 10)), 142 | `resolution`: ztool.Str_Select(cookie[`resolution`], `1920x1080`), //设备分辨率 143 | `__csrf`: cookie[`__csrf`], //csrfToken, 144 | `os`: ztool.Str_Select(cookie[`os`], `ios`), 145 | `channel`: cookie[`channel`], 146 | `requestId`: ztool.Str_FastConcat( 147 | strconv.FormatInt(now.UnixMilli(), 10), `_`, 148 | strings.Repeat(`0`, 4-len(reqid)), reqid, 149 | ), 150 | } 151 | if cookie[`MUSIC_U`] != `` { 152 | header[`MUSIC_U`] = cookie[`MUSIC_U`] 153 | } 154 | if cookie[`MUSIC_A`] != `` { 155 | header[`MUSIC_A`] = cookie[`MUSIC_A`] 156 | } 157 | keys, i := make([]string, len(header)), 0 158 | for k, v := range header { 159 | keys[i] = ztool.Str_FastConcat( 160 | stdurl.QueryEscape(k), `=`, stdurl.QueryEscape(v), 161 | ) 162 | i++ 163 | } 164 | options.Headers[`Cookie`] = strings.Join(keys, `; `) 165 | out, err := json.Marshal(header) 166 | if err != nil { 167 | panic(err) 168 | } 169 | data[`header`] = out //bytesconv.BytesToString(out) 170 | form = eapi(options.Url, data) 171 | url = wapiReg.ReplaceAllString(url, `eapi`) 172 | default: 173 | return nil, errors.New(`not support`) 174 | } 175 | // values := stdurl.Values{} 176 | // for k, v := range data { 177 | // values.Add(k, v) 178 | // } 179 | answer := ReqAnswer{Status: 500, Body: map[string]any{} /*, Cookie: []string{}*/} 180 | err := ztool.Net_Request(method, url, 181 | strings.NewReader(form.Encode()), 182 | []ztool.Net_ReqHandlerFunc{ 183 | ztool.Net_ReqAddHeader(options.Headers), 184 | }, 185 | []ztool.Net_ResHandlerFunc{ 186 | func(res *http.Response) error { 187 | body, err := io.ReadAll(res.Body) 188 | if err == nil { 189 | // fmt.Println(`body:`, string(body), "\nstr:", body) 190 | if len(body) == 0 { 191 | err = errors.New(`nil Body`) 192 | } 193 | if err == nil { 194 | answer.Cookie = res.Header[`Set-Cookie`] //res.Header.Values(`set-cookie`) 195 | for i, v := range answer.Cookie { 196 | answer.Cookie[i] = domaReg.ReplaceAllString(v, ``) 197 | } 198 | if options.Crypto == `eapi` && body[0] != '{' { 199 | err = json.Unmarshal(decrypt(body), &answer.Body) 200 | } else { 201 | err = json.Unmarshal(body, &answer.Body) 202 | } 203 | if code, ok := answer.Body[`code`].(string); ok { 204 | answer.Body[`code`], err = strconv.Atoi(code) 205 | } else { 206 | answer.Body[`code`] = res.StatusCode 207 | } 208 | if err == nil { 209 | if code, ok := answer.Body[`code`].(int); ok { 210 | if !ztool.Chk_IsMatchInt(code, 201, 302, 400, 502, 800, 801, 802, 803) { 211 | // 特殊状态码 212 | answer.Status = 200 213 | } 214 | } 215 | if answer.Status < 100 || answer.Status >= 600 { 216 | answer.Status = 400 217 | } 218 | if answer.Status != 200 { 219 | err = errors.New(strconv.Itoa(answer.Status)) 220 | } 221 | } 222 | } 223 | } 224 | if err != nil { 225 | answer.Status = 502 226 | answer.Body = map[string]any{`code`: 502, `msg`: err.Error()} 227 | // return err 228 | } 229 | return err // nil 230 | }, 231 | }, 232 | ) 233 | return &answer, err 234 | } 235 | -------------------------------------------------------------------------------- /src/sources/custom/wy/modules/core_types.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | type ( 4 | // 音乐URL 5 | PlayInfo struct { 6 | Data []struct { 7 | ID int `json:"id"` 8 | URL string `json:"url"` 9 | Br int `json:"br"` 10 | Size int `json:"size"` 11 | Md5 string `json:"md5"` 12 | Code int `json:"code"` 13 | Expi int `json:"expi"` 14 | Type string `json:"type"` 15 | Gain float64 `json:"gain"` 16 | Peak float64 `json:"peak"` 17 | Fee int `json:"fee"` 18 | Uf interface{} `json:"uf"` 19 | Payed int `json:"payed"` 20 | Flag int `json:"flag"` 21 | CanExtend bool `json:"canExtend"` 22 | FreeTrialInfo *struct { 23 | AlgData interface{} `json:"algData"` 24 | End int `json:"end"` 25 | FragmentType int `json:"fragmentType"` 26 | Start int `json:"start"` 27 | } `json:"freeTrialInfo"` 28 | Level string `json:"level"` 29 | EncodeType string `json:"encodeType"` 30 | FreeTrialPrivilege struct { 31 | ResConsumable bool `json:"resConsumable"` 32 | UserConsumable bool `json:"userConsumable"` 33 | ListenType int `json:"listenType"` 34 | CannotListenReason int `json:"cannotListenReason"` 35 | PlayReason interface{} `json:"playReason"` 36 | } `json:"freeTrialPrivilege"` 37 | FreeTimeTrialPrivilege struct { 38 | ResConsumable bool `json:"resConsumable"` 39 | UserConsumable bool `json:"userConsumable"` 40 | Type int `json:"type"` 41 | RemainTime int `json:"remainTime"` 42 | } `json:"freeTimeTrialPrivilege"` 43 | URLSource int `json:"urlSource"` 44 | RightSource int `json:"rightSource"` 45 | PodcastCtrp interface{} `json:"podcastCtrp"` 46 | EffectTypes interface{} `json:"effectTypes"` 47 | Time int `json:"time"` 48 | } `json:"data"` 49 | Code int `json:"code"` 50 | } 51 | // 音乐是否可用 52 | VerifyInfo struct { 53 | Code int16 `json:"code"` 54 | Success bool `json:"success"` 55 | Message string `json:"message"` 56 | } 57 | // 音质数据 58 | QualityData struct { 59 | Br int `json:"br"` // 比特率 Bit Rate 60 | Fid int `json:"fid"` // ? 61 | Size int `json:"size"` // 文件大小 62 | Vd float64 `json:"vd"` // Volume Delta 63 | Sr int `json:"sr"` // 采样率 Sample Rate 64 | } 65 | // 歌曲音质详情 66 | QualityDetail struct { 67 | Data struct { 68 | SongID int `json:"songId"` 69 | H QualityData `json:"h"` // 高质量文件信息 70 | M QualityData `json:"m"` // 中质量文件信息 71 | L QualityData `json:"l"` // 低质量文件信息 72 | Sq QualityData `json:"sq"` // 无损质量文件信息 73 | Hr QualityData `json:"hr"` // Hi-Res质量文件信息 74 | Db QualityData `json:"db"` // 杜比音质 75 | Jm QualityData `json:"jm"` // jymaster(超清母带) 76 | Je QualityData `json:"je"` // jyeffect(高清环绕声) 77 | Sk QualityData `json:"sk"` // sky(沉浸环绕声) 78 | } `json:"data"` 79 | Code int `json:"code"` 80 | Message string `json:"message"` 81 | Success bool `json:"success"` 82 | Error bool `json:"error"` 83 | } 84 | // 扫码登录请求 85 | QrKey struct { 86 | Code int `json:"code"` 87 | UniKey string `json:"unikey"` 88 | } 89 | // 扫码登录结果 90 | QrCheck struct { 91 | Code int `json:"code"` 92 | Message string `json:"message"` 93 | AvatarUrl string `json:"avatarUrl"` 94 | NickName string `json:"nickname"` 95 | } 96 | ) 97 | -------------------------------------------------------------------------------- /src/sources/custom/wy/modules/login_qr_check.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | import "net/http" 4 | 5 | // 二维码检测扫码状态接口 6 | func LoginQrCheck(key string) (*ReqAnswer, error) { 7 | res, err := createRequest( 8 | http.MethodPost, 9 | `https://music.163.com/weapi/login/qrcode/client/login`, 10 | map[string]any{ 11 | `key`: key, 12 | `type`: 1, 13 | }, 14 | reqOptions{ 15 | Crypto: `weapi`, 16 | }, 17 | ) 18 | return res, err 19 | } 20 | -------------------------------------------------------------------------------- /src/sources/custom/wy/modules/login_qr_create.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | // 二维码生成接口 4 | func LoginQrCreate(key string) string { 5 | return `https://music.163.com/login?codekey=` + key 6 | } 7 | -------------------------------------------------------------------------------- /src/sources/custom/wy/modules/login_qr_key.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | import "net/http" 4 | 5 | // 二维码 key 生成接口 6 | func LoginQrKey() (*ReqAnswer, error) { 7 | res, err := createRequest( 8 | http.MethodPost, 9 | `https://music.163.com/weapi/login/qrcode/unikey`, 10 | map[string]any{ 11 | `type`: 1, 12 | }, 13 | reqOptions{ 14 | Crypto: `weapi`, 15 | Cookie: nil, 16 | }, 17 | ) 18 | return res, err 19 | } 20 | -------------------------------------------------------------------------------- /src/sources/custom/wy/modules/login_refresh.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | // 登录刷新 9 | func LoginRefresh(query ReqQuery) (*ReqAnswer, error) { 10 | res, err := createRequest( 11 | http.MethodPost, 12 | `https://music.163.com/weapi/login/token/refresh`, 13 | map[string]any{}, 14 | reqOptions{ 15 | Crypto: `weapi`, 16 | UA: `pc`, 17 | Cookie: query.Cookie, 18 | RealIP: query.RealIP, 19 | }, 20 | ) 21 | if code, ok := res.Body[`code`].(int); ok && err == nil { 22 | if code == 200 { 23 | res.Body[`cookie`] = strings.Join(res.Cookie, `;`) 24 | } 25 | } 26 | return res, err 27 | } 28 | -------------------------------------------------------------------------------- /src/sources/custom/wy/modules/song_url.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | import ( 4 | "net/http" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/ZxwyWebSite/ztool" 10 | ) 11 | 12 | // type Query_song_url struct { 13 | // Cookie map[string]string 14 | // Ids string 15 | // Br string 16 | // RealIP string 17 | // } 18 | 19 | // 歌曲链接 20 | func SongUrl(query ReqQuery) (*ReqAnswer, error) { 21 | if query.Cookie == nil { 22 | query.Cookie = make(map[string]string) 23 | } 24 | query.Cookie[`os`] = `pc` 25 | if query.Br == `` { 26 | query.Br = `999000` 27 | } 28 | ids := strings.Split(query.Ids, `,`) 29 | // idj, err := json.Marshal(ids) 30 | // if err != nil { 31 | // return nil, err 32 | // } 33 | data := map[string]any{ 34 | `ids`: ztool.Str_FastConcat(`["`, strings.Join(ids, `","`), `"]`), //bytesconv.BytesToString(idj), //`["1998644237"]`, 35 | `br`: query.Br, //ztool.Str_Select(query.Br, `999000`), 36 | } 37 | res, err := createRequest( 38 | http.MethodPost, 39 | `https://interface3.music.163.com/eapi/song/enhance/player/url`, 40 | data, 41 | reqOptions{ 42 | Crypto: `eapi`, 43 | Cookie: query.Cookie, 44 | RealIP: query.RealIP, 45 | Url: `/api/song/enhance/player/url`, 46 | }, 47 | ) 48 | // 根据id排序 49 | if length := len(ids); length > 1 && err == nil { 50 | indexOf := make(map[string]int, length) 51 | for i := 0; i < length; i++ { 52 | indexOf[ids[i]] = i 53 | } 54 | if data, ok := res.Body[`data`].([]interface{}); ok { 55 | sort.SliceStable(data, func(a, b int) bool { 56 | da, oa := data[a].(map[string]interface{}) 57 | db, ob := data[b].(map[string]interface{}) 58 | if oa && ob { 59 | ia, ka := da[`id`].(float64) 60 | ib, kb := db[`id`].(float64) 61 | if ka && kb { 62 | ta := strconv.FormatInt(int64(ia), 10) 63 | tb := strconv.FormatInt(int64(ib), 10) 64 | return indexOf[ta] < indexOf[tb] 65 | } 66 | } 67 | return false 68 | }) 69 | res.Body[`data`] = data 70 | } 71 | } 72 | return res, err 73 | } 74 | -------------------------------------------------------------------------------- /src/sources/custom/wy/modules/song_url_v1.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/ZxwyWebSite/ztool" 7 | ) 8 | 9 | // 歌曲链接 - v1 10 | // 此版本不再采用 br 作为音质区分的标准 11 | // 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断 12 | func SongUrlV1(query ReqQuery) (*ReqAnswer, error) { 13 | if query.Cookie == nil { 14 | query.Cookie = make(map[string]string) 15 | } 16 | query.Cookie[`os`] = `android` 17 | query.Cookie[`appver`] = `8.10.05` 18 | data := map[string]any{ 19 | `ids`: ztool.Str_FastConcat(`[`, query.Ids, `]`), 20 | `level`: query.Level, 21 | `encodeType`: `flac`, 22 | } 23 | if query.Level == `sky` /*|| query.Level == `jysky`*/ { 24 | data[`immerseType`] = `c51` 25 | } 26 | return createRequest( 27 | http.MethodPost, 28 | `https://interface.music.163.com/eapi/song/enhance/player/url/v1`, 29 | data, 30 | reqOptions{ 31 | Crypto: `eapi`, 32 | Cookie: query.Cookie, 33 | RealIP: query.RealIP, 34 | Url: `/api/song/enhance/player/url/v1`, 35 | }, 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/sources/custom/wy/player.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | import ( 4 | "lx-source/src/env" 5 | "lx-source/src/sources" 6 | "lx-source/src/sources/custom/utils" 7 | wm "lx-source/src/sources/custom/wy/modules" 8 | "lx-source/src/sources/example" 9 | "net/http" 10 | "strconv" 11 | "sync" 12 | "time" 13 | 14 | "github.com/ZxwyWebSite/ztool" 15 | "github.com/ZxwyWebSite/ztool/x/cookie" 16 | ) 17 | 18 | var ( 19 | wy_pool = &sync.Pool{New: func() any { return new(wm.PlayInfo) }} 20 | // wv_pool *sync.Pool 21 | 22 | Url func(string, string) (string, string) 23 | ) 24 | 25 | func init() { 26 | env.Inits.Add(func() { 27 | loger := env.Loger.NewGroup(`WyInit`) 28 | switch env.Config.Custom.Wy_Mode { 29 | case `0`, `builtin`: 30 | loger.Debug(`use builtin`) 31 | // if env.Config.Source.MusicIdVerify { 32 | // wv_pool = &sync.Pool{New: func() any { return new(verifyInfo) }} 33 | // } 34 | Url = builtin 35 | case `1`, `163api`: 36 | if env.Config.Custom.Wy_Api_Cookie == `` { 37 | loger.Fatal(`使用163api且Cookie参数为空`) 38 | } 39 | switch env.Config.Custom.Wy_Api_Type { 40 | case `0`, `native`: 41 | loger.Debug(`use 163api module`) 42 | Url = nmModule 43 | case `1`, `remote`: 44 | loger.Debug(`use 163api custom`) 45 | if env.Config.Custom.Wy_Api_Address == `` { 46 | loger.Fatal(`自定义接口地址为空`) 47 | } 48 | if env.Config.Custom.Wy_Api_Address[len(env.Config.Custom.Wy_Api_Address)-1] != '/' { 49 | env.Config.Custom.Wy_Api_Address += "/" // 补全尾部斜杠 50 | } 51 | loger.Info(`使用自定义接口: %v`, env.Config.Custom.Wy_Api_Address) 52 | Url = nmCustom 53 | default: 54 | loger.Fatal(`未定义的调用方式,请检查配置 [Custom].Wy_Api_Type`) 55 | } 56 | default: 57 | loger.Fatal(`未定义的接口模式,请检查配置 [Custom].Wy_Mode`) 58 | } 59 | loger.Free() 60 | }) 61 | } 62 | 63 | func builtin(songMid, quality string) (ourl, msg string) { 64 | loger := env.Loger.NewGroup(`Wy`) 65 | defer loger.Free() 66 | rquality, ok := qualityMap[quality] 67 | if !ok { 68 | msg = sources.E_QNotSupport 69 | return 70 | } 71 | resp := wy_pool.Get().(*wm.PlayInfo) 72 | defer wy_pool.Put(resp) 73 | url := ztool.Str_FastConcat( 74 | `https://`, example.Api_wy, `&id=`, songMid, `&level=`, rquality, 75 | `×tamp=`, strconv.FormatInt(time.Now().UnixMilli(), 10), 76 | ) 77 | err := ztool.Net_Request( 78 | http.MethodGet, url, nil, 79 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeaders(example.Header_wy)}, 80 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&resp)}, 81 | ) 82 | if err != nil { 83 | loger.Error(`HttpReq: %s`, err) 84 | msg = sources.ErrHttpReq 85 | return 86 | } 87 | loger.Debug(`Resp: %+v`, resp) 88 | if len(resp.Data) == 0 { 89 | msg = `No Data:Api接口忙,请稍后重试` 90 | return 91 | } 92 | var data = resp.Data[0] 93 | if data.Code != 200 || data.FreeTrialInfo != nil { 94 | msg = `触发风控或专辑单独收费: ` + strconv.Itoa(data.Code) 95 | return 96 | } 97 | if data.Level != rquality { 98 | msg = ztool.Str_FastConcat(`实际音质不匹配: `, rquality, ` <= `, data.Level) 99 | if !env.Config.Source.ForceFallback { 100 | return 101 | } 102 | } 103 | ourl = data.URL 104 | return 105 | } 106 | 107 | func nmModule(songMid, quality string) (ourl, msg string) { 108 | loger := env.Loger.NewGroup(`Wy`) 109 | defer loger.Free() 110 | rquality, ok := qualityMap[quality] 111 | if !ok { 112 | msg = sources.E_QNotSupport 113 | return 114 | } 115 | cookies := cookie.Parse(env.Config.Custom.Wy_Api_Cookie) 116 | answer, err := wm.SongUrlV1(wm.ReqQuery{ 117 | Cookie: cookie.ToMap(cookies), 118 | Ids: songMid, 119 | // Br: rquality, 120 | Level: rquality, 121 | }) 122 | body := wy_pool.Get().(*wm.PlayInfo) 123 | defer wy_pool.Put(body) 124 | if err == nil { 125 | err = ztool.Val_MapToStruct(answer.Body, &body) 126 | } 127 | if err != nil { 128 | loger.Error(`SongUrl: %s`, err) 129 | msg = sources.ErrHttpReq 130 | return 131 | } 132 | loger.Debug(`Resp: %+v`, body) 133 | if len(body.Data) == 0 { 134 | msg = `No Data:无返回数据` 135 | return 136 | } 137 | data := body.Data[0] 138 | if data.Code != 200 { 139 | msg = `触发风控或专辑单独收费: ` + strconv.Itoa(data.Code) 140 | return 141 | } 142 | if data.Level != rquality { 143 | msg = ztool.Str_FastConcat(`实际音质不匹配: `, rquality, ` <= `, data.Level) 144 | if !env.Config.Source.ForceFallback { 145 | return 146 | } 147 | } 148 | // br := strconv.Itoa(data.Br) // 注:由于flac返回br值不固定,暂无法进行比较 149 | // if br != rquality && !ztool.Chk_IsMatch(br, sources.Q_flac, sources.Q_fl24) { 150 | // msg = sources.E_QNotMatch 151 | // return 152 | // } 153 | ourl = utils.DelQuery(data.URL) 154 | return 155 | } 156 | 157 | func nmCustom(songMid, quality string) (ourl, msg string) { 158 | loger := env.Loger.NewGroup(`Wy`) 159 | defer loger.Free() 160 | rquality, ok := qualityMap[quality] 161 | if !ok { 162 | msg = sources.E_QNotSupport 163 | return 164 | } 165 | body := wy_pool.Get().(*wm.PlayInfo) 166 | defer wy_pool.Put(body) 167 | err := ztool.Net_Request( 168 | http.MethodGet, 169 | ztool.Str_FastConcat( 170 | env.Config.Custom.Wy_Api_Address, `song/url/v1`, `?id=`, songMid, `&level=`, rquality, 171 | // `×tamp=`, strconv.FormatInt(time.Now().UnixMilli(), 10), 172 | ), nil, 173 | []ztool.Net_ReqHandlerFunc{ztool.Net_ReqAddHeaders(map[string]string{ 174 | `Cookie`: env.Config.Custom.Wy_Api_Cookie, 175 | })}, 176 | []ztool.Net_ResHandlerFunc{ztool.Net_ResToStruct(&body)}, 177 | ) 178 | if err != nil { 179 | loger.Error(`SongUrl: %s`, err) 180 | msg = sources.ErrHttpReq 181 | return 182 | } 183 | loger.Debug(`Resp: %+v`, body) 184 | if len(body.Data) == 0 { 185 | msg = `No Data:无返回数据` 186 | return 187 | } 188 | data := body.Data[0] 189 | if data.Code != 200 { 190 | msg = `触发风控或专辑单独收费: ` + strconv.Itoa(data.Code) 191 | return 192 | } 193 | if data.Level != rquality { 194 | msg = ztool.Str_FastConcat(`实际音质不匹配: `, rquality, ` <= `, data.Level) 195 | if !env.Config.Source.ForceFallback { 196 | return 197 | } 198 | } 199 | ourl = utils.DelQuery(data.URL) 200 | return 201 | } 202 | -------------------------------------------------------------------------------- /src/sources/custom/wy/refresh.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | import ( 4 | "lx-source/src/env" 5 | wy "lx-source/src/sources/custom/wy/modules" 6 | "time" 7 | 8 | // "time" 9 | 10 | "github.com/ZxwyWebSite/ztool/logs" 11 | "github.com/ZxwyWebSite/ztool/x/cookie" 12 | ) 13 | 14 | /* 15 | 刷新登录模块 (来自 NeteaseCloudMusicApi) 16 | 逻辑: 17 | 检测返回结果中是否含有"MUSIC_U": 18 | 如果有则为正常刷新,延时30天 19 | 否则延时1天 20 | 注: 21 | 原代码未提供详细描述,无法确定有效结果判断条件,暂时先这么写 22 | 2024-02-15: 23 | MUSIC_U 改变 则 6天 后 继续执行 24 | MUSIC_U 不变 则 1天 后 继续执行 25 | 原理: 26 | 听说隔壁某解析群的账号经常使用,Token快一年了也没过期, 27 | 所以模拟正常使用,每天调用一次刷新接口, 28 | 证明这个Token还在使用,类似于给它"续期", 29 | 就像SPlayer客户端一样,Cookie变了就合并, 30 | (这是我随便猜的,未经测试仅供参考) 31 | */ 32 | 33 | func refresh(loger *logs.Logger, now int64) error { 34 | // 前置检测 35 | // now := time.Now().Unix() //(执行时间已改为从参数获取) 36 | if now < env.Config.Custom.Wy_Refresh_Interval { 37 | loger.Debug(`Key未过期,跳过...`) 38 | return nil 39 | } 40 | // 刷新逻辑 41 | cookies := cookie.ToMap(cookie.Parse(env.Config.Custom.Wy_Api_Cookie)) 42 | res, err := wy.LoginRefresh(wy.ReqQuery{ 43 | Cookie: cookies, 44 | }) 45 | loger.Debug(`Resp: %+v`, res) 46 | if err == nil { 47 | if out, ok := res.Body[`cookie`].(string); ok { 48 | loger.Info(`获取数据成功`) 49 | cmap := cookie.ToMap(cookie.Parse(out)) 50 | // inline call to maps.Copy 51 | for k, v := range cmap { 52 | cookies[k] = v 53 | } 54 | env.Config.Custom.Wy_Api_Cookie = cookie.Marshal(cookies) 55 | loger.Debug(`Cookie: %#v`, cookies) 56 | // if _, ok := cmap[`MUSIC_U`]; ok { 57 | // // MUSIC_U 改变 则 6天 后 继续执行 58 | // env.Config.Custom.Wy_Refresh_Interval = now + 518400 //2147483647 - 86000 59 | // loger.Debug(`MUSIC_U 改变, 6天 后 继续执行`) 60 | // } else { 61 | // // MUSIC_U 不变 则 1天 后 继续执行 62 | // env.Config.Custom.Wy_Refresh_Interval = now + 86000 63 | // loger.Debug(`MUSIC_U 不变, 1天 后 继续执行`) //`未发现有效结果,将在下次检测时再次尝试` 64 | // } 65 | tnow := time.Now() 66 | env.Config.Custom.Wy_Refresh_Interval = time.Date(tnow.Year(), tnow.Month(), tnow.Day()+1, 0, 0, 0, 0, tnow.Location()).Unix() 67 | // env.Config.Custom.Wy_Refresh_Interval = now + 86000 68 | err = env.Cfg.Save(``) 69 | if err == nil { 70 | loger.Info(`配置更新成功`) 71 | } 72 | } 73 | } 74 | return err 75 | } 76 | 77 | func init() { 78 | env.Inits.Add(func() { 79 | if env.Config.Custom.Wy_Refresh_Enable && env.Config.Custom.Wy_Api_Cookie != `` { 80 | env.Tasker.Add(`wy_refresh`, refresh, 86000, true) 81 | } 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /src/sources/custom/wy/types.go: -------------------------------------------------------------------------------- 1 | package wy 2 | 3 | import "lx-source/src/sources" 4 | 5 | var ( 6 | // brMap = map[string]string{ 7 | // sources.Q_128k: `128000`, 8 | // sources.Q_320k: `320000`, 9 | // sources.Q_flac: `1000000`, //`743625`,`915752` 10 | // sources.Q_fl24: `2000000`, //`1453955`,`1683323` 11 | // } 12 | qualityMap = map[string]string{ 13 | sources.Q_128k: `standard`, 14 | sources.Q_320k: `exhigh`, 15 | sources.Q_flac: `lossless`, 16 | sources.Q_fl24: `hires`, 17 | 18 | sources.Q_dolby: `jyeffect`, 19 | sources.Q_sky: sources.Q_sky, 20 | sources.Q_master: `jymaster`, 21 | } 22 | // 优化:返回音质与查询音质相同,完全可以直接比较,不用多一步Reverse 23 | // qualityMapReverse = map[string]string{ 24 | // `standard`: sources.Q_128k, 25 | // `exhigh`: sources.Q_320k, 26 | // `lossless`: sources.Q_flac, 27 | // `hires`: sources.Q_fl24, 28 | // `jyeffect`: `dolby`, 29 | // `jysky`: `sky`, 30 | // `jymaster`: `master`, 31 | // } 32 | ) 33 | -------------------------------------------------------------------------------- /src/sources/example/data.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "github.com/ZxwyWebSite/ztool" 7 | "github.com/ZxwyWebSite/ztool/zcypt" 8 | ) 9 | 10 | var ( 11 | Api_wy string 12 | Api_mg string 13 | 14 | Vef_wy string 15 | 16 | Header_wy map[string]string 17 | Header_mg map[string]string 18 | ) 19 | 20 | func init() { 21 | // InitBuiltInData 22 | var initdata = struct { 23 | Api_Wy *string 24 | Api_Mg *string 25 | Vef_Wy *string 26 | Header_Wy *map[string]string 27 | Header_Mg *map[string]string 28 | }{ 29 | Api_Wy: &Api_wy, 30 | Api_Mg: &Api_mg, 31 | Vef_Wy: &Vef_wy, 32 | Header_Wy: &Header_wy, 33 | Header_Mg: &Header_mg, 34 | } 35 | data := []byte{0x53, 0x6e, 0x38, 0x44, 0x41, 0x51, 0x4c, 0x2f, 0x67, 0x41, 0x41, 0x42, 0x42, 0x51, 0x45, 0x47, 0x51, 0x58, 0x42, 0x70, 0x58, 0x31, 0x64, 0x35, 0x41, 0x51, 0x77, 0x41, 0x41, 0x51, 0x5a, 0x42, 0x63, 0x47, 0x6c, 0x66, 0x54, 0x57, 0x63, 0x42, 0x44, 0x41, 0x41, 0x42, 0x42, 0x6c, 0x5a, 0x6c, 0x5a, 0x6c, 0x39, 0x58, 0x65, 0x51, 0x45, 0x4d, 0x41, 0x41, 0x45, 0x4a, 0x53, 0x47, 0x56, 0x68, 0x5a, 0x47, 0x56, 0x79, 0x58, 0x31, 0x64, 0x35, 0x41, 0x66, 0x2b, 0x43, 0x41, 0x41, 0x45, 0x4a, 0x53, 0x47, 0x56, 0x68, 0x5a, 0x47, 0x56, 0x79, 0x58, 0x30, 0x31, 0x6e, 0x41, 0x66, 0x2b, 0x43, 0x41, 0x41, 0x41, 0x41, 0x49, 0x66, 0x2b, 0x42, 0x42, 0x41, 0x45, 0x42, 0x45, 0x57, 0x31, 0x68, 0x63, 0x46, 0x74, 0x7a, 0x64, 0x48, 0x4a, 0x70, 0x62, 0x6d, 0x64, 0x64, 0x63, 0x33, 0x52, 0x79, 0x61, 0x57, 0x35, 0x6e, 0x41, 0x66, 0x2b, 0x43, 0x41, 0x41, 0x45, 0x4d, 0x41, 0x51, 0x77, 0x41, 0x41, 0x50, 0x34, 0x42, 0x72, 0x76, 0x2b, 0x41, 0x41, 0x53, 0x52, 0x6a, 0x63, 0x32, 0x30, 0x75, 0x63, 0x32, 0x46, 0x35, 0x63, 0x58, 0x6f, 0x75, 0x59, 0x32, 0x39, 0x74, 0x4c, 0x32, 0x46, 0x77, 0x61, 0x53, 0x38, 0x2f, 0x64, 0x48, 0x6c, 0x77, 0x5a, 0x54, 0x31, 0x68, 0x63, 0x47, 0x6c, 0x54, 0x62, 0x32, 0x35, 0x6e, 0x56, 0x58, 0x4a, 0x73, 0x56, 0x6a, 0x45, 0x42, 0x4e, 0x6d, 0x30, 0x75, 0x62, 0x58, 0x56, 0x7a, 0x61, 0x57, 0x4d, 0x75, 0x62, 0x57, 0x6c, 0x6e, 0x64, 0x53, 0x35, 0x6a, 0x62, 0x69, 0x39, 0x74, 0x61, 0x57, 0x64, 0x31, 0x62, 0x58, 0x56, 0x7a, 0x61, 0x57, 0x4d, 0x76, 0x61, 0x44, 0x55, 0x76, 0x63, 0x47, 0x78, 0x68, 0x65, 0x53, 0x39, 0x68, 0x64, 0x58, 0x52, 0x6f, 0x4c, 0x32, 0x64, 0x6c, 0x64, 0x46, 0x4e, 0x76, 0x62, 0x6d, 0x64, 0x51, 0x62, 0x47, 0x46, 0x35, 0x53, 0x57, 0x35, 0x6d, 0x62, 0x77, 0x45, 0x6c, 0x59, 0x33, 0x4e, 0x74, 0x4c, 0x6e, 0x4e, 0x68, 0x65, 0x58, 0x46, 0x36, 0x4c, 0x6d, 0x4e, 0x76, 0x62, 0x53, 0x39, 0x68, 0x63, 0x47, 0x6b, 0x76, 0x50, 0x33, 0x52, 0x35, 0x63, 0x47, 0x55, 0x39, 0x59, 0x33, 0x4e, 0x74, 0x51, 0x32, 0x68, 0x6c, 0x59, 0x57, 0x74, 0x4e, 0x64, 0x58, 0x4e, 0x70, 0x59, 0x77, 0x45, 0x43, 0x43, 0x6c, 0x56, 0x7a, 0x5a, 0x58, 0x49, 0x74, 0x51, 0x57, 0x64, 0x6c, 0x62, 0x6e, 0x52, 0x65, 0x54, 0x57, 0x39, 0x36, 0x61, 0x57, 0x78, 0x73, 0x59, 0x53, 0x38, 0x31, 0x4c, 0x6a, 0x41, 0x67, 0x4b, 0x46, 0x64, 0x70, 0x62, 0x6d, 0x52, 0x76, 0x64, 0x33, 0x4d, 0x67, 0x54, 0x6c, 0x51, 0x67, 0x4e, 0x69, 0x34, 0x78, 0x4f, 0x79, 0x42, 0x58, 0x54, 0x31, 0x63, 0x32, 0x4e, 0x43, 0x6b, 0x67, 0x51, 0x58, 0x42, 0x77, 0x62, 0x47, 0x56, 0x58, 0x5a, 0x57, 0x4a, 0x4c, 0x61, 0x58, 0x51, 0x76, 0x4e, 0x54, 0x4d, 0x33, 0x4c, 0x6a, 0x4d, 0x32, 0x49, 0x43, 0x68, 0x4c, 0x53, 0x46, 0x52, 0x4e, 0x54, 0x43, 0x77, 0x67, 0x62, 0x47, 0x6c, 0x72, 0x5a, 0x53, 0x42, 0x48, 0x5a, 0x57, 0x4e, 0x72, 0x62, 0x79, 0x6b, 0x67, 0x51, 0x32, 0x68, 0x79, 0x62, 0x32, 0x31, 0x6c, 0x4c, 0x7a, 0x55, 0x77, 0x4c, 0x6a, 0x41, 0x75, 0x4d, 0x6a, 0x59, 0x32, 0x4d, 0x53, 0x34, 0x34, 0x4e, 0x78, 0x42, 0x59, 0x4c, 0x56, 0x4a, 0x6c, 0x63, 0x58, 0x56, 0x6c, 0x63, 0x33, 0x52, 0x6c, 0x5a, 0x43, 0x31, 0x58, 0x61, 0x58, 0x52, 0x6f, 0x44, 0x6c, 0x68, 0x4e, 0x54, 0x45, 0x68, 0x30, 0x64, 0x48, 0x42, 0x53, 0x5a, 0x58, 0x46, 0x31, 0x5a, 0x58, 0x4e, 0x30, 0x41, 0x51, 0x51, 0x48, 0x55, 0x6d, 0x56, 0x6d, 0x5a, 0x58, 0x4a, 0x6c, 0x63, 0x68, 0x74, 0x6f, 0x64, 0x48, 0x52, 0x77, 0x63, 0x7a, 0x6f, 0x76, 0x4c, 0x32, 0x30, 0x75, 0x62, 0x58, 0x56, 0x7a, 0x61, 0x57, 0x4d, 0x75, 0x62, 0x57, 0x6c, 0x6e, 0x64, 0x53, 0x35, 0x6a, 0x62, 0x69, 0x39, 0x32, 0x4e, 0x43, 0x38, 0x43, 0x51, 0x6e, 0x6b, 0x67, 0x4d, 0x44, 0x52, 0x6d, 0x4f, 0x44, 0x45, 0x30, 0x4e, 0x6a, 0x46, 0x68, 0x4f, 0x54, 0x68, 0x6a, 0x4e, 0x32, 0x46, 0x6d, 0x4e, 0x54, 0x55, 0x33, 0x5a, 0x6d, 0x56, 0x68, 0x4d, 0x32, 0x4e, 0x6d, 0x4d, 0x6a, 0x68, 0x6a, 0x4e, 0x47, 0x56, 0x68, 0x4d, 0x54, 0x55, 0x48, 0x59, 0x32, 0x68, 0x68, 0x62, 0x6d, 0x35, 0x6c, 0x62, 0x41, 0x63, 0x77, 0x4d, 0x54, 0x51, 0x77, 0x4d, 0x44, 0x42, 0x45, 0x42, 0x6b, 0x4e, 0x76, 0x62, 0x32, 0x74, 0x70, 0x5a, 0x54, 0x68, 0x54, 0x52, 0x56, 0x4e, 0x54, 0x53, 0x55, 0x39, 0x4f, 0x50, 0x56, 0x70, 0x55, 0x53, 0x58, 0x64, 0x50, 0x52, 0x47, 0x74, 0x35, 0x54, 0x55, 0x52, 0x52, 0x64, 0x45, 0x39, 0x55, 0x52, 0x54, 0x46, 0x4f, 0x55, 0x7a, 0x41, 0x77, 0x54, 0x55, 0x52, 0x6f, 0x62, 0x45, 0x78, 0x55, 0x61, 0x47, 0x68, 0x4e, 0x56, 0x30, 0x56, 0x30, 0x54, 0x57, 0x70, 0x52, 0x4d, 0x45, 0x34, 0x79, 0x57, 0x54, 0x4a, 0x4e, 0x65, 0x6d, 0x73, 0x79, 0x54, 0x31, 0x52, 0x42, 0x65, 0x67, 0x41, 0x3d} 36 | dec, _ := zcypt.Base64Decode(base64.StdEncoding, data) 37 | ztool.Val_GobDecode(dec, &initdata) 38 | } 39 | -------------------------------------------------------------------------------- /src/sources/source.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | // import ( 4 | // "lx-source/src/caches" 5 | // ) 6 | 7 | // var Loger = env.Loger.NewGroup(`Sources`) // JieXiApis 8 | 9 | const ( 10 | Err_Verify = `Verify Failed` 11 | // 通用音质 12 | Q_128k = `128k` 13 | Q_320k = `320k` 14 | Q_flac = `flac` 15 | Q_fl24 = `flac24bit` 16 | // 扩展音质 17 | Q_dolby = `dolby` 18 | Q_sky = `sky` 19 | Q_master = `master` 20 | // 文件扩展 21 | // X_aac = `aac` 22 | X_mp3 = `mp3` 23 | // X_flac = Q_flac 24 | // 通用平台 25 | S_wy = `wy` // 小芸 26 | S_mg = `mg` // 小蜜 27 | S_kw = `kw` // 小蜗 28 | S_kg = `kg` // 小枸 29 | S_tx = `tx` // 小秋 30 | S_lx = `lx` // 小洛 (预留) 31 | // 常用错误 32 | E_QNotSupport = `不支持的音质` 33 | E_QNotMatch = `实际音质不匹配` 34 | E_NoLink = `无法获取音乐链接` 35 | E_VefMusicId = `音乐ID校验失败` 36 | // 内置错误 37 | ErrHttpReq = `无法连接解析接口` 38 | ErrNoLink = `无法获取试听链接` 39 | ErrDisable = `该音乐源已被禁用` 40 | // 缓存时间 41 | C_tx = 80400 // 不知道tx为什么要取一个这么不对劲的数字当过期时长 42 | C_kg = 24 * 60 * 60 // 24 hours 43 | C_kw = 60 * 60 // 60 minutes 44 | C_wy = 20 * 60 // 20 minutes 45 | C_mg = C_kg // 永久直链但最多缓存1天 46 | C_lx = 12 * 60 * 60 // 本地默认缓存12小时 47 | ) 48 | 49 | const ( 50 | I_wy = iota 51 | I_mg 52 | I_kw 53 | I_kg 54 | I_tx 55 | I_lx 56 | ) 57 | 58 | var ( 59 | S_al = []string{S_wy, S_mg, S_kw, S_kg, S_tx, S_lx} // 全部平台 60 | ) 61 | 62 | // 源查询接口 63 | /* 64 | Origin: 65 | 首先调用Verify验证源是否支持 66 | 再尝试查询缓存 67 | 无缓存则解析链接 68 | 69 | 参考Python版: 70 | 不验证当前源是否支持,直接查询缓存 71 | 验证部分放到GetLink里 72 | 73 | */ 74 | // type Source interface { 75 | // Verify(*caches.Query) (string, bool) // 验证是否可用 <查询参数> 76 | // GetLink(*caches.Query) (string, string) // 查询获取链接 <查询参数> <链接,信息> 77 | // } 78 | 79 | // 默认空接口 80 | // type NullSource struct{} 81 | 82 | // func (*NullSource) Verify(*caches.Query) (string, bool) { return ``, false } 83 | // func (*NullSource) GetLink(*caches.Query) (string, string) { return ``, `NullSource` } 84 | 85 | // var UseSource Source = &NullSource{} // = &builtin.Source{} 86 | 87 | // 统一错误 88 | // type ( 89 | // ErrDef struct { 90 | // Typ string 91 | // Msg string 92 | // } 93 | // ErrQul struct { 94 | // Need string 95 | // Real string 96 | // } 97 | // ) 98 | 99 | // func (e *ErrDef) Error() string { 100 | // return ztool.Str_FastConcat(e.Typ, `: `, e.Msg) 101 | // } 102 | // func (e *ErrQul) Error() string { 103 | // return ztool.Str_FastConcat(`实际音质不匹配: `, e.Need, ` <= `, e.Real) 104 | // } 105 | 106 | // 验证失败(Verify Failed) 107 | // func ErrVerify(msg string) error { 108 | // return &ErrDef{ 109 | // Typ: Err_Verify, 110 | // Msg: msg, 111 | // } 112 | // } 113 | 114 | // 实际音质不匹配 115 | // func ErrQuality(need, real string) error { 116 | // return &ErrQul{ 117 | // Need: need, 118 | // Real: real, 119 | // } 120 | // } 121 | 122 | // 无返回数据(No Data) 123 | // func ErrNoData() error { 124 | // return &ErrDef{ 125 | // Typ: `No Data`, 126 | // Msg: ``, 127 | // } 128 | // } 129 | -------------------------------------------------------------------------------- /src/sources/types.go: -------------------------------------------------------------------------------- 1 | //go:build extapp 2 | 3 | package sources 4 | 5 | // MusicFree 数据结构 6 | type ( 7 | // 其他 8 | IExtra map[string]interface{} 9 | // 音乐 10 | IMusicItem struct { 11 | Artist string `json:"artist"` // 作者 12 | Title string `json:"title"` // 歌曲标题 13 | Duration int `json:"duration,omitempty"` // 时长(s) 14 | Album string `json:"album,omitempty"` // 专辑名 15 | Artwork string `json:"artwork,omitempty"` // 专辑封面图 16 | Url string `json:"url,omitempty"` // 默认音源 17 | Lrc string `json:"lrc,omitempty"` // 歌词URL 18 | RawLrc string `json:"rawLrc,omitempty"` // 歌词文本(lrc格式 带时间戳) 19 | Other IExtra `json:"extra,omitempty"` // 其他 20 | } 21 | // 歌单 22 | IMusicSheetItem struct { 23 | Artwork string `json:"artwork,omitempty"` // 封面图 24 | Title string `json:"title"` // 标题 25 | Description string `json:"description,omitempty"` // 描述 26 | WorksNum int `json:"worksNum,omitempty"` // 作品总数 27 | PlayCount int `json:"playCount,omitempty"` // 播放次数 28 | MusicList []IMusicItem `json:"musicList,omitempty"` // 播放列表 29 | CreateAt int64 `json:"createAt,omitempty"` // 歌单创建日期 30 | Artist string `json:"artist,omitempty"` // 歌单作者 31 | Other IExtra `json:"extra,omitempty"` // 其他 32 | } 33 | // 专辑 34 | IAlbumItem IMusicSheetItem 35 | // 作者 36 | IArtistItem struct { 37 | Platform string `json:"platform,omitempty"` // 插件名 38 | ID interface{} `json:"id"` // 唯一id 39 | Name string `json:"name"` // 姓名 40 | Fans int `json:"fans,omitempty"` // 粉丝数 41 | Description string `json:"description,omitempty"` // 简介 42 | Avatar string `json:"avatar,omitempty"` // 头像 43 | WorksNum int `json:"worksNum,omitempty"` // 作品数目 44 | MusicList []IMusicItem `json:"musicList,omitempty"` // 作者的音乐列表 45 | AlbumList []IAlbumItem `json:"albumList,omitempty"` // 作者的专辑列表 46 | Other IExtra `json:"extra,omitempty"` // 其他 47 | } 48 | ) 49 | --------------------------------------------------------------------------------