├── .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 | [![Star History Chart](https://api.star-history.com/svg?repos=Bistutu/GoMusic&type=Date)](https://star-history.com/#Bistutu/GoMusic&Date) 41 | 42 | # 赞赏码 43 | 44 | 网站免费、开源、保持简单,如果你想支持作者,请使用微信扫描赞赏码,以下是赞赏榜的前10名赞助者(最后更新 2025.2.26) 45 | 46 | wechat 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 | [![Star History Chart](https://api.star-history.com/svg?repos=Bistutu/GoMusic&type=Date)](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 | [![Star History Chart](https://api.star-history.com/svg?repos=Bistutu/GoMusic&type=Date)](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 | 4 | 5 | 6 | 7 | 8 | 9 | 音乐迁移-GoMusic 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /static/src/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 156 | 157 | 441 | 442 | 443 | -------------------------------------------------------------------------------- /static/src/assets/approve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bistutu/GoMusic/0f64ebaf9a2668eabbbf77a5b0a1ac35bdf32498/static/src/assets/approve.png -------------------------------------------------------------------------------- /static/src/main.js: -------------------------------------------------------------------------------- 1 | import {createApp} from 'vue' 2 | import App from './App.vue' 3 | import ElementPlus from 'element-plus' 4 | import 'element-plus/dist/index.css' 5 | 6 | const app = createApp(App); 7 | app.use(ElementPlus); 8 | app.mount('#app') 9 | -------------------------------------------------------------------------------- /static/src/utils/tip.js: -------------------------------------------------------------------------------- 1 | import {ElMessage} from "element-plus"; 2 | 3 | const prefix = ""; 4 | 5 | function _sendErrorMessage(message) { 6 | ElMessage({message: prefix + message, type: 'error'}); 7 | } 8 | 9 | function _sendSuccessMessage(message) { 10 | ElMessage({message: prefix + message, type: 'success'}); 11 | } 12 | 13 | // limit the frequency of function calls 14 | export function throttle(fn, interval) { 15 | let last = 0; // 维护上次执行的时间 16 | return function (...args) { 17 | const now = Date.now(); 18 | if (now - last >= interval) { 19 | last = now; 20 | fn.apply(this, args); // 使用 apply 来传递参数数组 21 | } 22 | }; 23 | } 24 | 25 | // 使用防抖函数包装,1s 内只能发送一次消息 26 | export const sendErrorMessage = throttle(_sendErrorMessage, 1000); 27 | export const sendSuccessMessage = throttle(_sendSuccessMessage, 1000); 28 | -------------------------------------------------------------------------------- /static/src/utils/utils.js: -------------------------------------------------------------------------------- 1 | // 检查是否为有效链接 2 | const isValidUrl = (url) => { 3 | const urlRegex = /http[s]?:\/\/[^\s]+/; 4 | return urlRegex.test(url); 5 | }; 6 | 7 | // 检查是否为支持的平台 8 | const isSupportedPlatform = (url) => { 9 | const supportedPlatformsRegex = /(163)|(qq)|(qishui)|(douyin)/; 10 | return supportedPlatformsRegex.test(url); 11 | }; 12 | 13 | 14 | export {isValidUrl, isSupportedPlatform}; -------------------------------------------------------------------------------- /static/vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vue/cli-service') 2 | module.exports = defineConfig({ 3 | transpileDependencies: true, 4 | pages: { 5 | index: { 6 | entry: 'src/main.js', 7 | title: '音乐迁移-GoMusic' 8 | } 9 | } 10 | }) 11 | --------------------------------------------------------------------------------