├── .gitignore
├── LICENSE
├── README.md
├── README_EN.md
├── README_KO.md
├── go.mod
├── go.sum
├── handler
├── music.go
└── router.go
├── logic
├── neteasy.go
├── neteasy_test.go
├── qishuimusic.go
├── qqmusic.go
└── qqmusic_test.go
├── main.go
├── misc
├── .DS_Store
├── httputil
│ └── http.go
├── images
│ ├── 0.png
│ ├── 1.png
│ └── approve.png
├── log
│ └── log.go
├── models
│ ├── common.go
│ ├── db.go
│ ├── neteasy.go
│ ├── qqmusic.go
│ └── result.go
├── test
│ └── test_test.go
└── utils
│ ├── music.go
│ ├── qqmusic_encrypt.js
│ ├── qqmusic_sign.go
│ ├── qqmusic_sign_native.go
│ └── qqmusic_sign_test.go
├── repo
├── cache
│ ├── redis.go
│ └── redis_test.go
└── db
│ ├── mysql.go
│ └── mysql_test.go
└── static
├── README.md
├── babel.config.js
├── jsconfig.json
├── package.json
├── public
└── index.html
├── src
├── App.vue
├── assets
│ └── approve.png
├── main.js
└── utils
│ ├── tip.js
│ └── utils.js
└── vue.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/git_toolbox_prj.xml
2 | repo/cache/redis.go
3 | repo/cache/redis.go
4 | .idea/inspectionProfiles/Project_Default.xml
5 | common/.DS_Store
6 | repo/cache/redis.go
7 | GoMusic
8 | *.meta
9 | *.xml
10 | *.iml
11 | .DS_Store
12 | static/dist
13 | static/node_modules
14 | static/package-lock.json
15 | static/pnpm-lock.yaml
16 |
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 The Algorithms
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 迁移网易云/汽水/QQ音乐歌单至 Apple/Youtube/Spotify Music
2 |
3 | > 简体中文| [English](README_EN.md) | [한국어](README_KO.md)
4 |
5 | 链接:https://music.unmeta.cn
6 |
7 | 项目后端使用 Golang + Gin 开发,前端使用 Vue + ElementUI 编写。
8 |
9 |
10 |
11 | # 使用指南
12 |
13 | > 如何迁移至 Apple/Youtube/Spotify Music
14 |
15 | 1. 输入歌单链接,如:http://163cn.tv/zoIxm3
16 | 2. 复制查询结果
17 | 3. 打开 **[TunemyMusic](https://www.tunemymusic.com/zh-CN/transfer)** or **[Spotlistr](https://spotlistr.com/)** 网站
18 | 4. 选择歌单来源“任意文本”,将刚刚复制的歌单粘贴进去,选择 Apple/Youtube/Spotify Music 作为目的地,确认迁移
19 |
20 |
21 |
22 | > 如何迁移至网易云/QQ音乐
23 |
24 | 见 GitHub issue:https://github.com/Bistutu/GoMusic/issues/17
25 |
26 | # 如何启动程序?
27 |
28 | - 安装 Golang
29 | - 将程序克隆至本地
30 | - 编译并运行
31 |
32 | ```shell
33 | git clone https://github.com/Bistutu/GoMusic.git
34 | cd GoMusic
35 | go build &&./GoMusic
36 | ```
37 |
38 | ## Star 历史记录
39 |
40 | [](https://star-history.com/#Bistutu/GoMusic&Date)
41 |
42 | # 赞赏码
43 |
44 | 网站免费、开源、保持简单,如果你想支持作者,请使用微信扫描赞赏码,以下是赞赏榜的前10名赞助者(最后更新 2025.2.26)
45 |
46 |
47 |
48 | | 序号 | 🌼赞助者🌼 | 赞助金额 |
49 | | :--: | :--------------------: | :------: |
50 | | 1 | 不疯就行 | 100 |
51 | | 2 | 什么长发及腰不如短发凉 | 87 |
52 | | 3 | Youyo🍊 | 66 |
53 | | 4 | 安分wa | 50 |
54 | | 5 | 高小伦 | 50 |
55 | | 6 | 平 | 30 |
56 | | 7 | 匿名用户 | 30 |
57 | | 8 | 迷失了就不酷了 | 30 |
58 | | 9 | Ember Celica | 20 |
59 | | 10 | 廿四味 | 20 |
60 | | ... | … | … |
61 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 | # Transfer NetEase/QQ Music Playlists to Apple/Youtube/Spotify Music
2 |
3 | > [简体中文](README.md)| English | [한국어](README_KO.md)
4 |
5 | Link: [https://music.unmeta.cn](https://music.unmeta.cn)
6 |
7 | This project's backend is developed using Golang + Gin, and the frontend is built with Vue + ElementUI.
8 |
9 |
10 |
11 | # User Guide
12 |
13 | 1. Enter the playlist link, for example: http://163cn.tv/zoIxm3
14 | 2. Copy the search results.
15 | 3. Open the **[TunemyMusic](https://www.tunemymusic.com/transfer)** website.
16 | 4. Choose "Any Text" as the playlist source, paste the copied playlist, select Apple/Youtube/Spotify Music as the destination, and confirm the transfer.
17 |
18 |
19 |
20 | # How to Start the Program?
21 |
22 | - Install Golang.
23 | - Clone the program to your local machine.
24 | - Compile and run.
25 |
26 | ```shell
27 | git clone https://github.com/Bistutu/GoMusic.git
28 | cd GoMusic
29 | go build && ./GoMusic
30 | ```
31 |
32 | ## Star History Record
33 |
34 | [](https://star-history.com/#Bistutu/GoMusic&Date)
--------------------------------------------------------------------------------
/README_KO.md:
--------------------------------------------------------------------------------
1 | # 넷이즈/큐큐 음악 플레이리스트를 Apple/Youtube/Spotify Music으로 이전하기
2 |
3 | > [简体中文](README.md) | [English](README_EN.md) | 한국어
4 |
5 | 링크: [https://music.unmeta.cn](https://music.unmeta.cn)
6 |
7 | 이 프로젝트의 백엔드는 Golang + Gin을 사용하여 개발되었고, 프론트엔드는 Vue + ElementUI로 구축되었습니다.
8 |
9 |
10 |
11 | # 사용 안내
12 |
13 | 1. 플레이리스트 링크 입력, 예: http://163cn.tv/zoIxm3
14 | 2. 검색 결과 복사
15 | 3. **[TunemyMusic](https://www.tunemymusic.com/ko/transfer)** 웹사이트 열기
16 | 4. 플레이리스트 출처로 "임의의 텍스트" 선택, 방금 복사한 플레이리스트 붙여넣기, 목적지로 Apple/Youtube/Spotify Music 선택 후 이전 확인
17 |
18 |
19 |
20 | # 프로그램 시작 방법?
21 |
22 | - Golang 설치
23 | - 프로그램을 로컬로 클론
24 | - 컴파일 후 실행
25 |
26 | ```shell
27 | git clone https://github.com/Bistutu/GoMusic.git
28 | cd GoMusic
29 | go build && ./GoMusic
30 | ```
31 |
32 | ## 스타 이력 기록
33 |
34 | [](https://star-history.com/#Bistutu/GoMusic&Date)
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module GoMusic
2 |
3 | go 1.23
4 |
5 | toolchain go1.23.4
6 |
7 | require (
8 | github.com/gin-contrib/cors v1.4.0
9 | github.com/gin-gonic/gin v1.9.1
10 | github.com/go-redis/redis/v8 v8.11.5
11 | github.com/go-sql-driver/mysql v1.7.1
12 | github.com/robertkrimen/otto v0.2.1
13 | github.com/stretchr/testify v1.8.4
14 | go.uber.org/zap v1.26.0
15 | golang.org/x/sync v0.8.0
16 | )
17 |
18 | require (
19 | github.com/PuerkitoBio/goquery v1.10.0 // indirect
20 | github.com/andybalholm/cascadia v1.3.2 // indirect
21 | github.com/bits-and-blooms/bitset v1.11.0 // indirect
22 | github.com/bits-and-blooms/bloom/v3 v3.6.0 // indirect
23 | github.com/bytedance/sonic v1.9.1 // indirect
24 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
25 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
27 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
28 | github.com/fsnotify/fsnotify v1.6.0 // indirect
29 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect
30 | github.com/gin-contrib/sse v0.1.0 // indirect
31 | github.com/go-playground/locales v0.14.1 // indirect
32 | github.com/go-playground/universal-translator v0.18.1 // indirect
33 | github.com/go-playground/validator/v10 v10.14.0 // indirect
34 | github.com/goccy/go-json v0.10.2 // indirect
35 | github.com/google/go-cmp v0.5.9 // indirect
36 | github.com/jinzhu/inflection v1.0.0 // indirect
37 | github.com/jinzhu/now v1.1.5 // indirect
38 | github.com/json-iterator/go v1.1.12 // indirect
39 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect
40 | github.com/kr/pretty v0.3.1 // indirect
41 | github.com/leodido/go-urn v1.2.4 // indirect
42 | github.com/mattn/go-isatty v0.0.19 // indirect
43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
44 | github.com/modern-go/reflect2 v1.0.2 // indirect
45 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect
46 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
47 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
48 | github.com/ugorji/go/codec v1.2.11 // indirect
49 | go.uber.org/multierr v1.10.0 // indirect
50 | golang.org/x/arch v0.3.0 // indirect
51 | golang.org/x/crypto v0.27.0 // indirect
52 | golang.org/x/net v0.29.0 // indirect
53 | golang.org/x/sys v0.25.0 // indirect
54 | golang.org/x/text v0.18.0 // indirect
55 | google.golang.org/protobuf v1.31.0 // indirect
56 | gopkg.in/sourcemap.v1 v1.0.5 // indirect
57 | gopkg.in/yaml.v3 v3.0.1 // indirect
58 | gorm.io/driver/mysql v1.5.2 // indirect
59 | gorm.io/gorm v1.25.5 // indirect
60 | )
61 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
2 | github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
3 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
4 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
5 | github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
6 | github.com/bits-and-blooms/bitset v1.11.0 h1:RMyy2mBBShArUAhfVRZJ2xyBO58KCBCtZFShw3umo6k=
7 | github.com/bits-and-blooms/bitset v1.11.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
8 | github.com/bits-and-blooms/bloom/v3 v3.6.0 h1:dTU0OVLJSoOhz9m68FTXMFfA39nR8U/nTCs1zb26mOI=
9 | github.com/bits-and-blooms/bloom/v3 v3.6.0/go.mod h1:VKlUSvp0lFIYqxJjzdnSsZEw4iHb1kOL2tfHTgyJBHg=
10 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
11 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
12 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
13 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
14 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
15 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
16 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
17 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
18 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
19 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
21 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
22 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
23 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
24 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
25 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
26 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
27 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
28 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
29 | github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
30 | github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
31 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
32 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
33 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
34 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
35 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
36 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
37 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
38 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
39 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
40 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
41 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
42 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
43 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
44 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
45 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
46 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
47 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
48 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
49 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
50 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
51 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
52 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
53 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
54 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
55 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
56 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
57 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
58 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
59 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
60 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
61 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
62 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
63 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
64 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
65 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
66 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
67 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
68 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
69 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
70 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
71 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
72 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
73 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
74 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
75 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
76 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
77 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
78 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
79 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
80 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
81 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
82 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
83 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
84 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
85 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
86 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
87 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
88 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
89 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
90 | github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
91 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
92 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
93 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
94 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
95 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
96 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
97 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
98 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
99 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
100 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
101 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
102 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
103 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
104 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
105 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
106 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
107 | github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
108 | github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
109 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
110 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
111 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
112 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
113 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
114 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
115 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
116 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
117 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
118 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
119 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
120 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
121 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
122 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
123 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
124 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
125 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
126 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
127 | github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
128 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
129 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
130 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
131 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
132 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
133 | go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
134 | go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
135 | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
136 | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
137 | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
138 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
139 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
140 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
141 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
142 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
143 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
144 | golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
145 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
146 | golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
147 | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
148 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
149 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
150 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
151 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
152 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
153 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
154 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
155 | golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
156 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
157 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
158 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
159 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
160 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
161 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
162 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
163 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
164 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
165 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
166 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
167 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
168 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
169 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
170 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
171 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
172 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
173 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
174 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
175 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
176 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
177 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
178 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
179 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
180 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
181 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
182 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
183 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
184 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
185 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
186 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
187 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
188 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
189 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
190 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
191 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
192 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
193 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
194 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
195 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
196 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
197 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
198 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
199 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
200 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
201 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
202 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
203 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
204 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
205 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
206 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
207 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
208 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
209 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
210 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
211 | gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
212 | gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
213 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
214 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
215 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
216 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
217 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
218 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
219 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
220 | gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
221 | gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
222 | gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
223 | gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
224 | gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
225 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
226 |
--------------------------------------------------------------------------------
/handler/music.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "net/http"
5 | "regexp"
6 | "strings"
7 | "sync/atomic"
8 |
9 | "GoMusic/logic"
10 | "GoMusic/misc/log"
11 | "GoMusic/misc/models"
12 |
13 | "github.com/gin-gonic/gin"
14 | )
15 |
16 | const (
17 | netEasy = `(163cn)|(\.163\.)`
18 | qqMusic = `.qq.`
19 | qishuiMusic = `(qishui)|(douyin)`
20 | SUCCESS = "success"
21 | )
22 |
23 | var (
24 | netEasyRegx, _ = regexp.Compile(netEasy)
25 | qqMusicRegx, _ = regexp.Compile(qqMusic)
26 | qishuiMusicRegx, _ = regexp.Compile(qishuiMusic)
27 | counter atomic.Int64 // request counter
28 | )
29 |
30 | // MusicHandler 处理音乐请求的入口函数
31 | func MusicHandler(c *gin.Context) {
32 | link := c.PostForm("url")
33 | detailed := c.Query("detailed") == "true"
34 | format := c.Query("format")
35 | currentCount := counter.Add(1)
36 |
37 | log.Infof("第 %v 次歌单请求:%v,详细歌曲名:%v,歌曲格式:%v", currentCount, link, detailed, format)
38 |
39 | // 路由到不同的音乐服务处理函数
40 | switch {
41 | case netEasyRegx.MatchString(link):
42 | handleNetEasyMusic(c, link, detailed, format)
43 | case qqMusicRegx.MatchString(link):
44 | handleQQMusic(c, link, detailed, format)
45 | case qishuiMusicRegx.MatchString(link):
46 | handleQiShuiMusic(c, link, detailed, format)
47 | default:
48 | log.Warnf("不支持的音乐链接格式: %s", link)
49 | c.JSON(http.StatusBadRequest, &models.Result{Code: models.FailureCode, Msg: "不支持的音乐链接格式", Data: nil})
50 | }
51 | }
52 |
53 | // handleNetEasyMusic 处理网易云音乐歌单
54 | func handleNetEasyMusic(c *gin.Context, link string, detailed bool, format string) {
55 | songList, err := logic.NetEasyDiscover(link, detailed)
56 | if err != nil {
57 | if strings.Contains(err.Error(), "无权限访问该歌单") {
58 | log.Errorf("获取歌单失败,无权限访问: %v", link)
59 | } else {
60 | log.Errorf("获取歌单失败: %v", err)
61 | }
62 | c.JSON(http.StatusBadRequest, &models.Result{Code: models.FailureCode, Msg: err.Error(), Data: nil})
63 | return
64 | }
65 |
66 | // 根据格式选项处理歌曲列表
67 | formatSongList(songList, format)
68 |
69 | c.JSON(http.StatusOK, &models.Result{Code: models.SuccessCode, Msg: SUCCESS, Data: songList})
70 | }
71 |
72 | // handleQQMusic 处理QQ音乐歌单
73 | func handleQQMusic(c *gin.Context, link string, detailed bool, format string) {
74 | if link == "https://i.y.qq.com/v8/playsong.html" {
75 | c.JSON(http.StatusBadRequest, &models.Result{Code: models.FailureCode, Msg: "无效歌单链接,请检查是否正确", Data: nil})
76 | return
77 | }
78 |
79 | songList, err := logic.QQMusicDiscover(link, detailed)
80 | if err != nil {
81 | log.Errorf("获取歌单失败: %v", err)
82 | c.JSON(http.StatusBadRequest, &models.Result{Code: models.FailureCode, Msg: err.Error(), Data: nil})
83 | return
84 | }
85 |
86 | // 根据格式选项处理歌曲列表
87 | formatSongList(songList, format)
88 |
89 | c.JSON(http.StatusOK, &models.Result{Code: models.SuccessCode, Msg: SUCCESS, Data: songList})
90 | }
91 |
92 | // handleQiShuiMusic 处理汽水音乐歌单
93 | func handleQiShuiMusic(c *gin.Context, link string, detailed bool, format string) {
94 | songList, err := logic.QiShuiMusicDiscover(link, detailed)
95 | if err != nil {
96 | log.Errorf("获取汽水音乐歌单失败: %v", err)
97 | c.JSON(http.StatusBadRequest, &models.Result{Code: models.FailureCode, Msg: err.Error(), Data: nil})
98 | return
99 | }
100 |
101 | // 根据格式选项处理歌曲列表
102 | formatSongList(songList, format)
103 |
104 | c.JSON(http.StatusOK, &models.Result{Code: models.SuccessCode, Msg: SUCCESS, Data: songList})
105 | }
106 |
107 | // formatSongList 根据指定的格式处理歌曲列表
108 | func formatSongList(songList *models.SongList, format string) {
109 | if songList == nil || len(songList.Songs) == 0 {
110 | return
111 | }
112 |
113 | // 如果没有指定格式或格式为默认的"歌名-歌手",则不做处理
114 | if format == "" || format == "song-singer" {
115 | return
116 | }
117 |
118 | formattedSongs := make([]string, 0, len(songList.Songs))
119 |
120 | for _, song := range songList.Songs {
121 | switch format {
122 | case "singer-song":
123 | // 将"歌名 - 歌手"转换为"歌手 - 歌名"
124 | parts := strings.Split(song, " - ")
125 | if len(parts) == 2 {
126 | formattedSongs = append(formattedSongs, parts[1]+" - "+parts[0])
127 | } else {
128 | // 如果格式不符合预期,保持原样
129 | formattedSongs = append(formattedSongs, song)
130 | }
131 | case "song":
132 | // 只保留歌名
133 | parts := strings.Split(song, " - ")
134 | if len(parts) > 0 {
135 | formattedSongs = append(formattedSongs, parts[0])
136 | } else {
137 | formattedSongs = append(formattedSongs, song)
138 | }
139 | default:
140 | // 未知格式,保持原样
141 | formattedSongs = append(formattedSongs, song)
142 | }
143 | }
144 |
145 | // 更新歌曲列表
146 | songList.Songs = formattedSongs
147 | }
148 |
--------------------------------------------------------------------------------
/handler/router.go:
--------------------------------------------------------------------------------
1 | package handler
2 |
3 | import (
4 | "github.com/gin-contrib/cors"
5 | "github.com/gin-gonic/gin"
6 | )
7 |
8 | func NewRouter() *gin.Engine {
9 | router := gin.Default()
10 | router.Use(cors.Default()) // allow all origins
11 | router.StaticFile("/", "./static") // load static files
12 | router.POST("/songlist", MusicHandler) // bind route to handler
13 | return router
14 | }
15 |
--------------------------------------------------------------------------------
/logic/neteasy.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "strings"
9 | "sync"
10 |
11 | "golang.org/x/sync/errgroup"
12 |
13 | "GoMusic/misc/httputil"
14 | "GoMusic/misc/log"
15 | "GoMusic/misc/utils"
16 | "GoMusic/repo/db"
17 |
18 | "GoMusic/misc/models"
19 | "GoMusic/repo/cache"
20 | )
21 |
22 | const (
23 | netEasyRedis = "net:%v"
24 | netEasyUrlV6 = "https://music.163.com/api/v6/playlist/detail"
25 | netEasyUrlV3 = "https://music.163.com/api/v3/song/detail"
26 | chunkSize = 400
27 | )
28 |
29 | // NetEasyDiscover 获取网易云音乐歌单信息
30 | // link: 歌单链接
31 | // detailed: 是否使用详细歌曲名(原始歌曲名,不去除括号等内容)
32 | func NetEasyDiscover(link string, detailed bool) (*models.SongList, error) {
33 | // 1. 获取歌单基本信息
34 | songIdsResp, err := getSongsInfo(link)
35 | if err != nil {
36 | return nil, fmt.Errorf("获取歌单信息失败: %w", err)
37 | }
38 |
39 | playlistName := songIdsResp.Playlist.Name // 歌单名
40 | trackIds := songIdsResp.Playlist.TrackIds // 歌曲ID列表
41 | tracksCount := songIdsResp.Playlist.TrackCount // 歌曲总数
42 |
43 | // 如果歌单为空,直接返回
44 | if len(trackIds) == 0 {
45 | return &models.SongList{
46 | Name: playlistName,
47 | Songs: []string{},
48 | SongsCount: 0,
49 | }, nil
50 | }
51 |
52 | // 详细模式下,直接从API获取所有歌曲信息,不走缓存和数据库
53 | if detailed {
54 | log.Infof("详细模式:直接从网易云API获取歌曲信息: %v", link)
55 | // 收集所有歌曲ID
56 | allSongIds := make([]uint, len(trackIds))
57 | for i, track := range trackIds {
58 | allSongIds[i] = track.Id
59 | }
60 |
61 | // 存储歌曲信息的结果集
62 | resultMap := sync.Map{}
63 |
64 | // 直接从API获取所有歌曲信息
65 | // 注意:在详细模式下,我们不会将获取的数据写入缓存和数据库
66 | allSongIdsSlice := make([]*models.SongId, len(allSongIds))
67 | for i, id := range allSongIds {
68 | allSongIdsSlice[i] = &models.SongId{Id: id}
69 | }
70 |
71 | // 分块处理,避免请求过大
72 | missSize := len(allSongIdsSlice)
73 | chunkCount := (missSize + chunkSize - 1) / chunkSize
74 | chunks := make([][]*models.SongId, chunkCount)
75 |
76 | for i := 0; i < missSize; i += chunkSize {
77 | end := i + chunkSize
78 | if end > missSize {
79 | end = missSize
80 | }
81 | chunks[i/chunkSize] = allSongIdsSlice[i:end]
82 | }
83 |
84 | // 并发请求处理
85 | var eg errgroup.Group
86 |
87 | for _, chunk := range chunks {
88 | chunk := chunk // 创建副本避免闭包问题
89 | eg.Go(func() error {
90 | return processChunkDetailed(chunk, &resultMap)
91 | })
92 | }
93 |
94 | // 等待所有请求完成
95 | if err := eg.Wait(); err != nil {
96 | return nil, fmt.Errorf("获取歌曲详情失败: %w", err)
97 | }
98 |
99 | // 返回最终结果
100 | return createSongList(playlistName, trackIds, resultMap, tracksCount), nil
101 | }
102 |
103 | // 非详细模式下,走正常的缓存和数据库流程
104 | // 2. 构建缓存键
105 | songCacheKeys := make([]string, len(trackIds))
106 | for i, track := range trackIds {
107 | songCacheKeys[i] = fmt.Sprintf(netEasyRedis, track.Id)
108 | }
109 |
110 | // 3. 存储歌曲信息的结果集
111 | resultMap := sync.Map{}
112 |
113 | // 4. 尝试从缓存获取歌曲信息
114 | cacheResults, _ := cache.MGet(songCacheKeys...)
115 |
116 | // 5. 收集缓存未命中的歌曲ID
117 | missCacheKeys := make([]uint, 0, len(trackIds))
118 | for i, result := range cacheResults {
119 | if result != nil {
120 | resultMap.Store(trackIds[i].Id, result.(string))
121 | } else {
122 | missCacheKeys = append(missCacheKeys, trackIds[i].Id)
123 | }
124 | }
125 |
126 | // 缓存全部命中,直接返回结果
127 | if len(missCacheKeys) == 0 {
128 | log.Infof("网易云歌单缓存全部命中: %v", link)
129 | return createSongList(playlistName, trackIds, resultMap, tracksCount), nil
130 | }
131 |
132 | // 6. 从数据库查询缓存未命中的歌曲
133 | dbResults, _ := db.BatchGetSongById(missCacheKeys)
134 |
135 | // 7. 收集数据库未命中的歌曲ID
136 | missDBKeys := make([]uint, 0, len(missCacheKeys))
137 | for _, id := range missCacheKeys {
138 | if val, ok := dbResults[id]; ok {
139 | resultMap.Store(id, val)
140 | } else {
141 | missDBKeys = append(missDBKeys, id)
142 | }
143 | }
144 |
145 | // 数据库全部命中,更新缓存并返回结果
146 | if len(missDBKeys) == 0 {
147 | log.Infof("网易云歌单数据库全部命中: %v", link)
148 | // 更新缓存
149 | missKeyCacheMap := sync.Map{}
150 | for k, v := range dbResults {
151 | missKeyCacheMap.Store(fmt.Sprintf(netEasyRedis, k), v)
152 | }
153 | _ = cache.MSet(missKeyCacheMap)
154 |
155 | return createSongList(playlistName, trackIds, resultMap, tracksCount), nil
156 | }
157 |
158 | // 8. 从网易云API获取未命中的歌曲信息
159 | missKeyCacheMap, err := batchGetSongs(missDBKeys, resultMap, detailed)
160 | if err != nil {
161 | return nil, fmt.Errorf("获取歌曲详情失败: %w", err)
162 | }
163 |
164 | // 9. 将新获取的歌曲信息写入数据库
165 | missDbData := make([]*models.NetEasySong, 0, len(missDBKeys))
166 | for _, id := range missDBKeys {
167 | if value, ok := missKeyCacheMap.Load(fmt.Sprintf(netEasyRedis, id)); ok {
168 | missDbData = append(missDbData, &models.NetEasySong{Id: id, Name: value.(string)})
169 | }
170 | }
171 |
172 | if len(missDbData) > 0 {
173 | if err := db.BatchInsertSong(missDbData); err != nil {
174 | log.Warnf("写入数据库失败: %v", err)
175 | }
176 | }
177 |
178 | // 10. 更新缓存
179 | if err := cache.MSet(missKeyCacheMap); err != nil {
180 | log.Warnf("更新缓存失败: %v", err)
181 | }
182 |
183 | // 11. 返回最终结果
184 | return createSongList(playlistName, trackIds, resultMap, tracksCount), nil
185 | }
186 |
187 | // createSongList 创建歌单结果
188 | func createSongList(name string, trackIds []*models.TrackId, resultMap sync.Map, count int) *models.SongList {
189 | return &models.SongList{
190 | Name: name,
191 | Songs: utils.SyncMapToSortedSlice(trackIds, resultMap),
192 | SongsCount: count,
193 | }
194 | }
195 |
196 | // getSongsInfo 获取歌单基本信息
197 | func getSongsInfo(link string) (*models.NetEasySongId, error) {
198 | songListId, err := utils.GetNetEasyParam(link)
199 | if err != nil {
200 | return nil, fmt.Errorf("解析歌单链接失败: %w", err)
201 | }
202 |
203 | resp, err := httputil.Post(netEasyUrlV6, strings.NewReader("id="+songListId))
204 | if err != nil {
205 | return nil, fmt.Errorf("请求网易云API失败: %w", err)
206 | }
207 | defer resp.Body.Close()
208 |
209 | body, err := io.ReadAll(resp.Body)
210 | if err != nil {
211 | return nil, fmt.Errorf("读取响应内容失败: %w", err)
212 | }
213 |
214 | songIdsResp := &models.NetEasySongId{}
215 | if err = json.Unmarshal(body, songIdsResp); err != nil {
216 | return nil, fmt.Errorf("解析响应内容失败: %w", err)
217 | }
218 |
219 | if songIdsResp.Code == 401 {
220 | return nil, errors.New("无权限访问该歌单")
221 | }
222 |
223 | return songIdsResp, nil
224 | }
225 |
226 | // batchGetSongs 批量获取歌曲详情
227 | func batchGetSongs(missKeys []uint, resultMap sync.Map, detailed bool) (sync.Map, error) {
228 | if len(missKeys) == 0 {
229 | return sync.Map{}, nil
230 | }
231 |
232 | // 1. 构建请求参数
233 | missSongIds := make([]*models.SongId, len(missKeys))
234 | for i, id := range missKeys {
235 | missSongIds[i] = &models.SongId{Id: id}
236 | }
237 |
238 | // 2. 分块处理,避免请求过大
239 | missSize := len(missSongIds)
240 | chunkCount := (missSize + chunkSize - 1) / chunkSize
241 | chunks := make([][]*models.SongId, chunkCount)
242 |
243 | for i := 0; i < missSize; i += chunkSize {
244 | end := i + chunkSize
245 | if end > missSize {
246 | end = missSize
247 | }
248 | chunks[i/chunkSize] = missSongIds[i:end]
249 | }
250 |
251 | // 3. 并发请求处理
252 | var eg errgroup.Group
253 | missKeyCacheMap := sync.Map{}
254 |
255 | for _, chunk := range chunks {
256 | chunk := chunk // 创建副本避免闭包问题
257 | eg.Go(func() error {
258 | return processChunk(chunk, &missKeyCacheMap, &resultMap, detailed)
259 | })
260 | }
261 |
262 | // 4. 等待所有请求完成
263 | if err := eg.Wait(); err != nil {
264 | return sync.Map{}, err
265 | }
266 |
267 | return missKeyCacheMap, nil
268 | }
269 |
270 | // processChunk 处理一个分块的歌曲ID
271 | func processChunk(chunk []*models.SongId, missKeyCacheMap *sync.Map, resultMap *sync.Map, detailed bool) error {
272 | // 1. 序列化请求参数
273 | marshal, err := json.Marshal(chunk)
274 | if err != nil {
275 | return fmt.Errorf("序列化请求参数失败: %w", err)
276 | }
277 |
278 | // 2. 发送请求
279 | resp, err := httputil.Post(netEasyUrlV3, strings.NewReader("c="+string(marshal)))
280 | if err != nil {
281 | return fmt.Errorf("请求歌曲详情失败: %w", err)
282 | }
283 | defer resp.Body.Close()
284 |
285 | // 3. 读取响应内容
286 | bytes, err := io.ReadAll(resp.Body)
287 | if err != nil {
288 | return fmt.Errorf("读取响应内容失败: %w", err)
289 | }
290 |
291 | // 4. 解析响应内容
292 | songs := &models.Songs{}
293 | if err = json.Unmarshal(bytes, songs); err != nil {
294 | return fmt.Errorf("解析响应内容失败: %w", err)
295 | }
296 |
297 | // 5. 处理歌曲信息
298 | for _, song := range songs.Songs {
299 | // 根据detailed参数决定是否使用原始歌曲名
300 | var songName string
301 | if detailed {
302 | songName = song.Name // 使用原始歌曲名
303 | } else {
304 | songName = utils.StandardSongName(song.Name) // 使用标准化的歌曲名
305 | }
306 |
307 | // 构建作者信息
308 | authors := make([]string, len(song.Ar))
309 | for i, ar := range song.Ar {
310 | authors[i] = ar.Name
311 | }
312 |
313 | // 格式化歌曲信息
314 | songInfo := fmt.Sprintf("%s - %s", songName, strings.Join(authors, " / "))
315 |
316 | // 存储结果
317 | cacheKey := fmt.Sprintf(netEasyRedis, song.Id)
318 | missKeyCacheMap.Store(cacheKey, songInfo)
319 | resultMap.Store(song.Id, songInfo)
320 | }
321 |
322 | return nil
323 | }
324 |
325 | // processChunkDetailed 处理一个分块的歌曲ID(详细模式)
326 | func processChunkDetailed(chunk []*models.SongId, resultMap *sync.Map) error {
327 | // 1. 序列化请求参数
328 | marshal, err := json.Marshal(chunk)
329 | if err != nil {
330 | return fmt.Errorf("序列化请求参数失败: %w", err)
331 | }
332 |
333 | // 2. 发送请求
334 | resp, err := httputil.Post(netEasyUrlV3, strings.NewReader("c="+string(marshal)))
335 | if err != nil {
336 | return fmt.Errorf("请求歌曲详情失败: %w", err)
337 | }
338 | defer resp.Body.Close()
339 |
340 | // 3. 读取响应内容
341 | bytes, err := io.ReadAll(resp.Body)
342 | if err != nil {
343 | return fmt.Errorf("读取响应内容失败: %w", err)
344 | }
345 |
346 | // 4. 解析响应内容
347 | songs := &models.Songs{}
348 | if err = json.Unmarshal(bytes, songs); err != nil {
349 | return fmt.Errorf("解析响应内容失败: %w", err)
350 | }
351 |
352 | // 5. 处理歌曲信息
353 | for _, song := range songs.Songs {
354 | // 使用原始歌曲名
355 | songName := song.Name
356 |
357 | // 构建作者信息
358 | authors := make([]string, len(song.Ar))
359 | for i, ar := range song.Ar {
360 | authors[i] = ar.Name
361 | }
362 |
363 | // 格式化歌曲信息
364 | songInfo := fmt.Sprintf("%s - %s", songName, strings.Join(authors, " / "))
365 |
366 | // 存储结果 - 注意这里直接使用song.Id作为key,而不是缓存键
367 | resultMap.Store(song.Id, songInfo)
368 | }
369 |
370 | return nil
371 | }
372 |
--------------------------------------------------------------------------------
/logic/neteasy_test.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 |
10 | "GoMusic/misc/utils"
11 | )
12 |
13 | const (
14 | V1 = "http://163cn.tv/zoIxm3"
15 | V5 = "http://music.163.com/playlist/2275447155/434174568/?userid=440609461"
16 |
17 | V2 = "https://music.163.com/#/playlist?app_version=8.10.81&id=8725919816&dlt=0846&creatorId=341246998"
18 | V3 = "https://music.163.com/playlist?id=477577176&userid=341246998"
19 | V4 = "分享Mad_Cat_的歌单《Mad_Cat_喜欢的音乐》http://163cn.tv/aSl9Z1 (@网易云音乐)"
20 | )
21 |
22 | func TestRegex(t *testing.T) {
23 | t.Run("Extracted URL", func(t *testing.T) {
24 | sample := []string{V1, V2, V3, V4, V5}
25 | urlPattern := `http[s]?://[^ ]+`
26 | re := regexp.MustCompile(urlPattern)
27 | for _, v := range sample {
28 | fmt.Println("Extracted URL:", re.FindString(v))
29 | }
30 | })
31 | t.Run("Extracted ID", func(t *testing.T) {
32 | sample := []string{V1, V2, V3, V4, V5}
33 | re := regexp.MustCompile(`playlist/(\d+)`)
34 | // 在字符串中查找第一个匹配项
35 | for _, v := range sample {
36 | match := re.FindStringSubmatch(v)
37 | // 检查是否找到匹配项,并打印
38 | if len(match) > 1 {
39 | fmt.Println(match[1]) // 第二个元素包含第一个括号内的匹配内容
40 | } else {
41 | fmt.Println("No match found")
42 | }
43 | }
44 | })
45 |
46 | }
47 |
48 | func TestBracketRegex(t *testing.T) {
49 | fmt.Println(utils.StandardSongName("理想三旬(女声版) - 藤柒吖"))
50 | fmt.Println(utils.StandardSongName("小酒窝(Live) - 蔡卓妍 / 林俊杰"))
51 | fmt.Println(utils.StandardSongName("最后一页(完整版) - 洛尘鞅_"))
52 | fmt.Println(utils.StandardSongName("知我(抒情版) - 尘ah."))
53 | fmt.Println(utils.StandardSongName("幻听(女声版) - 星月酱"))
54 | }
55 |
56 | func TestDiscover(t *testing.T) {
57 | sample := []string{V1, V2, V3, V4, V5}
58 | for _, v := range sample {
59 | discover, err := NetEasyDiscover(v, false)
60 | assert.NoError(t, err)
61 | t.Log(discover)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/logic/qishuimusic.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/url"
7 | "regexp"
8 | "strings"
9 |
10 | "GoMusic/misc/httputil"
11 | "GoMusic/misc/models"
12 | "GoMusic/misc/utils"
13 |
14 | "github.com/PuerkitoBio/goquery"
15 | )
16 |
17 | // 歌曲链接正则
18 | const (
19 | qishuiMusicV1 = `https?://[a-zA-Z0-9./?=&_-]+`
20 | qishuiMusicV2 = `playlist_id=`
21 | qishuiMusicV3 = `https?://qishui\.douyin\.com/s/[a-zA-Z0-9]+/?` // 匹配汽水音乐分享链接
22 | )
23 |
24 | var (
25 | qishuiMusicV1Regx, _ = regexp.Compile(qishuiMusicV1)
26 | qishuiMusicV2Regx, _ = regexp.Compile(qishuiMusicV2)
27 | qishuiMusicV3Regx, _ = regexp.Compile(qishuiMusicV3) // 专门匹配汽水音乐链接
28 | )
29 |
30 | // 歌曲信息列表#root > div > div > div > div > div:nth-child(2) > div > div > div > div > div 下的子元素nth-child
31 | // 歌曲名称 div:nth-child(2) > div:nth-child(1) > p
32 | // 歌曲作者 div:nth-child(2) > div:nth-child(2) > p
33 |
34 | // QiShuiMusicDiscover 解析歌单
35 | // link: 歌单链接
36 | // detailed: 是否使用详细歌曲名(原始歌曲名,不去除括号等内容)
37 | func QiShuiMusicDiscover(link string, detailed bool) (*models.SongList, error) {
38 | // 从文本中提取汽水音乐链接
39 | extractedLink := qishuiMusicV3Regx.FindString(link)
40 | if extractedLink != "" {
41 | link = extractedLink
42 | }
43 |
44 | params, err := getQiShuiParams(link)
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | resp, err := httputil.Get(link, strings.NewReader(params))
50 | if err != nil {
51 | return nil, err
52 | }
53 | defer resp.Body.Close()
54 |
55 | songList, err := parseQsSongList(resp.Body, detailed)
56 | if err != nil {
57 | return nil, err
58 | }
59 | return songList, nil
60 | }
61 |
62 | // getQiShuiParams 获取参数,但是都爬网页了好像没有必要
63 | func getQiShuiParams(link string) (string, error) {
64 | extractLink := qishuiMusicV1Regx.FindString(link)
65 | if !qishuiMusicV2Regx.MatchString(extractLink) {
66 | var err error
67 | extractLink, err = httputil.GetRedirectLocation(extractLink)
68 | if err != nil {
69 | return "", err
70 | }
71 | }
72 | fmt.Println(extractLink)
73 | parsedURL, err := url.Parse(extractLink)
74 | if err != nil {
75 | return "", err
76 | }
77 | params := parsedURL.Query()
78 | return params.Encode(), nil
79 | }
80 |
81 | // parseQsSongList 解析网页
82 | // detailed: 是否使用详细歌曲名(原始歌曲名,不去除括号等内容)
83 | func parseQsSongList(body io.Reader, detailed bool) (*models.SongList, error) {
84 | docDetail, err := goquery.NewDocumentFromReader(body)
85 | if err != nil {
86 | return nil, err
87 | }
88 | songListName := docDetail.Find("#root > div > div > div > div > div:nth-child(1) > div:nth-child(3) > h1 > p").Text()
89 | songListAuthor := docDetail.Find("#root > div > div > div > div > div:nth-child(1) > div:nth-child(3) > div > div > div:nth-child(2) > p").Text()
90 | songList := models.SongList{
91 | Name: fmt.Sprintf("%s-%s", songListName, songListAuthor),
92 | SongsCount: 0,
93 | }
94 | docDetail.Find("#root > div > div > div > div > div:nth-child(2) > div > div > div > div > div").Each(
95 | func(i int, s *goquery.Selection) {
96 | title := s.Find("div:nth-child(2) > div:nth-child(1) > p").Text()
97 | artist := s.Find("div:nth-child(2) > div:nth-child(2) > p").Text()
98 |
99 | // artist 需要格式化,去除 • 后面的字符,例如:G.E.M. 邓紫棋 • T.I.M.E. -> G.E.M. 邓紫棋
100 | artist = strings.Split(artist, "•")[0]
101 |
102 | // 根据detailed参数决定是否使用原始歌曲名
103 | var songName string
104 | if detailed {
105 | songName = title // 使用原始歌曲名
106 | } else {
107 | songName = utils.StandardSongName(title) // 使用标准化的歌曲名
108 | }
109 |
110 | // 按照网易云音乐的格式化方式: "歌曲名 - 歌手"
111 | formattedSong := fmt.Sprintf("%s - %s", songName, artist)
112 | songList.Songs = append(songList.Songs, formattedSong)
113 | songList.SongsCount++
114 | },
115 | )
116 | return &songList, nil
117 | }
118 |
--------------------------------------------------------------------------------
/logic/qqmusic.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "regexp"
10 | "strconv"
11 | "strings"
12 | "time"
13 |
14 | "GoMusic/misc/httputil"
15 | "GoMusic/misc/log"
16 | "GoMusic/misc/models"
17 | "GoMusic/misc/utils"
18 | )
19 |
20 | // QQ音乐相关常量
21 | const (
22 | // API相关
23 | qqMusicRedis = "qq_music:%d"
24 | qqMusicAPIURL = "https://u6.y.qq.com/cgi-bin/musics.fcg?sign=%s&_=%d"
25 |
26 | // 错误响应长度标识
27 | qqMusicErrorResponseLength = 108
28 |
29 | // 分页相关
30 | maxSongsPerPage = 1000 // 每页最大歌曲数
31 | maxTotalSongs = 10000 // 最大支持的歌曲总数
32 | )
33 |
34 | // 链接类型正则表达式
35 | var (
36 | // 短链接,需要重定向
37 | shortLinkRegex = regexp.MustCompile(`fcgi-bin`)
38 |
39 | // 详情页链接,包含details关键词
40 | detailsLinkRegex = regexp.MustCompile(`details`)
41 |
42 | // 包含id=数字的链接
43 | idParamLinkRegex = regexp.MustCompile(`id=\d+`)
44 |
45 | // 包含playlist/数字的链接
46 | playlistLinkRegex = regexp.MustCompile(`.*playlist/\d+$`)
47 | )
48 |
49 | // QQMusicDiscover 获取QQ音乐歌单信息
50 | // link: 歌单链接
51 | // detailed: 是否使用详细歌曲名(原始歌曲名,不去除括号等内容)
52 | func QQMusicDiscover(link string, detailed bool) (*models.SongList, error) {
53 | // 1. 从链接中提取歌单ID
54 | tid, err := extractPlaylistID(link)
55 | if err != nil || tid == 0 {
56 | return nil, errors.New("无效的歌单链接")
57 | }
58 |
59 | // 2. 获取歌单数据
60 | responseData, err := fetchPlaylistData(tid)
61 | if err != nil {
62 | log.Errorf("获取QQ音乐歌单数据失败: %v", err)
63 | return nil, fmt.Errorf("获取歌单数据失败: %w", err)
64 | }
65 |
66 | // 3. 解析响应数据
67 | qqMusicResponse := &models.QQMusicResp{}
68 | if err = json.Unmarshal(responseData, qqMusicResponse); err != nil {
69 | log.Errorf("解析QQ音乐响应数据失败: %v", err)
70 | return nil, fmt.Errorf("解析歌单数据失败: %w", err)
71 | }
72 |
73 | // 4. 构建歌曲列表
74 | songList := buildSongList(qqMusicResponse, detailed)
75 |
76 | return songList, nil
77 | }
78 |
79 | // fetchPlaylistData 获取QQ音乐歌单数据
80 | // 尝试多个平台参数,直到获取有效响应
81 | // 支持分页获取大型歌单的所有歌曲
82 | func fetchPlaylistData(tid int) ([]byte, error) {
83 | // 1. 先获取歌单基本信息,了解总歌曲数
84 | basicInfo, err := fetchPlaylistBasicInfo(tid)
85 | if err != nil {
86 | return nil, fmt.Errorf("获取歌单基本信息失败: %w", err)
87 | }
88 |
89 | // 解析基本信息
90 | basicResp := &models.QQMusicResp{}
91 | if err = json.Unmarshal(basicInfo, basicResp); err != nil {
92 | return nil, fmt.Errorf("解析歌单基本信息失败: %w", err)
93 | }
94 |
95 | // 获取歌曲总数
96 | totalSongs := basicResp.Req0.Data.Dirinfo.Songnum
97 | if totalSongs <= maxSongsPerPage {
98 | // 如果歌曲数量不超过单页上限,直接返回基本信息
99 | return basicInfo, nil
100 | }
101 |
102 | // 2. 分页获取所有歌曲
103 | log.Infof("歌单包含%d首歌曲,需要分页获取", totalSongs)
104 |
105 | // 限制最大获取数量,防止请求过多
106 | if totalSongs > maxTotalSongs {
107 | log.Warnf("歌单歌曲数量(%d)超过最大支持数量(%d),将只获取前%d首",
108 | totalSongs, maxTotalSongs, maxTotalSongs)
109 | totalSongs = maxTotalSongs
110 | }
111 |
112 | // 计算需要的页数
113 | pageCount := (totalSongs + maxSongsPerPage - 1) / maxSongsPerPage
114 |
115 | // 创建一个新的响应对象,用于合并所有页的数据
116 | mergedResp := models.QQMusicResp{
117 | Code: basicResp.Code,
118 | Req0: struct {
119 | Code int `json:"code"`
120 | Data struct {
121 | Dirinfo struct {
122 | Title string `json:"title"`
123 | Songnum int `json:"songnum"`
124 | } `json:"dirinfo"`
125 | Songlist []struct {
126 | Name string `json:"name"`
127 | Singer []struct {
128 | Name string `json:"name"`
129 | } `json:"singer"`
130 | } `json:"songlist"`
131 | } `json:"data"`
132 | }{
133 | Code: basicResp.Req0.Code,
134 | Data: struct {
135 | Dirinfo struct {
136 | Title string `json:"title"`
137 | Songnum int `json:"songnum"`
138 | } `json:"dirinfo"`
139 | Songlist []struct {
140 | Name string `json:"name"`
141 | Singer []struct {
142 | Name string `json:"name"`
143 | } `json:"singer"`
144 | } `json:"songlist"`
145 | }{
146 | Dirinfo: basicResp.Req0.Data.Dirinfo,
147 | Songlist: make([]struct {
148 | Name string `json:"name"`
149 | Singer []struct {
150 | Name string `json:"name"`
151 | } `json:"singer"`
152 | }, 0, totalSongs),
153 | },
154 | },
155 | }
156 |
157 | // 添加第一页的歌曲
158 | mergedResp.Req0.Data.Songlist = append(mergedResp.Req0.Data.Songlist, basicResp.Req0.Data.Songlist...)
159 |
160 | // 获取剩余页的数据
161 | for page := 1; page < pageCount; page++ {
162 | songBegin := page * maxSongsPerPage
163 | songNum := maxSongsPerPage
164 | if songBegin+songNum > totalSongs {
165 | songNum = totalSongs - songBegin
166 | }
167 |
168 | log.Infof("获取第%d页歌曲,起始位置: %d,数量: %d", page+1, songBegin, songNum)
169 |
170 | // 获取当前页数据
171 | pageData, err := fetchPlaylistPage(tid, songBegin, songNum)
172 | if err != nil {
173 | log.Errorf("获取第%d页歌曲失败: %v", page+1, err)
174 | continue
175 | }
176 |
177 | // 解析当前页数据
178 | pageResp := &models.QQMusicResp{}
179 | if err = json.Unmarshal(pageData, pageResp); err != nil {
180 | log.Errorf("解析第%d页歌曲数据失败: %v", page+1, err)
181 | continue
182 | }
183 |
184 | // 添加当前页歌曲到合并的响应中
185 | mergedResp.Req0.Data.Songlist = append(mergedResp.Req0.Data.Songlist, pageResp.Req0.Data.Songlist...)
186 | }
187 |
188 | // 更新合并后的歌曲总数
189 | mergedResp.Req0.Data.Dirinfo.Songnum = len(mergedResp.Req0.Data.Songlist)
190 |
191 | // 将合并后的响应转换为JSON
192 | mergedData, err := json.Marshal(mergedResp)
193 | if err != nil {
194 | return nil, fmt.Errorf("合并歌单数据失败: %w", err)
195 | }
196 |
197 | log.Infof("成功获取全部%d首歌曲", len(mergedResp.Req0.Data.Songlist))
198 | return mergedData, nil
199 | }
200 |
201 | // fetchPlaylistBasicInfo 获取歌单基本信息(第一页数据)
202 | func fetchPlaylistBasicInfo(tid int) ([]byte, error) {
203 | return fetchPlaylistPage(tid, 0, maxSongsPerPage)
204 | }
205 |
206 | // fetchPlaylistPage 获取歌单指定页的数据
207 | func fetchPlaylistPage(tid int, songBegin, songNum int) ([]byte, error) {
208 | // 支持的平台列表
209 | platforms := []string{"-1", "android", "iphone", "h5", "wxfshare", "iphone_wx", "windows"}
210 |
211 | var lastErr error
212 | var resp *http.Response
213 |
214 | // 尝试不同平台参数
215 | for _, platform := range platforms {
216 | // 1. 构建请求参数
217 | paramString := models.GetQQMusicReqStringWithPagination(tid, platform, songBegin, songNum)
218 | sign := utils.Encrypt(paramString)
219 | requestURL := fmt.Sprintf(qqMusicAPIURL, sign, time.Now().UnixMilli())
220 |
221 | // 2. 发送请求
222 | resp, lastErr = httputil.Post(requestURL, strings.NewReader(paramString))
223 | if lastErr != nil {
224 | log.Errorf("HTTP请求失败(平台:%s): %v", platform, lastErr)
225 | continue
226 | }
227 |
228 | // 3. 读取响应
229 | data, _ := io.ReadAll(resp.Body)
230 | resp.Body.Close()
231 |
232 | // 4. 检查响应是否有效
233 | // 108字节长度表示返回了错误信息,需要尝试其他平台
234 | if len(data) != qqMusicErrorResponseLength {
235 | return data, nil
236 | }
237 | }
238 |
239 | return nil, fmt.Errorf("尝试所有平台参数均失败: %w", lastErr)
240 | }
241 |
242 | // extractPlaylistID 从QQ音乐链接中提取歌单ID
243 | func extractPlaylistID(link string) (int, error) {
244 | // 1. 处理playlist/数字格式的链接
245 | if playlistLinkRegex.MatchString(link) {
246 | return extractNumberAfterKeyword(link, "playlist/")
247 | }
248 |
249 | // 2. 处理id=数字格式的链接
250 | if idParamLinkRegex.MatchString(link) {
251 | return extractNumberAfterKeyword(link, "id=")
252 | }
253 |
254 | // 3. 处理需要重定向的短链接
255 | if shortLinkRegex.MatchString(link) {
256 | redirectedLink, err := httputil.GetRedirectLocation(link)
257 | if err != nil {
258 | log.Errorf("获取重定向链接失败: %v", err)
259 | return 0, fmt.Errorf("处理短链接失败: %w", err)
260 | }
261 | // 递归处理重定向后的链接
262 | return extractPlaylistID(redirectedLink)
263 | }
264 |
265 | // 4. 处理details页面链接
266 | if detailsLinkRegex.MatchString(link) {
267 | tidString, err := utils.GetQQMusicParam(link)
268 | if err != nil {
269 | log.Errorf("从details链接提取ID失败: %v", err)
270 | return 0, fmt.Errorf("提取歌单ID失败: %w", err)
271 | }
272 |
273 | tid, err := strconv.Atoi(tidString)
274 | if err != nil {
275 | log.Errorf("歌单ID转换为数字失败: %v", err)
276 | return 0, fmt.Errorf("歌单ID格式错误: %w", err)
277 | }
278 |
279 | return tid, nil
280 | }
281 |
282 | return 0, errors.New("无效的歌单链接格式")
283 | }
284 |
285 | // buildSongList 根据QQ音乐响应数据构建歌曲列表
286 | func buildSongList(response *models.QQMusicResp, detailed bool) *models.SongList {
287 | songsCount := response.Req0.Data.Dirinfo.Songnum
288 | songList := response.Req0.Data.Songlist
289 |
290 | songs := make([]string, 0, len(songList))
291 | builder := strings.Builder{}
292 |
293 | for _, song := range songList {
294 | builder.Reset()
295 |
296 | // 根据detailed参数决定是否使用原始歌曲名
297 | if detailed {
298 | builder.WriteString(song.Name) // 使用原始歌曲名
299 | } else {
300 | builder.WriteString(utils.StandardSongName(song.Name)) // 去除多余符号
301 | }
302 |
303 | builder.WriteString(" - ")
304 |
305 | // 处理歌手信息
306 | singers := make([]string, 0, len(song.Singer))
307 | for _, singer := range song.Singer {
308 | singers = append(singers, singer.Name)
309 | }
310 | builder.WriteString(strings.Join(singers, " / "))
311 |
312 | songs = append(songs, builder.String())
313 | }
314 |
315 | return &models.SongList{
316 | Name: response.Req0.Data.Dirinfo.Title,
317 | Songs: songs,
318 | SongsCount: songsCount,
319 | }
320 | }
321 |
322 | // extractNumberAfterKeyword 从字符串中提取关键词后面的数字
323 | func extractNumberAfterKeyword(s, keyword string) (int, error) {
324 | index := strings.Index(s, keyword)
325 | if index < 0 {
326 | return 0, fmt.Errorf("未找到关键词: %s", keyword)
327 | }
328 |
329 | // 提取关键词后面的所有数字
330 | startIndex := index + len(keyword)
331 | endIndex := len(s)
332 |
333 | // 找到数字结束的位置
334 | for i := startIndex; i < endIndex; i++ {
335 | if s[i] < '0' || s[i] > '9' {
336 | endIndex = i
337 | break
338 | }
339 | }
340 |
341 | // 提取并转换数字
342 | numStr := s[startIndex:endIndex]
343 | num, err := strconv.Atoi(numStr)
344 | if err != nil {
345 | return 0, fmt.Errorf("数字转换失败: %w", err)
346 | }
347 |
348 | return num, nil
349 | }
350 |
--------------------------------------------------------------------------------
/logic/qqmusic_test.go:
--------------------------------------------------------------------------------
1 | package logic
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestQQMusicDiscover(t *testing.T) {
11 | // https://c6.y.qq.com/base/fcgi-bin/u?__=4V33zWKDE3tI
12 | // https://y.qq.com/n/ryqq/playlist/7364061065
13 | // https://i.y.qq.com/n2/m/share/details/taoge.html?hosteuin=oKE57evqoiEPoz**&id=1596010000&appversion=120801&ADTAG=wxfshare&appshare=iphone_wx
14 | // https://i.y.qq.com/n2/m/share/details/taoge.html?platform=11&appshare=android_qq&appversion=12090008&hosteuin=oK6kowEAoK4z7eSk7eEloKCFoz**&id=5204875759&ADTAG=wxfshare
15 | // https://i.y.qq.com/n2/m/share/details/taoge.html?id=9094523921 // 12.11格式
16 | // https://i.y.qq.com/n2/m/share/details/taoge.html?id=8177163754
17 | // https://y.qq.com/n/ryqq/playlist/1953563505
18 | // https://i.y.qq.com/n2/m/share/details/taoge.html?id=9115464345&hosteuin=
19 | // https://i.y.qq.com/n2/m/share/details/taoge.html?hosteuin=owEkNKSzNK65&id=930054744&appversion=130000&ADTAG=qfshare&source=qq&appshare=iphone
20 | // https://c6.y.qq.com/base/fcgi-bin/u?__=XogXh1TLpP1t
21 | // https://i.y.qq.com/n2/m/share/details/taoge.html?hosteuin=ownioivi7wvq7n**&id=8683730831&appversion=120801&ADTAG=wxfshare&appshare=iphone_wx
22 |
23 | t.Run("v1", func(t *testing.T) {
24 | discover, err := QQMusicDiscover("https://c6.y.qq.com/base/fcgi-bin/u?__=4V33zWKDE3tI", false)
25 | assert.NoError(t, err)
26 | fmt.Println(discover)
27 | })
28 | t.Run("v2", func(t *testing.T) {
29 | discover, err := QQMusicDiscover("https://y.qq.com/n/ryqq/playlist/7364061065", false)
30 | assert.NoError(t, err)
31 | fmt.Println(discover)
32 | })
33 | t.Run("v3", func(t *testing.T) {
34 | discover, err := QQMusicDiscover("https://i.y.qq.com/n2/m/share/details/taoge.html?hosteuin=oKE57evqoiEPoz**&id=1596010000&appversion=120801&ADTAG=wxfshare&appshare=iphone_wx", false)
35 | assert.NoError(t, err)
36 | fmt.Println(discover)
37 | })
38 | t.Run("v4", func(t *testing.T) {
39 | discover, err := QQMusicDiscover("https://i.y.qq.com/n2/m/share/details/taoge.html?platform=11&appshare=android_qq&appversion=12090008&hosteuin=oK6kowEAoK4z7eSk7eEloKCFoz**&id=5204875759&ADTAG=wxfshare", false)
40 | assert.NoError(t, err)
41 | fmt.Println(discover)
42 | })
43 | t.Run("v5", func(t *testing.T) {
44 | discover, err := QQMusicDiscover("https://i.y.qq.com/n2/m/share/details/taoge.html?id=9094523921", false)
45 | assert.NoError(t, err)
46 | fmt.Println(discover)
47 | })
48 | t.Run("v6", func(t *testing.T) {
49 | discover, err := QQMusicDiscover("https://i.y.qq.com/n2/m/share/details/taoge.html?id=8177163754", false)
50 | assert.NoError(t, err)
51 | fmt.Println(discover)
52 | })
53 | t.Run("v7", func(t *testing.T) {
54 | discover, err := QQMusicDiscover("https://y.qq.com/n/ryqq/playlist/1953563505", false)
55 | assert.NoError(t, err)
56 | fmt.Println(discover)
57 | })
58 | t.Run("v8", func(t *testing.T) {
59 | discover, err := QQMusicDiscover("https://i.y.qq.com/n2/m/share/details/taoge.html?id=9115464345&hosteuin=", false )
60 | assert.NoError(t, err)
61 | fmt.Println(discover)
62 | })
63 | t.Run("v9", func(t *testing.T) {
64 | discover, err := QQMusicDiscover("https://i.y.qq.com/n2/m/share/details/taoge.html?hosteuin=owEkNKSzNK65&id=930054744&appversion=130000&ADTAG=qfshare&source=qq&appshare=iphone", false)
65 | assert.NoError(t, err)
66 | fmt.Println(discover)
67 | })
68 | t.Run("v10", func(t *testing.T) {
69 | discover, err := QQMusicDiscover("https://c6.y.qq.com/base/fcgi-bin/u?__=XogXh1TLpP1t", false)
70 | assert.NoError(t, err)
71 | fmt.Println(discover)
72 | })
73 | t.Run("V11", func(t *testing.T) {
74 | discover, err := QQMusicDiscover("https://i.y.qq.com/n2/m/share/details/taoge.html?hosteuin=ownioivi7wvq7n**&id=8683730831&appversion=120801&ADTAG=wxfshare&appshare=iphone_wx", false)
75 | assert.NoError(t, err)
76 | fmt.Println(discover)
77 | })
78 | }
79 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "GoMusic/handler"
7 | "GoMusic/misc/log"
8 | "GoMusic/misc/models"
9 | _ "GoMusic/repo/db"
10 | )
11 |
12 | func main() {
13 | r := handler.NewRouter()
14 | if err := r.Run(fmt.Sprintf(models.PortFormat, models.Port)); err != nil {
15 | log.Errorf("fail to run server: %v", err)
16 | panic(err)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/misc/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bistutu/GoMusic/0f64ebaf9a2668eabbbf77a5b0a1ac35bdf32498/misc/.DS_Store
--------------------------------------------------------------------------------
/misc/httputil/http.go:
--------------------------------------------------------------------------------
1 | package httputil
2 |
3 | import (
4 | "io"
5 | "net/http"
6 |
7 | "GoMusic/misc/log"
8 | "GoMusic/misc/models"
9 | )
10 |
11 | // not allow redirect client
12 | var client *http.Client
13 |
14 | // allow redirect client
15 | var clientNoRedirect *http.Client
16 |
17 | func init() {
18 | client = &http.Client{}
19 | clientNoRedirect = &http.Client{
20 | CheckRedirect: func(req *http.Request, via []*http.Request) error {
21 | return http.ErrUseLastResponse // return this error to prevent redirect
22 | },
23 | }
24 | }
25 |
26 | // Post ...
27 | func Post(link string, data io.Reader) (*http.Response, error) {
28 | req, err := http.NewRequest(models.POST, link, data)
29 | if err != nil {
30 | log.Errorf("http NewRequest error: %+v", err)
31 | return nil, err
32 | }
33 | req.Header.Add(models.ContentType, "application/x-www-form-urlencoded")
34 | return client.Do(req)
35 | }
36 |
37 | // GetRedirectLocation ...
38 | func GetRedirectLocation(link string) (string, error) {
39 | rsp, err := clientNoRedirect.Get(link)
40 | if err != nil {
41 | log.Errorf("http Get error: %+v", err)
42 | return "", err
43 | }
44 | return rsp.Header.Get("Location"), nil
45 | }
46 | func Get(link string, data io.Reader) (*http.Response, error) {
47 | req, err := http.NewRequest("GET", link, data)
48 | if err != nil {
49 | log.Errorf("http NewRequest error: %+v", err)
50 | return nil, err
51 | }
52 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
53 | return client.Do(req)
54 | }
55 |
--------------------------------------------------------------------------------
/misc/images/0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bistutu/GoMusic/0f64ebaf9a2668eabbbf77a5b0a1ac35bdf32498/misc/images/0.png
--------------------------------------------------------------------------------
/misc/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bistutu/GoMusic/0f64ebaf9a2668eabbbf77a5b0a1ac35bdf32498/misc/images/1.png
--------------------------------------------------------------------------------
/misc/images/approve.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Bistutu/GoMusic/0f64ebaf9a2668eabbbf77a5b0a1ac35bdf32498/misc/images/approve.png
--------------------------------------------------------------------------------
/misc/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "go.uber.org/zap"
5 | )
6 |
7 | var logger *zap.SugaredLogger
8 |
9 | func init() {
10 | baseLogger, _ := zap.NewDevelopment()
11 | logger = baseLogger.Sugar()
12 | }
13 |
14 | // Info logs a non-formatted info message.
15 | func Info(args ...interface{}) {
16 | logger.Info(args...)
17 | }
18 |
19 | // Infof logs an info message with formatting.
20 | func Infof(template string, args ...interface{}) {
21 | logger.Infof(template, args...)
22 | }
23 |
24 | // Infow logs an info message with named fields.
25 | func Infow(msg string, keysAndValues ...interface{}) {
26 | logger.Infow(msg, keysAndValues...)
27 | }
28 |
29 | // Debug logs a non-formatted debug message.
30 | func Debug(args ...interface{}) {
31 | logger.Debug(args...)
32 | }
33 |
34 | // Debugf logs a debug message with formatting.
35 | func Debugf(template string, args ...interface{}) {
36 | logger.Debugf(template, args...)
37 | }
38 |
39 | // Debugw logs a debug message with named fields.
40 | func Debugw(msg string, keysAndValues ...interface{}) {
41 | logger.Debugw(msg, keysAndValues...)
42 | }
43 |
44 | // Warn logs a non-formatted warning message.
45 | func Warn(args ...interface{}) {
46 | logger.Warn(args...)
47 | }
48 |
49 | // Warnf logs a warning message with formatting.
50 | func Warnf(template string, args ...interface{}) {
51 | logger.Warnf(template, args...)
52 | }
53 |
54 | // Warnw logs a warning message with named fields.
55 | func Warnw(msg string, keysAndValues ...interface{}) {
56 | logger.Warnw(msg, keysAndValues...)
57 | }
58 |
59 | // Error logs a non-formatted error message.
60 | func Error(args ...interface{}) {
61 | logger.Error(args...)
62 | }
63 |
64 | // Errorf logs an error message with formatting.
65 | func Errorf(template string, args ...interface{}) {
66 | logger.Errorf(template, args...)
67 | }
68 |
69 | // Errorw logs an error message with named fields.
70 | func Errorw(msg string, keysAndValues ...interface{}) {
71 | logger.Errorw(msg, keysAndValues...)
72 | }
73 |
--------------------------------------------------------------------------------
/misc/models/common.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | const (
4 | Port = 8081
5 | PortFormat = ":%d"
6 | )
7 |
8 | const (
9 | POST = "POST"
10 | ContentType = "Content-Type"
11 | )
12 |
13 | const (
14 | SuccessCode = 1
15 | FailureCode = -1
16 | )
17 |
--------------------------------------------------------------------------------
/misc/models/db.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "time"
4 |
5 | // NetEasySong NetEasy song, the table will be created automatically if successful run the program
6 | type NetEasySong struct {
7 | Id uint `gorm:"column:id;primarykey"`
8 | Name string `gorm:"column:name;type:varchar(512);unique:true"`
9 | CreatedAt time.Time `gorm:"column:created_at"`
10 | }
11 |
--------------------------------------------------------------------------------
/misc/models/neteasy.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "fmt"
4 |
5 | // SongList represents a song list entity.
6 | type SongList struct {
7 | Name string `json:"name"` // song list name
8 | Songs []string `json:"songs"`
9 | SongsCount int `json:"songs_count"`
10 | }
11 |
12 | // SongId represents a song ID entity.
13 | type SongId struct {
14 | Id uint `json:"id"`
15 | }
16 |
17 | // String returns the string representation of the SongId.
18 | func (r *SongId) String() string {
19 | if r == nil {
20 | return "nil"
21 | }
22 | return fmt.Sprintf("{\"id\":%v}", r.Id)
23 | }
24 |
25 | // NetEasySongId represents a NetEasy song ID entity.
26 | type NetEasySongId struct {
27 | Code int `json:"code"`
28 | Playlist struct {
29 | Id int64 `json:"id"`
30 | Name string `json:"name"`
31 | TrackIds []*TrackId `json:"trackIds"`
32 | TrackCount int `json:"trackCount"`
33 | } `json:"playlist"`
34 | }
35 |
36 | // TrackId represents a track ID entity.
37 | type TrackId struct {
38 | Id uint `json:"id"`
39 | }
40 |
41 | // Songs represents a songs entity.
42 | type Songs struct {
43 | Songs []struct {
44 | Id uint `json:"id"`
45 | Name string `json:"name"`
46 | Ar []struct {
47 | Id int64 `json:"id"`
48 | Name string `json:"name"`
49 | } `json:"ar"`
50 | } `json:"songs"`
51 | }
52 |
--------------------------------------------------------------------------------
/misc/models/qqmusic.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import "encoding/json"
4 |
5 | type QQMusicReq struct {
6 | Req0 struct {
7 | Module string `json:"module"`
8 | Method string `json:"method"`
9 | Param struct {
10 | Disstid int `json:"disstid"`
11 | EncHostUin string `json:"enc_host_uin"`
12 | Tag int `json:"tag"`
13 | Userinfo int `json:"userinfo"`
14 | SongBegin int `json:"song_begin"`
15 | SongNum int `json:"song_num"`
16 | } `json:"param"`
17 | } `json:"req_0"`
18 | Comm struct {
19 | GTk int `json:"g_tk"`
20 | Uin int `json:"uin"`
21 | Format string `json:"format"`
22 | Platform string `json:"platform"`
23 | } `json:"comm"`
24 | }
25 |
26 | func NewQQMusicReq(disstid int, platform string, songBegin, songNum int) *QQMusicReq {
27 | return &QQMusicReq{
28 | Req0: struct {
29 | Module string `json:"module"`
30 | Method string `json:"method"`
31 | Param struct {
32 | Disstid int `json:"disstid"`
33 | EncHostUin string `json:"enc_host_uin"`
34 | Tag int `json:"tag"`
35 | Userinfo int `json:"userinfo"`
36 | SongBegin int `json:"song_begin"`
37 | SongNum int `json:"song_num"`
38 | } `json:"param"`
39 | }{
40 | Module: "music.srfDissInfo.aiDissInfo",
41 | Method: "uniform_get_Dissinfo",
42 | Param: struct {
43 | Disstid int `json:"disstid"`
44 | EncHostUin string `json:"enc_host_uin"`
45 | Tag int `json:"tag"`
46 | Userinfo int `json:"userinfo"`
47 | SongBegin int `json:"song_begin"`
48 | SongNum int `json:"song_num"`
49 | }{
50 | Disstid: disstid,
51 | EncHostUin: "",
52 | Tag: 1,
53 | Userinfo: 1,
54 | SongBegin: songBegin,
55 | SongNum: songNum,
56 | },
57 | },
58 | Comm: struct {
59 | GTk int `json:"g_tk"`
60 | Uin int `json:"uin"`
61 | Format string `json:"format"`
62 | Platform string `json:"platform"`
63 | }{
64 | GTk: 5381,
65 | Uin: 0,
66 | Format: "json",
67 | Platform: platform,
68 | },
69 | }
70 | }
71 |
72 | // GetQQMusicReqStringWithPagination 获取带分页参数的请求字符串
73 | func GetQQMusicReqStringWithPagination(disstid int, platform string, songBegin, songNum int) string {
74 | param := NewQQMusicReq(disstid, platform, songBegin, songNum)
75 | marshal, _ := json.Marshal(param)
76 | return string(marshal)
77 | }
78 |
79 | type QQMusicResp struct {
80 | Code int `json:"code"`
81 | Req0 struct {
82 | Code int `json:"code"`
83 | Data struct {
84 | Dirinfo struct {
85 | Title string `json:"title"`
86 | Songnum int `json:"songnum"`
87 | } `json:"dirinfo"`
88 | Songlist []struct {
89 | Name string `json:"name"`
90 | Singer []struct {
91 | Name string `json:"name"`
92 | } `json:"singer"`
93 | } `json:"songlist"`
94 | } `json:"data"`
95 | } `json:"req_0"`
96 | }
97 |
--------------------------------------------------------------------------------
/misc/models/result.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type Result struct {
4 | Code int `json:"code"`
5 | Msg string `json:"msg"`
6 | Data any `json:"data"`
7 | }
8 |
--------------------------------------------------------------------------------
/misc/test/test_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "testing"
7 | )
8 |
9 | const (
10 | netEasyRedis = "net:%v"
11 | )
12 |
13 | func BenchmarkFmt(b *testing.B) {
14 |
15 | b.Run("1", func(b *testing.B) {
16 | songCacheKey := make([]any, 0)
17 | for i := 0; i < b.N; i++ {
18 | songCacheKey = append(songCacheKey, fmt.Sprintf(netEasyRedis, i))
19 | }
20 | })
21 | b.Run("2", func(b *testing.B) {
22 | songCacheKey := make([]any, 0)
23 | for i := 0; i < b.N; i++ {
24 | songCacheKey = append(songCacheKey, strconv.FormatInt(int64(i), 10))
25 |
26 | }
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/misc/utils/music.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "net/url"
5 | "regexp"
6 | "sync"
7 |
8 | "GoMusic/misc/httputil"
9 | "GoMusic/misc/log"
10 | "GoMusic/misc/models"
11 | )
12 |
13 | const (
14 | bracketsPattern = `(|)` // 去除特殊符号
15 | miscPattern = `\s?【.*】` // 去除特殊符号
16 | netEasyV2 = "163cn" // 短链接
17 | shardModel = `http[s]?://[^ ]+`
18 | restfulModel = `playlist/(\d+)`
19 | )
20 |
21 | var (
22 | bracketsRegex = regexp.MustCompile(bracketsPattern)
23 | miscRegex = regexp.MustCompile(miscPattern)
24 | netEasyV2Regex = regexp.MustCompile(netEasyV2)
25 | shardModelRegex = regexp.MustCompile(shardModel)
26 | restfulModeRegex = regexp.MustCompile(restfulModel)
27 | )
28 |
29 | func GetQQMusicParam(link string) (string, error) {
30 | parse, err := url.ParseRequestURI(link)
31 | if err != nil {
32 | log.Errorf("fail to parse url: %v", err)
33 | return "", err
34 | }
35 | query, err := url.ParseQuery(parse.RawQuery)
36 | if err != nil {
37 | log.Errorf("fail to parse query: %v", err)
38 | return "", err
39 | }
40 | id := query.Get("id")
41 | return id, nil
42 | }
43 |
44 | func GetNetEasyParam(link string) (string, error) {
45 | link, err := standardUrl(link)
46 | if err != nil {
47 | log.Errorf("fail to standard url: %v", err)
48 | return "", err
49 | }
50 | parse, err := url.ParseRequestURI(link)
51 | if err != nil {
52 | log.Errorf("fail to parse url: %v", err)
53 | return "", err
54 | }
55 | query, err := url.ParseQuery(parse.RawQuery)
56 | if err != nil {
57 | log.Errorf("fail to parse query: %v", err)
58 | return "", err
59 | }
60 | return query.Get("id"), nil
61 | }
62 |
63 | func standardUrl(link string) (string, error) {
64 | // 格式化带中文的分享链接
65 | link = shardModelRegex.FindString(link)
66 | // 短链转换
67 | if netEasyV2Regex.MatchString(link) {
68 | return httputil.GetRedirectLocation(link)
69 | }
70 | // 匹配 restful 链接
71 | if match := restfulModeRegex.FindStringSubmatch(link); len(match) > 1 {
72 | link = "https://music.163.com/playlist?id=" + match[1]
73 | }
74 | return link, nil
75 | }
76 |
77 | // StandardSongName 获取标准化歌名
78 | func StandardSongName(songName string) string {
79 | return miscRegex.ReplaceAllString(replaceCNBrackets(songName), "")
80 | }
81 |
82 | // 将中文括号替换为英文括号
83 | func replaceCNBrackets(s string) string {
84 | return bracketsRegex.ReplaceAllStringFunc(s, func(m string) string {
85 | if m == "(" {
86 | return " (" // 左括号前面追加空格
87 | }
88 | return ")"
89 | })
90 | }
91 |
92 | func SyncMapToSortedSlice(trackIds []*models.TrackId, sm sync.Map) []string {
93 | strings := make([]string, 0, len(trackIds))
94 | for _, v := range trackIds {
95 | value, _ := sm.Load(v.Id)
96 | if elems, ok := value.(string); ok {
97 | strings = append(strings, elems)
98 | }
99 | }
100 | return strings
101 | }
102 |
--------------------------------------------------------------------------------
/misc/utils/qqmusic_encrypt.js:
--------------------------------------------------------------------------------
1 | this.window = this;
2 | var sign = null;
3 |
4 | !function(n, t) {
5 | "object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (n = n || self).getSecuritySign = t()
6 | } (this,
7 | function() {
8 | "use strict";
9 | var n = function() {
10 | if ("undefined" != typeof self) return self;
11 | if ("undefined" != typeof window) return window;
12 | if ("undefined" != typeof global) return global;
13 | throw new Error("unable to locate global object")
14 | } ();
15 | n.__sign_hash_20200305 = function(n) {
16 | function l(n, t) {
17 | var o = (65535 & n) + (65535 & t);
18 | return (n >> 16) + (t >> 16) + (o >> 16) << 16 | 65535 & o
19 | }
20 | function r(n, t, o, e, u, p) {
21 | return l((i = l(l(t, n), l(e, p))) << (r = u) | i >>> 32 - r, o);
22 | var i, r
23 | }
24 | function g(n, t, o, e, u, p, i) {
25 | return r(t & o | ~t & e, n, t, u, p, i)
26 | }
27 | function a(n, t, o, e, u, p, i) {
28 | return r(t & e | o & ~e, n, t, u, p, i)
29 | }
30 | function s(n, t, o, e, u, p, i) {
31 | return r(t ^ o ^ e, n, t, u, p, i)
32 | }
33 | function v(n, t, o, e, u, p, i) {
34 | return r(o ^ (t | ~e), n, t, u, p, i)
35 | }
36 | function t(n) {
37 | return function(n) {
38 | var t, o = "";
39 | for (t = 0; t < 32 * n.length; t += 8) o += String.fromCharCode(n[t >> 5] >>> t % 32 & 255);
40 | return o
41 | } (function(n, t) {
42 | n[t >> 5] |= 128 << t % 32,
43 | n[14 + (t + 64 >>> 9 << 4)] = t;
44 | var o, e, u, p, i, r = 1732584193,
45 | f = -271733879,
46 | h = -1732584194,
47 | c = 271733878;
48 | for (o = 0; o < n.length; o += 16) r = g(e = r, u = f, p = h, i = c, n[o], 7, -680876936),
49 | c = g(c, r, f, h, n[o + 1], 12, -389564586),
50 | h = g(h, c, r, f, n[o + 2], 17, 606105819),
51 | f = g(f, h, c, r, n[o + 3], 22, -1044525330),
52 | r = g(r, f, h, c, n[o + 4], 7, -176418897),
53 | c = g(c, r, f, h, n[o + 5], 12, 1200080426),
54 | h = g(h, c, r, f, n[o + 6], 17, -1473231341),
55 | f = g(f, h, c, r, n[o + 7], 22, -45705983),
56 | r = g(r, f, h, c, n[o + 8], 7, 1770035416),
57 | c = g(c, r, f, h, n[o + 9], 12, -1958414417),
58 | h = g(h, c, r, f, n[o + 10], 17, -42063),
59 | f = g(f, h, c, r, n[o + 11], 22, -1990404162),
60 | r = g(r, f, h, c, n[o + 12], 7, 1804603682),
61 | c = g(c, r, f, h, n[o + 13], 12, -40341101),
62 | h = g(h, c, r, f, n[o + 14], 17, -1502002290),
63 | r = a(r, f = g(f, h, c, r, n[o + 15], 22, 1236535329), h, c, n[o + 1], 5, -165796510),
64 | c = a(c, r, f, h, n[o + 6], 9, -1069501632),
65 | h = a(h, c, r, f, n[o + 11], 14, 643717713),
66 | f = a(f, h, c, r, n[o], 20, -373897302),
67 | r = a(r, f, h, c, n[o + 5], 5, -701558691),
68 | c = a(c, r, f, h, n[o + 10], 9, 38016083),
69 | h = a(h, c, r, f, n[o + 15], 14, -660478335),
70 | f = a(f, h, c, r, n[o + 4], 20, -405537848),
71 | r = a(r, f, h, c, n[o + 9], 5, 568446438),
72 | c = a(c, r, f, h, n[o + 14], 9, -1019803690),
73 | h = a(h, c, r, f, n[o + 3], 14, -187363961),
74 | f = a(f, h, c, r, n[o + 8], 20, 1163531501),
75 | r = a(r, f, h, c, n[o + 13], 5, -1444681467),
76 | c = a(c, r, f, h, n[o + 2], 9, -51403784),
77 | h = a(h, c, r, f, n[o + 7], 14, 1735328473),
78 | r = s(r, f = a(f, h, c, r, n[o + 12], 20, -1926607734), h, c, n[o + 5], 4, -378558),
79 | c = s(c, r, f, h, n[o + 8], 11, -2022574463),
80 | h = s(h, c, r, f, n[o + 11], 16, 1839030562),
81 | f = s(f, h, c, r, n[o + 14], 23, -35309556),
82 | r = s(r, f, h, c, n[o + 1], 4, -1530992060),
83 | c = s(c, r, f, h, n[o + 4], 11, 1272893353),
84 | h = s(h, c, r, f, n[o + 7], 16, -155497632),
85 | f = s(f, h, c, r, n[o + 10], 23, -1094730640),
86 | r = s(r, f, h, c, n[o + 13], 4, 681279174),
87 | c = s(c, r, f, h, n[o], 11, -358537222),
88 | h = s(h, c, r, f, n[o + 3], 16, -722521979),
89 | f = s(f, h, c, r, n[o + 6], 23, 76029189),
90 | r = s(r, f, h, c, n[o + 9], 4, -640364487),
91 | c = s(c, r, f, h, n[o + 12], 11, -421815835),
92 | h = s(h, c, r, f, n[o + 15], 16, 530742520),
93 | r = v(r, f = s(f, h, c, r, n[o + 2], 23, -995338651), h, c, n[o], 6, -198630844),
94 | c = v(c, r, f, h, n[o + 7], 10, 1126891415),
95 | h = v(h, c, r, f, n[o + 14], 15, -1416354905),
96 | f = v(f, h, c, r, n[o + 5], 21, -57434055),
97 | r = v(r, f, h, c, n[o + 12], 6, 1700485571),
98 | c = v(c, r, f, h, n[o + 3], 10, -1894986606),
99 | h = v(h, c, r, f, n[o + 10], 15, -1051523),
100 | f = v(f, h, c, r, n[o + 1], 21, -2054922799),
101 | r = v(r, f, h, c, n[o + 8], 6, 1873313359),
102 | c = v(c, r, f, h, n[o + 15], 10, -30611744),
103 | h = v(h, c, r, f, n[o + 6], 15, -1560198380),
104 | f = v(f, h, c, r, n[o + 13], 21, 1309151649),
105 | r = v(r, f, h, c, n[o + 4], 6, -145523070),
106 | c = v(c, r, f, h, n[o + 11], 10, -1120210379),
107 | h = v(h, c, r, f, n[o + 2], 15, 718787259),
108 | f = v(f, h, c, r, n[o + 9], 21, -343485551),
109 | r = l(r, e),
110 | f = l(f, u),
111 | h = l(h, p),
112 | c = l(c, i);
113 | return [r, f, h, c]
114 | } (function(n) {
115 | var t, o = [];
116 | for (o[(n.length >> 2) - 1] = void 0, t = 0; t < o.length; t += 1) o[t] = 0;
117 | for (t = 0; t < 8 * n.length; t += 8) o[t >> 5] |= (255 & n.charCodeAt(t / 8)) << t % 32;
118 | return o
119 | } (n), 8 * n.length))
120 | }
121 | function o(n) {
122 | return t(unescape(encodeURIComponent(n)))
123 | }
124 | return function(n) {
125 | var t, o, e = "0123456789abcdef",
126 | u = "";
127 | for (o = 0; o < n.length; o += 1) t = n.charCodeAt(o),
128 | u += e.charAt(t >>> 4 & 15) + e.charAt(15 & t);
129 | return u
130 | } (o(n))
131 | },
132 | function r(f, h, c, l, g) {
133 | g = g || [[this], [{}]];
134 | for (var t = [], o = null, n = [function() {
135 | return ! 0
136 | },
137 | function() {},
138 | function() {
139 | g.length = c[h++]
140 | },
141 | function() {
142 | g.push(c[h++])
143 | },
144 | function() {
145 | g.pop()
146 | },
147 | function() {
148 | var n = c[h++],
149 | t = g[g.length - 2 - n];
150 | g[g.length - 2 - n] = g.pop(),
151 | g.push(t)
152 | },
153 | function() {
154 | g.push(g[g.length - 1])
155 | },
156 | function() {
157 | g.push([g.pop(), g.pop()].reverse())
158 | },
159 | function() {
160 | g.push([l, g.pop()])
161 | },
162 | function() {
163 | g.push([g.pop()])
164 | },
165 | function() {
166 | var n = g.pop();
167 | g.push(n[0][n[1]])
168 | },
169 | function() {
170 | g.push(g[g.pop()[0]][0])
171 | },
172 | function() {
173 | var n = g[g.length - 2];
174 | n[0][n[1]] = g[g.length - 1]
175 | },
176 | function() {
177 | g[g[g.length - 2][0]][0] = g[g.length - 1]
178 | },
179 | function() {
180 | var n = g.pop(),
181 | t = g.pop();
182 | g.push([t[0][t[1]], n])
183 | },
184 | function() {
185 | var n = g.pop();
186 | g.push([g[g.pop()][0], n])
187 | },
188 | function() {
189 | var n = g.pop();
190 | g.push(delete n[0][n[1]])
191 | },
192 | function() {
193 | var n = [];
194 | for (var t in g.pop()) n.push(t);
195 | g.push(n)
196 | },
197 | function() {
198 | g[g.length - 1].length ? g.push(g[g.length - 1].shift(), !0) : g.push(void 0, !1)
199 | },
200 | function() {
201 | var n = g[g.length - 2],
202 | t = Object.getOwnPropertyDescriptor(n[0], n[1]) || {
203 | configurable: !0,
204 | enumerable: !0
205 | };
206 | t.get = g[g.length - 1],
207 | Object.defineProperty(n[0], n[1], t)
208 | },
209 | function() {
210 | var n = g[g.length - 2],
211 | t = Object.getOwnPropertyDescriptor(n[0], n[1]) || {
212 | configurable: !0,
213 | enumerable: !0
214 | };
215 | t.set = g[g.length - 1],
216 | Object.defineProperty(n[0], n[1], t)
217 | },
218 | function() {
219 | h = c[h++]
220 | },
221 | function() {
222 | var n = c[h++];
223 | g[g.length - 1] && (h = n)
224 | },
225 | function() {
226 | throw g[g.length - 1]
227 | },
228 | function() {
229 | var n = c[h++],
230 | t = n ? g.slice( - n) : [];
231 | g.length -= n,
232 | g.push(g.pop().apply(l, t))
233 | },
234 | function() {
235 | var n = c[h++],
236 | t = n ? g.slice( - n) : [];
237 | g.length -= n;
238 | var o = g.pop();
239 | g.push(o[0][o[1]].apply(o[0], t))
240 | },
241 | function() {
242 | var n = c[h++],
243 | t = n ? g.slice( - n) : [];
244 | g.length -= n,
245 | t.unshift(null),
246 | g.push(new(Function.prototype.bind.apply(g.pop(), t)))
247 | },
248 | function() {
249 | var n = c[h++],
250 | t = n ? g.slice( - n) : [];
251 | g.length -= n,
252 | t.unshift(null);
253 | var o = g.pop();
254 | g.push(new(Function.prototype.bind.apply(o[0][o[1]], t)))
255 | },
256 | function() {
257 | g.push(!g.pop())
258 | },
259 | function() {
260 | g.push(~g.pop())
261 | },
262 | function() {
263 | g.push(typeof g.pop())
264 | },
265 | function() {
266 | g[g.length - 2] = g[g.length - 2] == g.pop()
267 | },
268 | function() {
269 | g[g.length - 2] = g[g.length - 2] === g.pop()
270 | },
271 | function() {
272 | g[g.length - 2] = g[g.length - 2] > g.pop()
273 | },
274 | function() {
275 | g[g.length - 2] = g[g.length - 2] >= g.pop()
276 | },
277 | function() {
278 | g[g.length - 2] = g[g.length - 2] << g.pop()
279 | },
280 | function() {
281 | g[g.length - 2] = g[g.length - 2] >> g.pop()
282 | },
283 | function() {
284 | g[g.length - 2] = g[g.length - 2] >>> g.pop()
285 | },
286 | function() {
287 | g[g.length - 2] = g[g.length - 2] + g.pop()
288 | },
289 | function() {
290 | g[g.length - 2] = g[g.length - 2] - g.pop()
291 | },
292 | function() {
293 | g[g.length - 2] = g[g.length - 2] * g.pop()
294 | },
295 | function() {
296 | g[g.length - 2] = g[g.length - 2] / g.pop()
297 | },
298 | function() {
299 | g[g.length - 2] = g[g.length - 2] % g.pop()
300 | },
301 | function() {
302 | g[g.length - 2] = g[g.length - 2] | g.pop()
303 | },
304 | function() {
305 | g[g.length - 2] = g[g.length - 2] & g.pop()
306 | },
307 | function() {
308 | g[g.length - 2] = g[g.length - 2] ^ g.pop()
309 | },
310 | function() {
311 | g[g.length - 2] = g[g.length - 2] in g.pop()
312 | },
313 | function() {
314 | g[g.length - 2] = g[g.length - 2] instanceof g.pop()
315 | },
316 | function() {
317 | g[g[g.length - 1][0]] = void 0 === g[g[g.length - 1][0]] ? [] : g[g[g.length - 1][0]]
318 | },
319 | function() {
320 | for (var e = c[h++], u = [], n = c[h++], t = c[h++], p = [], o = 0; o < n; o++) u[c[h++]] = g[c[h++]];
321 | for (var i = 0; i < t; i++) p[i] = c[h++];
322 | g.push(function n() {
323 | var t = u.slice(0);
324 | t[0] = [this],
325 | t[1] = [arguments],
326 | t[2] = [n];
327 | for (var o = 0; o < p.length && o < arguments.length; o++) 0 < p[o] && (t[p[o]] = [arguments[o]]);
328 | return r(f, e, c, l, t)
329 | })
330 | },
331 | function() {
332 | t.push([c[h++], g.length, c[h++]])
333 | },
334 | function() {
335 | t.pop()
336 | },
337 | function() {
338 | return !! o
339 | },
340 | function() {
341 | o = null
342 | },
343 | function() {
344 | g[g.length - 1] += String.fromCharCode(c[h++])
345 | },
346 | function() {
347 | g.push("")
348 | },
349 | function() {
350 | g.push(void 0)
351 | },
352 | function() {
353 | g.push(null)
354 | },
355 | function() {
356 | g.push(!0)
357 | },
358 | function() {
359 | g.push(!1)
360 | },
361 | function() {
362 | g.length -= c[h++]
363 | },
364 | function() {
365 | g[g.length - 1] = c[h++]
366 | },
367 | function() {
368 | var n = g.pop(),
369 | t = g[g.length - 1];
370 | t[0][t[1]] = g[n[0]][0]
371 | },
372 | function() {
373 | var n = g.pop(),
374 | t = g[g.length - 1];
375 | t[0][t[1]] = n[0][n[1]]
376 | },
377 | function() {
378 | var n = g.pop(),
379 | t = g[g.length - 1];
380 | g[t[0]][0] = g[n[0]][0]
381 | },
382 | function() {
383 | var n = g.pop(),
384 | t = g[g.length - 1];
385 | g[t[0]][0] = n[0][n[1]]
386 | },
387 | function() {
388 | g[g.length - 2] = g[g.length - 2] < g.pop()
389 | },
390 | function() {
391 | g[g.length - 2] = g[g.length - 2] <= g.pop()
392 | }];;) try {
393 | for (; ! n[c[h++]](););
394 | if (o) throw o;
395 | return g.pop()
396 | } catch(n) {
397 | var e = t.pop();
398 | if (void 0 === e) throw n;
399 | o = n,
400 | h = e[0],
401 | g.length = e[1],
402 | e[2] && (g[e[2]][0] = o)
403 | }
404 | } (120731, 0, [21, 34, 50, 100, 57, 50, 102, 50, 98, 99, 101, 52, 54, 97, 52, 99, 55, 56, 52, 49, 57, 54, 57, 49, 56, 98, 102, 100, 100, 48, 48, 55, 55, 102, 2, 10, 3, 2, 9, 48, 61, 3, 9, 48, 61, 4, 9, 48, 61, 5, 9, 48, 61, 6, 9, 48, 61, 7, 9, 48, 61, 8, 9, 48, 61, 9, 9, 48, 4, 21, 427, 54, 2, 15, 3, 2, 9, 48, 61, 3, 9, 48, 61, 4, 9, 48, 61, 5, 9, 48, 61, 6, 9, 48, 61, 7, 9, 48, 61, 8, 9, 48, 61, 9, 9, 48, 61, 10, 9, 48, 61, 11, 9, 48, 61, 12, 9, 48, 61, 13, 9, 48, 61, 14, 9, 48, 61, 10, 9, 55, 54, 97, 54, 98, 54, 99, 54, 100, 54, 101, 54, 102, 54, 103, 54, 104, 54, 105, 54, 106, 54, 107, 54, 108, 54, 109, 54, 110, 54, 111, 54, 112, 54, 113, 54, 114, 54, 115, 54, 116, 54, 117, 54, 118, 54, 119, 54, 120, 54, 121, 54, 122, 54, 48, 54, 49, 54, 50, 54, 51, 54, 52, 54, 53, 54, 54, 54, 55, 54, 56, 54, 57, 13, 4, 61, 11, 9, 55, 54, 77, 54, 97, 54, 116, 54, 104, 8, 55, 54, 102, 54, 108, 54, 111, 54, 111, 54, 114, 14, 55, 54, 77, 54, 97, 54, 116, 54, 104, 8, 55, 54, 114, 54, 97, 54, 110, 54, 100, 54, 111, 54, 109, 14, 25, 0, 3, 4, 9, 11, 3, 3, 9, 11, 39, 3, 1, 38, 40, 3, 3, 9, 11, 38, 25, 1, 13, 4, 61, 12, 9, 55, 13, 4, 61, 13, 9, 3, 0, 13, 4, 4, 3, 13, 9, 11, 3, 11, 9, 11, 66, 22, 306, 4, 21, 422, 24, 4, 3, 14, 9, 55, 54, 77, 54, 97, 54, 116, 54, 104, 8, 55, 54, 102, 54, 108, 54, 111, 54, 111, 54, 114, 14, 55, 54, 77, 54, 97, 54, 116, 54, 104, 8, 55, 54, 114, 54, 97, 54, 110, 54, 100, 54, 111, 54, 109, 14, 25, 0, 3, 10, 9, 55, 54, 108, 54, 101, 54, 110, 54, 103, 54, 116, 54, 104, 15, 10, 40, 25, 1, 13, 4, 61, 12, 9, 6, 11, 3, 10, 9, 3, 14, 9, 11, 15, 10, 38, 13, 4, 61, 13, 9, 6, 11, 6, 5, 1, 5, 0, 3, 1, 38, 13, 4, 61, 0, 5, 0, 43, 4, 21, 291, 61, 3, 12, 9, 11, 0, 3, 9, 9, 49, 72, 0, 2, 3, 4, 13, 4, 61, 8, 9, 21, 721, 3, 2, 8, 3, 2, 9, 48, 61, 3, 9, 48, 61, 4, 9, 48, 61, 5, 9, 48, 61, 6, 9, 48, 61, 7, 9, 48, 4, 55, 54, 115, 54, 101, 54, 108, 54, 102, 8, 10, 30, 55, 54, 117, 54, 110, 54, 100, 54, 101, 54, 102, 54, 105, 54, 110, 54, 101, 54, 100, 32, 28, 22, 510, 4, 21, 523, 22, 4, 55, 54, 115, 54, 101, 54, 108, 54, 102, 8, 10, 0, 55, 54, 119, 54, 105, 54, 110, 54, 100, 54, 111, 54, 119, 8, 10, 30, 55, 54, 117, 54, 110, 54, 100, 54, 101, 54, 102, 54, 105, 54, 110, 54, 101, 54, 100, 32, 28, 22, 566, 4, 21, 583, 3, 4, 55, 54, 119, 54, 105, 54, 110, 54, 100, 54, 111, 54, 119, 8, 10, 0, 55, 54, 103, 54, 108, 54, 111, 54, 98, 54, 97, 54, 108, 8, 10, 30, 55, 54, 117, 54, 110, 54, 100, 54, 101, 54, 102, 54, 105, 54, 110, 54, 101, 54, 100, 32, 28, 22, 626, 4, 21, 643, 25, 4, 55, 54, 103, 54, 108, 54, 111, 54, 98, 54, 97, 54, 108, 8, 10, 0, 55, 54, 69, 54, 114, 54, 114, 54, 111, 54, 114, 8, 55, 54, 117, 54, 110, 54, 97, 54, 98, 54, 108, 54, 101, 54, 32, 54, 116, 54, 111, 54, 32, 54, 108, 54, 111, 54, 99, 54, 97, 54, 116, 54, 101, 54, 32, 54, 103, 54, 108, 54, 111, 54, 98, 54, 97, 54, 108, 54, 32, 54, 111, 54, 98, 54, 106, 54, 101, 54, 99, 54, 116, 27, 1, 23, 56, 0, 49, 444, 0, 0, 24, 0, 13, 4, 61, 8, 9, 55, 54, 95, 54, 95, 54, 103, 54, 101, 54, 116, 54, 83, 54, 101, 54, 99, 54, 117, 54, 114, 54, 105, 54, 116, 54, 121, 54, 83, 54, 105, 54, 103, 54, 110, 15, 21, 1126, 49, 2, 14, 3, 2, 9, 48, 61, 3, 9, 48, 61, 4, 9, 48, 61, 5, 9, 48, 61, 6, 9, 48, 61, 7, 9, 48, 61, 8, 9, 48, 61, 9, 9, 48, 61, 10, 9, 48, 61, 11, 9, 48, 61, 9, 9, 55, 54, 108, 54, 111, 54, 99, 54, 97, 54, 116, 54, 105, 54, 111, 54, 110, 8, 10, 30, 55, 54, 117, 54, 110, 54, 100, 54, 101, 54, 102, 54, 105, 54, 110, 54, 101, 54, 100, 32, 28, 22, 862, 21, 932, 21, 4, 55, 54, 108, 54, 111, 54, 99, 54, 97, 54, 116, 54, 105, 54, 111, 54, 110, 8, 55, 54, 104, 54, 111, 54, 115, 54, 116, 14, 55, 54, 105, 54, 110, 54, 100, 54, 101, 54, 120, 54, 79, 54, 102, 14, 55, 54, 121, 54, 46, 54, 113, 54, 113, 54, 46, 54, 99, 54, 111, 54, 109, 25, 1, 3, 0, 3, 1, 39, 32, 22, 963, 4, 55, 54, 67, 54, 74, 54, 66, 54, 80, 54, 65, 54, 67, 54, 114, 54, 82, 54, 117, 54, 78, 54, 121, 54, 55, 21, 974, 50, 4, 3, 12, 9, 11, 3, 8, 3, 10, 24, 2, 13, 4, 61, 10, 9, 3, 13, 9, 55, 54, 95, 54, 95, 54, 115, 54, 105, 54, 103, 54, 110, 54, 95, 54, 104, 54, 97, 54, 115, 54, 104, 54, 95, 54, 50, 54, 48, 54, 50, 54, 48, 54, 48, 54, 51, 54, 48, 54, 53, 15, 10, 22, 1030, 21, 1087, 22, 4, 3, 13, 9, 55, 54, 95, 54, 95, 54, 115, 54, 105, 54, 103, 54, 110, 54, 95, 54, 104, 54, 97, 54, 115, 54, 104, 54, 95, 54, 50, 54, 48, 54, 50, 54, 48, 54, 48, 54, 51, 54, 48, 54, 53, 15, 3, 9, 9, 11, 3, 3, 9, 11, 38, 25, 1, 13, 4, 61, 11, 9, 3, 12, 9, 11, 3, 10, 3, 53, 3, 37, 39, 24, 2, 13, 4, 4, 55, 54, 122, 54, 122, 54, 97, 3, 11, 9, 11, 38, 3, 10, 9, 11, 38, 0, 49, 771, 2, 1, 12, 9, 13, 8, 3, 12, 4, 4, 56, 0], n);
405 | var t = n.__getSecuritySign;
406 | sign = t;
407 | return t;
408 | });
409 |
410 | function get_sign(data){
411 | return sign(data)
412 | };
--------------------------------------------------------------------------------
/misc/utils/qqmusic_sign.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | "strings"
7 | )
8 |
9 | func Encrypt(param string) string {
10 | k1 := map[string]int{
11 | "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9,
12 | "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15,
13 | }
14 | l1 := []int{212, 45, 80, 68, 195, 163, 163, 203, 157, 220, 254, 91, 204, 79, 104, 6}
15 | t := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
16 |
17 | //jsonData, _ := json.Marshal()
18 | md5Hash := md5.Sum([]byte(param))
19 | md5Str := strings.ToUpper(hex.EncodeToString(md5Hash[:]))
20 |
21 | t1 := selectChars(md5Str, []int{21, 4, 9, 26, 16, 20, 27, 30})
22 | t3 := selectChars(md5Str, []int{18, 11, 3, 2, 1, 7, 6, 25})
23 |
24 | ls2 := make([]int, 0, 16)
25 | for i := 0; i < 16; i++ {
26 | x1 := k1[string(md5Str[i*2])]
27 | x2 := k1[string(md5Str[i*2+1])]
28 | x3 := (x1*16 ^ x2) ^ l1[i]
29 | ls2 = append(ls2, x3)
30 | }
31 |
32 | ls3 := make([]string, 0, 7)
33 | for i := 0; i < 6; i++ {
34 | if i == 5 {
35 | ls3 = append(ls3, string(t[ls2[len(ls2)-1]>>2]), string(t[(ls2[len(ls2)-1]&3)<<4]))
36 | } else {
37 | x4 := ls2[i*3] >> 2
38 | x5 := (ls2[i*3+1] >> 4) ^ ((ls2[i*3] & 3) << 4)
39 | x6 := (ls2[i*3+2] >> 6) ^ ((ls2[i*3+1] & 15) << 2)
40 | x7 := 63 & ls2[i*3+2]
41 | ls3 = append(ls3, string(t[x4])+string(t[x5])+string(t[x6])+string(t[x7]))
42 | }
43 | }
44 |
45 | t2 := strings.Join(ls3, "")
46 | t2 = strings.ReplaceAll(t2, "[\\/+]", "")
47 | sign := "zzb" + strings.ToLower(t1+t2+t3)
48 | return sign
49 | }
50 |
51 | func selectChars(str string, indices []int) string {
52 | var result string
53 | for _, index := range indices {
54 | result += string(str[index])
55 | }
56 | return result
57 | }
58 |
--------------------------------------------------------------------------------
/misc/utils/qqmusic_sign_native.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | _ "embed"
5 |
6 | "github.com/robertkrimen/otto"
7 |
8 | "GoMusic/misc/log"
9 | )
10 |
11 | //go:embed qqmusic_encrypt.js
12 | var qqmusicJS string
13 |
14 | var vm = otto.New()
15 |
16 | func init() {
17 | if _, err := vm.Run(qqmusicJS); err != nil {
18 | log.Errorf("fail to run js: %v", err)
19 | panic(err)
20 | }
21 | }
22 |
23 | func GetSign(data string) (string, error) {
24 | value, err := vm.Call("get_sign", nil, data)
25 | if err != nil {
26 | log.Errorf("fail to call js: %v", err)
27 | return "", err
28 | }
29 | return value.String(), nil
30 | }
31 |
--------------------------------------------------------------------------------
/misc/utils/qqmusic_sign_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestEncry(t *testing.T) {
9 | info := `{"req_0":{"module":"music.srfDissInfo.aiDissInfo","method":"uniform_get_Dissinfo","param":{"disstid":7364061065,"enc_host_uin":"","tag":1,"userinfo":1,"song_begin":1,"song_num":1024}},"comm":{"g_tk":5381,"uin":0,"format":"json","platform":"h5"}}`
10 | signNative, _ := GetSign(info)
11 | fmt.Println(signNative)
12 | sign := Encrypt(info)
13 | fmt.Println(sign)
14 | }
15 |
16 | func BenchmarkName(b *testing.B) {
17 | msg := `{"req_0":{"module":"music.srfDissInfo.aiDissInfo","method":"uniform_get_Dissinfo","param":{"disstid":7364061065,"enc_host_uin":"","tag":1,"userinfo":1,"song_begin":1,"song_num":1024}},"comm":{"g_tk":5381,"uin":0,"format":"json","platform":"h5"}}`
18 | b.Run("js", func(b *testing.B) {
19 | for i := 0; i < b.N; i++ {
20 | GetSign(msg)
21 | }
22 | })
23 | b.Run("go", func(b *testing.B) {
24 | for i := 0; i < b.N; i++ {
25 | Encrypt(msg)
26 | }
27 | })
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/repo/cache/redis.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "sync"
7 | "time"
8 |
9 | "github.com/go-redis/redis/v8"
10 |
11 | "GoMusic/misc/log"
12 | )
13 |
14 | const (
15 | month = 30 * 24 * time.Hour
16 | )
17 |
18 | var (
19 | ctx = context.Background()
20 | rdb *redis.Client
21 | )
22 |
23 | func init() {
24 | rdb = redis.NewClient(&redis.Options{
25 | Addr: "127.0.0.1:16379", // redis 服务端地址
26 | Password: "SzW7fh2Fs5d2ypwT", // redis 密码
27 | DB: 0,
28 | })
29 | }
30 |
31 | func SetKey(key string, value string) error {
32 | return rdb.Set(ctx, key, value, 30*time.Second).Err() // 缓存 30 秒
33 | }
34 |
35 | func GetKey(key string) (string, error) {
36 | val, err := rdb.Get(ctx, key).Result()
37 | if err != redis.Nil && err != nil {
38 | return "", err
39 | }
40 | return val, nil
41 | }
42 |
43 | func MGet(keys ...string) ([]interface{}, error) {
44 | if len(keys) == 0 {
45 | return nil, errors.New("keys is empty")
46 | }
47 | result, err := rdb.MGet(ctx, keys...).Result()
48 | if err != nil {
49 | log.Errorf("MGet error: %v", err)
50 | }
51 | return result, err
52 | }
53 |
54 | func MSet(kv sync.Map) error {
55 | pipeline := rdb.Pipeline()
56 | kv.Range(func(k, v any) bool {
57 | // 缓存 30 天
58 | pipeline.Set(ctx, k.(string), v, month)
59 | return true
60 | })
61 | // 不关注单个命令的执行结果,只关注 pipeline 执行的结果
62 | if _, err := pipeline.Exec(ctx); err != nil {
63 | log.Error("MSet error: ", err)
64 | return err
65 | }
66 | return nil
67 | }
68 |
--------------------------------------------------------------------------------
/repo/cache/redis_test.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestSet(t *testing.T) {
10 | msg := []string{"test1", "value1"}
11 | err := SetKey(msg[0], msg[1])
12 | assert.NoError(t, err)
13 | rs, err := GetKey(msg[0])
14 | assert.NoError(t, err)
15 | assert.Equal(t, msg[1], rs)
16 | }
17 |
--------------------------------------------------------------------------------
/repo/db/mysql.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | _ "github.com/go-sql-driver/mysql"
5 | "gorm.io/driver/mysql"
6 | "gorm.io/gorm"
7 | "gorm.io/gorm/clause"
8 |
9 | "GoMusic/misc/log"
10 | "GoMusic/misc/models"
11 | )
12 |
13 | var db *gorm.DB
14 |
15 | func init() {
16 | dsn := "go_music:12345678@tcp(127.0.0.1:3306)/go_music?charset=utf8mb4&parseTime=True&loc=Local"
17 | //dsn := "root:12345678@tcp(127.0.0.1:3306)/go_music?charset=utf8mb4&parseTime=True&loc=Local"
18 | open, err := gorm.Open(mysql.Open(dsn))
19 | if err != nil {
20 | log.Errorf("数据库连接失败:%v", err)
21 | panic(err)
22 | }
23 | db = open
24 | // 自动创建表
25 | db.AutoMigrate(&models.NetEasySong{})
26 |
27 | // 调用自定义迁移函数修改表结构
28 | if err := MigrateNameField(db); err != nil {
29 | log.Errorf("failed to migrate database: %v", err)
30 | }
31 | }
32 |
33 | func MigrateNameField(db *gorm.DB) error {
34 | // 使用原生 SQL 来修改字段长度
35 | return db.Exec("ALTER TABLE net_easy_songs MODIFY name VARCHAR(512);").Error
36 | }
37 |
38 | func BatchGetSongById(ids []uint) (map[uint]string, error) {
39 | var netEasySongs []*models.NetEasySong
40 | // 仅选择 id 和 name 列
41 | err := db.Select("id, name").Where("id in ?", ids).Find(&netEasySongs).Error
42 | if err != nil {
43 | log.Errorf("查询数据库失败:%v", err)
44 | return nil, err
45 | }
46 |
47 | // 歌曲id:歌曲信息
48 | netEasySongMap := make(map[uint]string, len(netEasySongs))
49 | for _, v := range netEasySongs {
50 | netEasySongMap[v.Id] = v.Name
51 | }
52 | return netEasySongMap, nil
53 | }
54 |
55 | func BatchInsertSong(netEasySongs []*models.NetEasySong) error {
56 | // 如果 Duplicate primary key 则执行 update 操作
57 | err := db.Clauses(clause.OnConflict{
58 | UpdateAll: true,
59 | }).CreateInBatches(netEasySongs, 500).Error
60 | if err != nil {
61 | log.Errorf("数据库插入失败:%v", err)
62 | }
63 | return err
64 | }
65 |
66 | func BatchDelSong(ids []int) error {
67 | err := db.Delete(&models.NetEasySong{}, ids).Error
68 | if err != nil {
69 | log.Errorf("数据库删除数据失败:%v", err)
70 | }
71 | return err
72 | }
73 |
--------------------------------------------------------------------------------
/repo/db/mysql_test.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 |
8 | "GoMusic/misc/models"
9 | )
10 |
11 | func TestBatchDelAndSet(t *testing.T) {
12 | var songs []*models.NetEasySong
13 | songs = append(songs, &models.NetEasySong{
14 | Id: 5241457,
15 | Name: "小酒窝(Live) - 蔡卓妍 / 林俊杰",
16 | })
17 | songs = append(songs, &models.NetEasySong{
18 | Id: 1935948203,
19 | Name: "星河万里 - 王大毛",
20 | })
21 |
22 | // Del
23 | err := BatchDelSong([]int{5241457, 1935948203})
24 | assert.NoError(t, err)
25 |
26 | // Set
27 | err = BatchInsertSong(songs)
28 | assert.NoError(t, err)
29 | }
30 |
31 | func TestBatchGet(t *testing.T) {
32 | songs, err := BatchGetSongById([]uint{5241457, 1935948203})
33 | assert.NoError(t, err)
34 | assert.Equal(t, "小酒窝(Live) - 蔡卓妍 / 林俊杰", songs[5241457])
35 | assert.Equal(t, "星河万里 - 王大毛", songs[1935948203])
36 | }
37 |
--------------------------------------------------------------------------------
/static/README.md:
--------------------------------------------------------------------------------
1 | # go-music
2 |
3 | ## Project setup
4 | ```
5 | npm install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | npm run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | npm run build
16 | ```
17 |
18 | ### Lints and fixes files
19 | ```
20 | npm run lint
21 | ```
22 |
23 | ### Customize configuration
24 | See [Configuration Reference](https://cli.vuejs.org/config/).
25 |
--------------------------------------------------------------------------------
/static/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/static/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "esnext",
5 | "baseUrl": "./",
6 | "moduleResolution": "node",
7 | "paths": {
8 | "@/*": [
9 | "src/*"
10 | ]
11 | },
12 | "lib": [
13 | "esnext",
14 | "dom",
15 | "dom.iterable",
16 | "scripthost"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/static/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "go-music",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "axios": "^1.6.8",
12 | "core-js": "^3.8.3",
13 | "element-plus": "^2.6.3",
14 | "vue": "^3.2.13"
15 | },
16 | "devDependencies": {
17 | "@babel/core": "^7.12.16",
18 | "@babel/eslint-parser": "^7.12.16",
19 | "@vue/cli-plugin-babel": "~5.0.0",
20 | "@vue/cli-plugin-eslint": "~5.0.0",
21 | "@vue/cli-service": "~5.0.0",
22 | "eslint": "^7.32.0",
23 | "eslint-plugin-vue": "^8.0.3"
24 | },
25 | "eslintConfig": {
26 | "root": true,
27 | "env": {
28 | "node": true
29 | },
30 | "extends": [
31 | "plugin:vue/vue3-essential",
32 | "eslint:recommended"
33 | ],
34 | "parserOptions": {
35 | "parser": "@babel/eslint-parser"
36 | },
37 | "rules": {}
38 | },
39 | "browserslist": [
40 | "> 1%",
41 | "last 2 versions",
42 | "not dead",
43 | "not ie 11"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/static/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ state.isEnglish ? i18n.title_first.en : i18n.title_first.zh }}
11 |
{{ state.isEnglish ? i18n.title_second.en : i18n.title_second.zh }}
12 |
128 | {{ 129 | state.isEnglish ? i18n.tipBetweenNetEaseAndQQ.en : i18n.tipBetweenNetEaseAndQQ.zh 130 | }} 131 | {{ state.isEnglish ? i18n.see.en : i18n.see.zh }}GitHub issue 133 |134 |
{{ state.isEnglish ? i18n.sponsorHint.en : i18n.sponsorHint.zh }}
139 |