├── WebSourceCode ├── README.md ├── auto-imports.d.ts ├── src │ ├── global │ │ └── MainStyle.scss │ ├── vite-env.d.ts │ ├── store │ │ ├── SystemStore.ts │ │ └── BasicStore.ts │ ├── components │ │ ├── Download.vue │ │ ├── SimpleCode.vue │ │ ├── NotFound.vue │ │ ├── Index.vue │ │ ├── Home.vue │ │ ├── Cloud.vue │ │ ├── SearchMusic.vue │ │ └── Netease.vue │ ├── assets │ │ └── vue.svg │ ├── main.ts │ ├── utils │ │ ├── type │ │ │ ├── NeteaseMusicPlayList.ts │ │ │ ├── BasicType.ts │ │ │ ├── UserInfoDetail.ts │ │ │ ├── NetEasePlayListSong.ts │ │ │ └── CloudResponse.ts │ │ ├── Utils.ts │ │ └── Http.ts │ ├── components.d.ts │ ├── App.vue │ ├── style.css │ ├── router │ │ └── router.ts │ ├── component │ │ └── UserInfo.vue │ └── auto-imports.d.ts ├── tsconfig.node.json ├── .gitignore ├── index.html ├── tsconfig.json ├── package.json ├── components.d.ts └── vite.config.ts ├── img.png ├── img_1.png ├── img_10.png ├── img_11.png ├── img_12.png ├── img_13.png ├── img_14.png ├── img_15.png ├── img_16.png ├── img_17.png ├── img_2.png ├── img_3.png ├── img_4.png ├── img_5.png ├── img_6.png ├── img_7.png ├── img_8.png ├── img_9.png ├── md ├── media │ ├── img.png │ ├── img_2.png │ └── 16589902696730 │ │ ├── 16589910239841.png │ │ ├── 16589915329732.jpg │ │ ├── 16589918835312.jpg │ │ ├── 16589919873070.jpg │ │ ├── 16589927417533.jpg │ │ ├── 16591253982291.jpg │ │ ├── 16591257371138.jpg │ │ ├── 16591588597095.jpg │ │ ├── 16591604097922.jpg │ │ └── 16591625487605.jpg ├── DecompileFiles │ ├── Cookie.java │ ├── QMDmain.java │ ├── EncryptAndDecrypt.java │ ├── TingXIaMain.java │ └── ting.java └── README.md ├── requirements.txt ├── .gitignore ├── docker-compose.yml ├── Dockerfile ├── flaskSystem ├── static │ └── index.html ├── src │ ├── Api │ │ ├── BaseApi.py │ │ ├── MiGu.py │ │ ├── MyFreeMP3.py │ │ ├── Kuwo.py │ │ └── Netease.py │ ├── Types │ │ └── Types.py │ └── Common │ │ ├── Concurrency.py │ │ ├── Http.py │ │ ├── Tools.py │ │ └── EncryptTools.py ├── API │ ├── kw.py │ ├── qq.py │ └── es.py └── App.py ├── MainServer.py ├── .idea └── runConfigurations │ ├── MainServer.xml │ └── .xml ├── test.py └── README.md /WebSourceCode/README.md: -------------------------------------------------------------------------------- 1 | # 下载器前端页面 2 | 使用技术: 3 | Vue3 + TS + Pinia -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img.png -------------------------------------------------------------------------------- /img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_1.png -------------------------------------------------------------------------------- /img_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_10.png -------------------------------------------------------------------------------- /img_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_11.png -------------------------------------------------------------------------------- /img_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_12.png -------------------------------------------------------------------------------- /img_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_13.png -------------------------------------------------------------------------------- /img_14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_14.png -------------------------------------------------------------------------------- /img_15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_15.png -------------------------------------------------------------------------------- /img_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_16.png -------------------------------------------------------------------------------- /img_17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_17.png -------------------------------------------------------------------------------- /img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_2.png -------------------------------------------------------------------------------- /img_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_3.png -------------------------------------------------------------------------------- /img_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_4.png -------------------------------------------------------------------------------- /img_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_5.png -------------------------------------------------------------------------------- /img_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_6.png -------------------------------------------------------------------------------- /img_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_7.png -------------------------------------------------------------------------------- /img_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_8.png -------------------------------------------------------------------------------- /img_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/img_9.png -------------------------------------------------------------------------------- /md/media/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/img.png -------------------------------------------------------------------------------- /md/media/img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/img_2.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.2.3 2 | flask_cors==3.0.10 3 | pycryptodome==3.17 4 | requests==2.28.2 5 | Werkzeug==2.2.3 6 | -------------------------------------------------------------------------------- /md/media/16589902696730/16589910239841.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/16589902696730/16589910239841.png -------------------------------------------------------------------------------- /md/media/16589902696730/16589915329732.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/16589902696730/16589915329732.jpg -------------------------------------------------------------------------------- /md/media/16589902696730/16589918835312.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/16589902696730/16589918835312.jpg -------------------------------------------------------------------------------- /md/media/16589902696730/16589919873070.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/16589902696730/16589919873070.jpg -------------------------------------------------------------------------------- /md/media/16589902696730/16589927417533.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/16589902696730/16589927417533.jpg -------------------------------------------------------------------------------- /md/media/16589902696730/16591253982291.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/16589902696730/16591253982291.jpg -------------------------------------------------------------------------------- /md/media/16589902696730/16591257371138.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/16589902696730/16591257371138.jpg -------------------------------------------------------------------------------- /md/media/16589902696730/16591588597095.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/16589902696730/16591588597095.jpg -------------------------------------------------------------------------------- /md/media/16589902696730/16591604097922.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/16589902696730/16591604097922.jpg -------------------------------------------------------------------------------- /md/media/16589902696730/16591625487605.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hhxhl/QQFlacMusicDownloader/HEAD/md/media/16589902696730/16591625487605.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /NetEase.cfg 2 | /config.json 3 | /music 4 | .DS_Store 5 | venv 6 | /login.png 7 | .idea/* 8 | **/__pycache__ 9 | !.idea/runConfigurations 10 | .vscode -------------------------------------------------------------------------------- /WebSourceCode/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-auto-import 5 | export {} 6 | declare global { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /WebSourceCode/src/global/MainStyle.scss: -------------------------------------------------------------------------------- 1 | $ppp: #ff1e00; 2 | 3 | .buttonEx { 4 | padding: 10px; 5 | border-radius: 5px; 6 | text-align: center; 7 | background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); 8 | } -------------------------------------------------------------------------------- /WebSourceCode/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } 8 | -------------------------------------------------------------------------------- /WebSourceCode/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /WebSourceCode/src/store/SystemStore.ts: -------------------------------------------------------------------------------- 1 | import { BasicStore } from "./BasicStore"; 2 | import { storeToRefs } from "pinia"; 3 | 4 | export const SystemStore = () => { 5 | return { 6 | basicStore: BasicStore(), 7 | }; 8 | }; 9 | 10 | export const ref2 = storeToRefs; 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | downloader: 4 | image: registry.cn-hangzhou.aliyuncs.com/music_downloader/qq_flac_music_downloader 5 | container_name: music 6 | network_mode: bridge 7 | volumes: 8 | - 本地目录:/workspace/music 9 | ports: 10 | - "127.0.0.1:8899:8899" 11 | restart: always -------------------------------------------------------------------------------- /WebSourceCode/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim-buster 2 | 3 | WORKDIR /workspace 4 | 5 | ADD ./ /workspace 6 | 7 | RUN pip3 install --no-cache-dir --upgrade pip -i https://mirrors.bfsu.edu.cn/pypi/web/simple && \ 8 | pip3 install --no-cache-dir -r /workspace/requirements.txt -i https://mirrors.bfsu.edu.cn/pypi/web/simple 9 | 10 | EXPOSE 8899 11 | 12 | CMD ["python3", "MainServer.py"] -------------------------------------------------------------------------------- /WebSourceCode/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 我的乐库 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /WebSourceCode/src/components/Download.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /WebSourceCode/src/components/SimpleCode.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /WebSourceCode/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /flaskSystem/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 我的乐库 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /WebSourceCode/src/components/NotFound.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /WebSourceCode/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createSSRApp } from "vue"; 2 | 3 | import element from "element-plus"; 4 | 5 | import "element-plus/dist/index.css"; 6 | import "element-plus/theme-chalk/dark/css-vars.css"; 7 | import "./style.css"; 8 | 9 | import App from "./App.vue"; 10 | 11 | import router from "./router/router"; 12 | import { createPinia } from "pinia"; 13 | import persist from "pinia-plugin-persistedstate"; 14 | 15 | const app = createSSRApp(App); 16 | const pinia = createPinia(); 17 | pinia.use(persist); 18 | 19 | app.use(router); 20 | app.use(pinia); 21 | app.use(element); 22 | app.mount("#app"); 23 | -------------------------------------------------------------------------------- /WebSourceCode/src/utils/type/NeteaseMusicPlayList.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 3 | * # @作者 : 秋城落叶(QiuChenly) 4 | * # @邮件 : qiuchenly@outlook.com 5 | * # @文件 : 项目 [WebSourceCode] - NeteaseMusicPlayList.ts 6 | * # @修改时间 : 2023-03-05 05:28:10 7 | * # @上次修改 : 2023/3/5 下午5:28 8 | */ 9 | 10 | export interface MusicPlaylist { 11 | code: number; 12 | list: MusicPlayList2List[]; 13 | } 14 | 15 | export interface MusicPlayList2List { 16 | coverImgUrl: string; 17 | id: number; 18 | name: string; 19 | trackCount: number; 20 | userId: number; 21 | } 22 | -------------------------------------------------------------------------------- /flaskSystem/src/Api/BaseApi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - BaseApi.py 5 | # @修改时间 : 2023-03-13 11:07:40 6 | # @上次修改 : 2023/3/13 下午11:07 7 | import abc 8 | 9 | from flaskSystem.src.Types.Types import Songs 10 | 11 | 12 | class BaseApi(abc.ABC): 13 | @abc.abstractmethod 14 | def search(self, searchKey: str) -> list[Songs]: 15 | """ 16 | 搜索歌曲并获取统一列表 17 | Args: 18 | searchKey: 搜索字符串 19 | 20 | Returns: 21 | 返回 `SearchResult` 类型的列表数据 22 | """ 23 | -------------------------------------------------------------------------------- /md/DecompileFiles/Cookie.java: -------------------------------------------------------------------------------- 1 | package DecompileFiles; 2 | 3 | public class Cookie { 4 | private static String Mkey; 5 | private static String QQ; 6 | 7 | // String Decryptor: 2 succeeded, 0 failed 8 | public static String getCookie() { 9 | return "qqmusic_key=[" + Cookie.Mkey + "];qqmusic_uin=[" + Cookie.QQ + "];"; 10 | } 11 | 12 | public static String getMkey() { 13 | return Cookie.Mkey; 14 | } 15 | 16 | public static String getQQ() { 17 | return Cookie.QQ; 18 | } 19 | 20 | /** 21 | * 解密后的密钥保存 22 | * 23 | * @param mkey 24 | * @param qq 25 | */ 26 | public static void setCookie(String mkey, String qq) { 27 | Cookie.Mkey = mkey; 28 | Cookie.QQ = qq; 29 | } 30 | } -------------------------------------------------------------------------------- /WebSourceCode/src/utils/type/BasicType.ts: -------------------------------------------------------------------------------- 1 | export interface SearchMusicResult { 2 | code: number 3 | list: SearchMusicResultSingle[] 4 | page: Page 5 | } 6 | 7 | export interface SearchMusicResultSingle { 8 | album: string 9 | extra: string 10 | mid: string 11 | musicid: number 12 | notice: string 13 | prefix: string 14 | readableText: string 15 | singer: string 16 | size: number 17 | songmid: string 18 | time_publish: string 19 | title: string 20 | } 21 | 22 | export interface Page { 23 | cur: number 24 | next: number 25 | searchKey: string 26 | size: number 27 | } 28 | 29 | 30 | export interface InitAnonimous { 31 | code: number 32 | cookie: string 33 | createTime: number 34 | userId: number 35 | } 36 | -------------------------------------------------------------------------------- /MainServer.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - MainServer.py 5 | # @修改时间 : 2023-03-13 11:07:40 6 | # @上次修改 : 2023/3/13 下午11:07 7 | 8 | # 由于Python解释性语言特性,必须要严格按照加载顺序 9 | 10 | import argparse 11 | 12 | parser = argparse.ArgumentParser(description="QQFlacMusicDownloader.") 13 | parser.add_argument("--port", default=8899, type=int) 14 | args = parser.parse_args() 15 | 16 | from flaskSystem.App import Start # 必须先加载他 这是初始化flask框架代码 17 | from flaskSystem.API.es import init as es # 下面无需顺序 18 | from flaskSystem.API.kw import init as kw # 下面无需顺序 19 | from flaskSystem.API.qq import init as qq # es qq模块不分顺序 20 | 21 | es() # 加载API接口 22 | qq() 23 | kw() 24 | 25 | Start(args.port) # 最后启动总函数 26 | -------------------------------------------------------------------------------- /WebSourceCode/src/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | import '@vue/runtime-core' 7 | 8 | export {} 9 | 10 | declare module '@vue/runtime-core' { 11 | export interface GlobalComponents { 12 | ElButton: typeof import('element-plus/es')['ElButton'] 13 | ElIcon: typeof import('element-plus/es')['ElIcon'] 14 | HelloWorld: typeof import('./components/HelloWorld.vue')['default'] 15 | Home: typeof import('./components/Home.vue')['default'] 16 | Index: typeof import('./components/Index.vue')['default'] 17 | NotFound: typeof import('./components/NotFound.vue')['default'] 18 | RouterLink: typeof import('vue-router')['RouterLink'] 19 | RouterView: typeof import('vue-router')['RouterView'] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /WebSourceCode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "esModuleInterop": true, 14 | "lib": [ 15 | "ESNext", 16 | "DOM" 17 | ], 18 | "skipLibCheck": true, 19 | "types": [ 20 | "unplugin-icons/types/vue" 21 | ], 22 | "paths": { 23 | "@/*": [ 24 | "./src/*" 25 | ] 26 | } 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "src/**/*.d.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "unplugin-icons/types/vue", 34 | "pinia-plugin-persist-uni" 35 | ], 36 | "references": [ 37 | { 38 | "path": "./tsconfig.node.json" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /WebSourceCode/src/utils/Utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 3 | * # @作者 : 秋城落叶(QiuChenly) 4 | * # @邮件 : qiuchenly@outlook.com 5 | * # @文件 : 项目 [WebSourceCode] - Utils.ts 6 | * # @修改时间 : 2023-03-05 06:13:16 7 | * # @上次修改 : 2023/3/5 下午6:13 8 | */ 9 | 10 | export function timestampToTime(timestamp: number) { 11 | let date = new Date(timestamp); //时间戳为10位需*1000,时间戳为13位的话不需乘1000 12 | let Y = date.getFullYear() + "-"; 13 | let M = 14 | (date.getMonth() + 1 < 10 15 | ? "0" + (date.getMonth() + 1) 16 | : date.getMonth() + 1) + "-"; 17 | let D = (date.getDate() < 10 ? "0" + date.getDate() : date.getDate()) + " "; 18 | let h = 19 | (date.getHours() < 10 ? "0" + date.getHours() : date.getHours()) + ":"; 20 | let m = 21 | (date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes()) + 22 | ":"; 23 | let s = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds(); 24 | return Y + M + D + h + m + s; 25 | } 26 | 27 | /** 28 | * 判断类型是否属于某一种 29 | * @param props 30 | */ 31 | export const isType = (props: any): props is T => 32 | //@ts-ignore 33 | typeof (props as T)["js"] !== "undefined"; 34 | -------------------------------------------------------------------------------- /WebSourceCode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icbcview", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc --noEmit && vite build --emptyOutDir", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@element-plus/icons-vue": "^2.1.0", 13 | "axios": "^1.4.0", 14 | "crypto-js": "^4.1.1", 15 | "element-plus": "^2.3.8", 16 | "qrcode.vue": "^3.4.0", 17 | "vue": "^3.3.4" 18 | }, 19 | "devDependencies": { 20 | "@iconify/json": "^2.2.93", 21 | "@types/crypto-js": "^4.1.1", 22 | "@types/node": "^20.4.4", 23 | "@vitejs/plugin-vue": "^4.2.3", 24 | "@vue/compiler-sfc": "^3.3.4", 25 | "@vue/tsconfig": "^0.4.0", 26 | "pinia": "^2.0.32", 27 | "pinia-plugin-persistedstate": "^3.1.0", 28 | "sass": "^1.64.1", 29 | "terser": "^5.19.2", 30 | "typescript": "^5.1.6", 31 | "unplugin-auto-import": "^0.16.6", 32 | "unplugin-icons": "^0.16.5", 33 | "unplugin-vue-components": "^0.25.1", 34 | "vite": "^4.4.6", 35 | "vite-plugin-compression": "^0.5.1", 36 | "vite-plugin-inspect": "^0.7.33", 37 | "vue-router": "^4.2.4", 38 | "vue-tsc": "^1.8.6" 39 | } 40 | } -------------------------------------------------------------------------------- /.idea/runConfigurations/MainServer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | -------------------------------------------------------------------------------- /.idea/runConfigurations/.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | -------------------------------------------------------------------------------- /WebSourceCode/src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | 30 | 50 | -------------------------------------------------------------------------------- /WebSourceCode/src/store/BasicStore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 3 | * # @作者 : 秋城落叶(QiuChenly) 4 | * # @邮件 : qiuchenly@outlook.com 5 | * # @文件 : 项目 [qqmusic] - BasicStore.ts 6 | * # @修改时间 : 2023-04-14 01:32:39 7 | * # @上次修改 : 2023/4/14 下午1:32 8 | */ 9 | 10 | import { defineStore } from "pinia"; 11 | import { NetEaseUserInfo } from "@/utils/type/UserInfoDetail"; 12 | 13 | export const BasicStore = defineStore("basicStore", { 14 | state: () => { 15 | return { 16 | firstOpen: true, 17 | token: "", 18 | searchHistory: [] as string[], 19 | lastSearch: "", 20 | filterKeys: [ 21 | "DJ", 22 | "Remix", 23 | "即兴", 24 | "变调", 25 | "Live", 26 | "伴奏", 27 | "版,", 28 | "版)", 29 | "慢四", 30 | "纯音乐", 31 | "二胡", 32 | "串烧", 33 | "现场", 34 | ], 35 | config: { 36 | onlyMatchSearchKey: false, 37 | ignoreNoAlbumSongs: false, 38 | classificationMusicFile: false, 39 | disableFilterKey: false, 40 | concurrency: { 41 | num: 16, 42 | downloadFolder: "", 43 | }, 44 | platform: "qq", 45 | }, 46 | netease: { 47 | isLogin: false, 48 | token: "", 49 | anonimousCookie: "", 50 | user: {} as NetEaseUserInfo, 51 | }, 52 | }; 53 | }, 54 | getters: {}, 55 | actions: { 56 | initEnv() {}, 57 | }, 58 | persist: true, 59 | }); 60 | -------------------------------------------------------------------------------- /flaskSystem/API/kw.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - kw.py 5 | # @修改时间 : 2023-03-13 11:07:40 6 | # @上次修改 : 2023/3/13 下午11:07 7 | from flask import request 8 | 9 | from flaskSystem.src.Api.Kuwo import KwApi 10 | from flaskSystem.src.Api.MiGu import MiGu 11 | from flaskSystem.src.Api.MyFreeMP3 import MyFreeMP3 12 | from flaskSystem.App import app 13 | 14 | kw = KwApi() 15 | mg = MiGu() 16 | myFreeMP3 = MyFreeMP3() 17 | 18 | 19 | @app.get("/kw/search///") 20 | def kwsearch(searchKey: str, page=1, size=100): 21 | lst = kw.search_kw_mac(searchKey, int(page), int(size)) # Mac端搜索接口 22 | page = lst['page'] 23 | return { 24 | 'code': 200, 25 | 'list': lst['data'], 26 | 'page': page 27 | } 28 | 29 | 30 | @app.get("/mg/search///") 31 | def mgsearch(searchKey: str, page=1, size=100): 32 | lst = mg.search(searchKey, int(page), int(size)) # Mac端搜索接口 33 | page = lst['page'] 34 | return { 35 | 'code': 200, 36 | 'list': lst['data'], 37 | 'page': page 38 | } 39 | 40 | 41 | @app.post("/myfreemp3/search") 42 | def myFreeMP3search(): 43 | data = request.get_json() 44 | lst = myFreeMP3.search(data) 45 | page = lst['page'] 46 | return { 47 | 'code': 200, 48 | 'list': lst['data'], 49 | 'page': page 50 | } 51 | 52 | 53 | def init(): 54 | pass 55 | -------------------------------------------------------------------------------- /flaskSystem/API/qq.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - qq.py 5 | # @修改时间 : 2023-03-14 02:55:44 6 | # @上次修改 : 2023/3/14 下午2:55 7 | 8 | from flaskSystem.src.Api.QQMusic import QQMusicApi 9 | from flaskSystem.App import app 10 | 11 | QQApi = QQMusicApi() 12 | 13 | 14 | @app.get("/qq/search///") 15 | def search(searchKey: str, page=1, size=30): 16 | # 检查前缀 17 | prefix = searchKey.split(":") 18 | if len(prefix) == 2: 19 | command = prefix[0] 20 | _id = prefix[1] 21 | # 高级指令 22 | if command == 'p': 23 | # 加载歌单 24 | lst = QQApi.parseQQMusicPlaylist(_id) 25 | elif command == 'b': 26 | # 加载专辑 27 | lst = QQApi.parseQQMusicAlbum(_id) 28 | elif command == 'id': 29 | # 指定单曲id 30 | lst = QQApi.getSingleMusicInfo(_id) 31 | elif command == 't': 32 | # 加载排行版 33 | lst = QQApi.parseQQMusicToplist(_id) 34 | else: 35 | lst = [] 36 | else: 37 | size = int(size) 38 | if size > 30: # 这里强制让qq音乐指定为30一页 因为qq服务器现在禁止超过30一页拉取数据 39 | size = 30 40 | lst = QQApi.getQQMusicSearch(searchKey, int(page), int(size)) 41 | page = lst['page'] 42 | lst = QQApi.formatList(lst['data']) 43 | return { 44 | 'code': 200, 45 | 'list': lst, 46 | 'page': page 47 | } 48 | 49 | 50 | def init(): 51 | pass 52 | -------------------------------------------------------------------------------- /flaskSystem/src/Types/Types.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - Types.py 5 | # @修改时间 : 2023-03-05 09:43:21 6 | # @上次修改 : 2023/3/5 下午9:43 7 | 8 | class Songs(object): 9 | def __init__(self, 10 | album: str, 11 | artist: str, 12 | musicrid: str, 13 | releaseDate: str, 14 | albumid: int, 15 | songTimeMinutes: str, 16 | pic120: str, 17 | albumpic: str, 18 | name: str, 19 | rid: int, 20 | *args, 21 | **kwargs) -> None: 22 | """ 23 | 24 | Args: 25 | album: 专辑 26 | artist: 艺术家,歌手 27 | musicrid: 音乐id 28 | releaseDate: 发布时间 29 | albumid: 专辑ID 30 | songTimeMinutes: 歌曲时间 31 | pic120: 音乐图片 32 | albumpic: 专辑图片 33 | name: 歌曲名称 34 | rid: 歌曲实际数字id 35 | *args: 36 | **kwargs: 37 | """ 38 | self.rid = rid 39 | self.name = name 40 | self.pic120 = pic120 41 | self.songTimeMinutes = songTimeMinutes 42 | self.albumid = albumid 43 | self.albumpic = albumpic 44 | self.releaseDate = releaseDate 45 | self.album = album 46 | self.artist = artist 47 | self.musicrid = musicrid 48 | 49 | # @property 50 | # def title(self): 51 | # return self._album 52 | -------------------------------------------------------------------------------- /WebSourceCode/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | Cloud: typeof import('./src/components/Cloud.vue')['default'] 11 | Download: typeof import('./src/components/Download.vue')['default'] 12 | Home: typeof import('./src/components/Home.vue')['default'] 13 | ICarbonCloudDataOps: typeof import('~icons/carbon/cloud-data-ops')['default'] 14 | ICarbonDownload: typeof import('~icons/carbon/download')['default'] 15 | ICarbonMoon: typeof import('~icons/carbon/moon')['default'] 16 | ICarbonSun: typeof import('~icons/carbon/sun')['default'] 17 | IIconParkTwotoneClearFormat: typeof import('~icons/icon-park-twotone/clear-format')['default'] 18 | Index: typeof import('./src/components/Index.vue')['default'] 19 | ISystemUiconsSearch: typeof import('~icons/system-uicons/search')['default'] 20 | ISystemUiconsSun: typeof import('~icons/system-uicons/sun')['default'] 21 | ISystemUiconsUndoHistory: typeof import('~icons/system-uicons/undo-history')['default'] 22 | Netease: typeof import('./src/components/Netease.vue')['default'] 23 | NotFound: typeof import('./src/components/NotFound.vue')['default'] 24 | RouterLink: typeof import('vue-router')['RouterLink'] 25 | RouterView: typeof import('vue-router')['RouterView'] 26 | SearchMusic: typeof import('./src/components/SearchMusic.vue')['default'] 27 | SimpleCode: typeof import('./src/components/SimpleCode.vue')['default'] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - test.py 5 | # @修改时间 : 2023-03-06 06:02:23 6 | # @上次修改 : 2023/3/6 下午6:02 7 | import time 8 | import uuid 9 | from concurrent.futures import Future 10 | from uuid import UUID 11 | 12 | import requests 13 | from flask import Flask 14 | 15 | # from src.Api.Kuwo import KwApi 16 | # from src.Api.Netease import Netease 17 | # from src.Common import Concurrency 18 | from src.Common.Concurrency import Downloader 19 | 20 | 21 | # from src.Common.Tools import subString 22 | # from src.Types.Types import Songs 23 | 24 | 25 | # s = Songs("哈咯") 26 | # s.title = "asd" 27 | # print(s.title) 28 | 29 | # subString("kw_token=123123;", "kw_token=", ";") 30 | 31 | # kw = KwApi() 32 | # res = kw.search("周杰伦") 33 | # res = kw.getDownloadUrl(res[0].rid) 34 | # res = requests.get(res) 35 | # with open("test.mp3", 'wb+') as w: 36 | # w.write(res.content) 37 | # w.flush() 38 | # 只能下载MP3格式 很遗憾 39 | 40 | # ease = Netease() 41 | # qrCode = ease.qrLogin() 42 | 43 | def done(ret: Future): 44 | print(f"ret is down {ret.result()}") 45 | 46 | 47 | def executeFn(a1: str, a2: bool): 48 | time.sleep(4) 49 | return a1 + "a2 True" if a2 else "a2 False" 50 | 51 | 52 | app: Flask = Flask(__name__) 53 | 54 | c = Downloader() 55 | c.initPool(16) 56 | 57 | 58 | @app.get("/") 59 | def add(): 60 | print("任务开始") 61 | c.addTask(done, executeFn, "1234", False) 62 | return { 63 | 'code': 200 64 | } 65 | 66 | 67 | app.run( 68 | '0.0.0.0', 69 | 8899, 70 | debug=False 71 | ) 72 | -------------------------------------------------------------------------------- /WebSourceCode/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: var(--el-font-family); 3 | 4 | font-synthesis: none; 5 | text-rendering: optimizeLegibility; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | -webkit-text-size-adjust: 100%; 9 | } 10 | 11 | html, body, #app { 12 | width: 100%; 13 | height: 100%; 14 | padding: 0; 15 | margin: 0; 16 | } 17 | 18 | button { 19 | border-radius: 8px; 20 | border: 1px solid transparent; 21 | font-size: 1em; 22 | font-weight: 500; 23 | font-family: inherit; 24 | cursor: pointer; 25 | transition: border-color 0.25s; 26 | } 27 | 28 | button:hover { 29 | border-color: #646cff; 30 | } 31 | 32 | button:focus, 33 | button:focus-visible { 34 | outline: 4px auto -webkit-focus-ring-color; 35 | } 36 | 37 | /* @media (prefers-color-scheme: light) { 38 | :root { 39 | color: #213547; 40 | background-color: #ffffff; 41 | } 42 | a:hover { 43 | color: #747bff; 44 | } 45 | button { 46 | background-color: #f9f9f9; 47 | } 48 | } */ 49 | 50 | html { 51 | --qiuchen-text: rgba(185, 185, 185, 0.5); 52 | --qiuchen-text-15: rgba(185, 185, 185, 0.15); 53 | --qiuchen-hover-text: rgba(0, 0, 0, 0.35); 54 | --qiuchen-normal-white: rgba(0, 0, 0, 0); 55 | --qiuchen-normal-black: rgba(0, 0, 0, 1); 56 | 57 | } 58 | 59 | html.dark { 60 | /* 自定义深色背景颜色 */ 61 | --qiuchen-text: rgba(215, 215, 215, 0.5); 62 | --qiuchen-text-15: rgba(215, 215, 215, 0.15); 63 | --qiuchen-hover-text: rgb(255, 255, 255, .8); 64 | --qiuchen-normal-white: rgba(255, 255, 255, .5); 65 | --qiuchen-normal-black: rgba(255, 255, 255, 1); 66 | } -------------------------------------------------------------------------------- /flaskSystem/src/Common/Concurrency.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - Concurrency.py 5 | # @修改时间 : 2023-03-04 09:15:16 6 | # @上次修改 : 2023/3/4 下午9:15 7 | import os 8 | from concurrent import futures 9 | from concurrent.futures import ThreadPoolExecutor 10 | 11 | 12 | class Downloader(): 13 | def __init__(self): 14 | self.maxWorks = 4 15 | self.mConcurrentPool: ThreadPoolExecutor = None 16 | self.mPoolThread = [] 17 | self.folder = '' 18 | self.set_folder(os.getcwd() + '/music/') 19 | 20 | def set_folder(self, folder): 21 | folder = folder.replace(' ', '') 22 | if not folder.endswith('/'): 23 | folder += '/' 24 | self.folder = folder 25 | if not os.path.exists(self.folder): 26 | os.mkdir(self.folder) 27 | return self.folder 28 | 29 | def get_folder(self): 30 | return self.folder 31 | 32 | def initPool(self, max_works: int): 33 | self.maxWorks = max_works 34 | if self.mConcurrentPool is not None: 35 | self.mConcurrentPool.shutdown(False) 36 | # for th in concurrent.futures.as_completed(pollCache): 37 | # song = th.result() 38 | self.mConcurrentPool = ThreadPoolExecutor(max_workers=max_works) 39 | 40 | def addTask(self, callback, fn, *args, **kwargs, ): 41 | t = self.mConcurrentPool.submit(fn, *args, **kwargs) 42 | t.add_done_callback(callback) 43 | self.mPoolThread.append(t) 44 | return True 45 | 46 | def getCurrentResize(self): 47 | return self.maxWorks 48 | -------------------------------------------------------------------------------- /WebSourceCode/src/router/router.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 3 | * # @作者 : 秋城落叶(QiuChenly) 4 | * # @邮件 : qiuchenly@outlook.com 5 | * # @文件 : 项目 [qqmusic] - router.ts 6 | * # @修改时间 : 2023-03-14 08:22:45 7 | * # @上次修改 : 2023/3/14 下午8:22 8 | */ 9 | 10 | import Home from "@/components/Home.vue"; 11 | import Index from "@/components/Index.vue"; 12 | import NotFound from "@/components/NotFound.vue"; 13 | import SearchMusic from "@/components/SearchMusic.vue"; 14 | import { 15 | createWebHistory, 16 | createRouter, 17 | createWebHashHistory, 18 | RouterOptions, 19 | RouteRecordRaw, 20 | } from "vue-router"; 21 | import Netease from "@/components/Netease.vue"; 22 | import Download from "@/components/Download.vue"; 23 | import Cloud from "@/components/Cloud.vue"; 24 | 25 | const routes = [ 26 | { 27 | path: "/", 28 | name: "Index", 29 | component: Index, 30 | children: [ 31 | { 32 | path: "home", 33 | alias: "/", //修复第一次打开页面白屏的问题 34 | component: Home, 35 | }, 36 | { 37 | path: "search", 38 | component: SearchMusic, 39 | }, 40 | { 41 | path: "netease", 42 | component: Netease, 43 | }, 44 | { 45 | path: "download", 46 | component: Download, 47 | }, 48 | { 49 | path: "cloud", 50 | component: Cloud, 51 | }, 52 | ], 53 | }, 54 | { 55 | path: "/home", 56 | name: "home", 57 | component: Home, 58 | }, 59 | { 60 | path: "/:catchAll(.*)", 61 | component: NotFound, 62 | }, 63 | ] as RouteRecordRaw[]; 64 | 65 | const router = createRouter({ 66 | history: createWebHashHistory(), 67 | routes, 68 | } as RouterOptions); 69 | 70 | export default router; 71 | -------------------------------------------------------------------------------- /WebSourceCode/src/utils/type/UserInfoDetail.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 3 | * # @作者 : 秋城落叶(QiuChenly) 4 | * # @邮件 : qiuchenly@outlook.com 5 | * # @文件 : 项目 [WebSourceCode] - UserInfoDetail.ts 6 | * # @修改时间 : 2023-03-05 03:46:32 7 | * # @上次修改 : 2023/3/5 下午3:46 8 | */ 9 | 10 | export interface NetEaseUserInfo { 11 | account: Account; 12 | code: number; 13 | profile: Profile; 14 | } 15 | 16 | export interface Account { 17 | anonimousUser: boolean; 18 | ban: number; 19 | baoyueVersion: number; 20 | createTime: number; 21 | donateVersion: number; 22 | id: number; 23 | paidFee: boolean; 24 | status: number; 25 | tokenVersion: number; 26 | type: number; 27 | userName: string; 28 | vipType: number; 29 | whitelistAuthority: number; 30 | } 31 | 32 | export interface Profile { 33 | accountStatus: number; 34 | accountType: number; 35 | anchor: boolean; 36 | authStatus: number; 37 | authenticated: boolean; 38 | authenticationTypes: number; 39 | authority: number; 40 | avatarDetail: any; 41 | avatarImgId: number; 42 | avatarUrl: string; 43 | backgroundImgId: number; 44 | backgroundUrl: string; 45 | birthday: number; 46 | city: number; 47 | createTime: number; 48 | defaultAvatar: boolean; 49 | description: any; 50 | detailDescription: any; 51 | djStatus: number; 52 | expertTags: any; 53 | experts: any; 54 | followed: boolean; 55 | gender: number; 56 | lastLoginIP: string; 57 | lastLoginTime: number; 58 | locationStatus: number; 59 | mutual: boolean; 60 | nickname: string; 61 | province: number; 62 | remarkName: any; 63 | shortUserName: string; 64 | signature: any; 65 | userId: number; 66 | userName: string; 67 | userType: number; 68 | vipType: number; 69 | viptypeVersion: number; 70 | } 71 | -------------------------------------------------------------------------------- /WebSourceCode/src/utils/type/NetEasePlayListSong.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 3 | * # @作者 : 秋城落叶(QiuChenly) 4 | * # @邮件 : qiuchenly@outlook.com 5 | * # @文件 : 项目 [qqmusic] - NetEasePlayListSong.ts 6 | * # @修改时间 : 2023-03-20 01:47:11 7 | * # @上次修改 : 2023/3/20 上午1:47 8 | */ 9 | 10 | export interface NeteasePlayListSongs { 11 | code: number; 12 | list: NeteasePlayListSongsList[]; 13 | } 14 | 15 | export interface NeteasePlayListSongsList { 16 | album: string; 17 | author: Author[]; 18 | author_simple: string; 19 | mid: number; 20 | title: string; 21 | publishTime: number; 22 | 23 | fee: number; 24 | cloud: boolean; 25 | 26 | copyright: number; 27 | 28 | privileges: Privileges; 29 | } 30 | 31 | export interface Privileges { 32 | chargeInfoList: ChargeInfoList[]; 33 | cp: number; 34 | cs: boolean; 35 | dl: number; 36 | dlLevel: string; 37 | downloadMaxBrLevel: string; 38 | downloadMaxbr: number; 39 | fee: number; 40 | fl: number; 41 | flLevel: string; 42 | flag: number; 43 | freeTrialPrivilege: FreeTrialPrivilege; 44 | id: number; 45 | maxBrLevel: string; 46 | maxbr: number; 47 | payed: number; 48 | pl: number; 49 | plLevel: string; 50 | playMaxBrLevel: string; 51 | playMaxbr: number; 52 | preSell: boolean; 53 | rscl: any; 54 | sp: number; 55 | st: number; 56 | subp: number; 57 | toast: boolean; 58 | } 59 | 60 | export interface ChargeInfoList { 61 | chargeMessage: any; 62 | chargeType: number; 63 | chargeUrl: any; 64 | rate: number; 65 | } 66 | 67 | export interface FreeTrialPrivilege { 68 | listenType: any; 69 | resConsumable: boolean; 70 | userConsumable: boolean; 71 | } 72 | 73 | export interface Author { 74 | alias: any[]; 75 | id: number; 76 | name: string; 77 | tns: any[]; 78 | } 79 | -------------------------------------------------------------------------------- /flaskSystem/src/Common/Http.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : 1925374620@qq.com 4 | # @文件 : 项目 [qqmusic] - Http.py 5 | # @修改时间 : 2023-07-24 02:07:51 6 | # @上次修改 : 2023/7/24 上午2:07 7 | 8 | import json 9 | from http.cookiejar import Cookie 10 | 11 | import requests 12 | from requests import Request, Session 13 | 14 | 15 | class HttpRequest: 16 | __session = None 17 | 18 | def __init__(self): 19 | # 全局唯一Session 20 | self.__session = requests.Session() 21 | 22 | def getHttp(self, 23 | url: str, 24 | method: int = 0, 25 | data: bytes = r'', 26 | header: dict[str, str] = {}, 27 | params: dict[str, str] = {} 28 | ) -> requests.Response: 29 | """ 30 | Http请求-提交二进制流 31 | Args: 32 | params: url后面的网址参数 33 | url: url网址 34 | method: 0 表示Get请求 1 表示用POST请求. 默认值为 0. 35 | data: 提交的二进制流data数据. 默认值为 r''. 36 | header: 协议头. 默认值为 {}. 37 | 38 | Returns: 39 | requests.Response: 返回的http数据 40 | """ 41 | if method == 0: 42 | d = self.__session.get(url, headers=header, params=params) 43 | else: 44 | d = self.__session.post(url, data, headers=header, params=params) 45 | return d 46 | 47 | def getHttp2Json(self, url: str, method: int = 0, data: dict = {}, header: dict = {}): 48 | """Http请求-提交json数据 49 | 50 | Args: 51 | url (str): url网址 52 | method (int): 0 表示Get请求 1 表示用POST请求. 默认值为 0. 53 | data (bytes): 提交的json对象数据. 默认值为 {}. 54 | header (dict): 协议头. 默认值为 {}. 55 | 56 | Returns: 57 | requests.Response: 返回的http数据 58 | """ 59 | d = json.dumps(data, ensure_ascii=False) 60 | d = d.encode('utf-8') 61 | return self.getHttp(url, method, d, header) 62 | 63 | def getSession(self) -> Session: 64 | return self.__session 65 | 66 | def setCookie(self, ck): 67 | for a in ck: 68 | self.__session.cookies.set(a, ck[a]) 69 | print("cookie set.") 70 | -------------------------------------------------------------------------------- /WebSourceCode/vite.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 3 | * # @作者 : 秋城落叶(QiuChenly) 4 | * # @邮件 : qiuchenly@outlook.com 5 | * # @文件 : 项目 [qqmusic] - vite.config.ts 6 | * # @修改时间 : 2023-03-15 03:50:51 7 | * # @上次修改 : 2023/3/15 上午3:50 8 | */ 9 | 10 | import { BuildOptions, defineConfig } from "vite"; 11 | import path from "path"; 12 | import vue from "@vitejs/plugin-vue"; 13 | import Icons from "unplugin-icons/vite"; 14 | import IconsResolver from "unplugin-icons/resolver"; 15 | import Inspect from "vite-plugin-inspect"; 16 | import Components from "unplugin-vue-components/vite"; 17 | import viteCompression from "vite-plugin-compression"; 18 | 19 | let pathSrc = path.resolve(__dirname, "src"); 20 | // https://vitejs.dev/config/ 21 | export default defineConfig({ 22 | build: { 23 | outDir: "../flaskSystem/static", 24 | minify: "terser", 25 | terserOptions: { 26 | compress: { 27 | drop_console: true, 28 | drop_debugger: true, 29 | }, 30 | }, 31 | rollupOptions: { 32 | // output: { //静态资源分类打包 33 | // chunkFileNames: 'static/js/[name]-[hash].js', 34 | // entryFileNames: 'static/js/[name]-[hash].js', 35 | // assetFileNames: 'static/[ext]/[name]-[hash].[ext]', 36 | // manualChunks(id) { //静态资源分拆打包 37 | // if (id.includes('node_modules')) { 38 | // return id.toString().split('node_modules/')[1].split('/')[0].toString(); 39 | // } 40 | // } 41 | // } 42 | }, 43 | } as BuildOptions, 44 | resolve: { 45 | alias: { 46 | "@": pathSrc, 47 | }, 48 | }, 49 | plugins: [ 50 | vue(), 51 | Components({ 52 | resolvers: [IconsResolver()], 53 | }), 54 | 55 | Icons({ 56 | compiler: "vue3", 57 | autoInstall: true, 58 | }), 59 | Inspect(), 60 | // viteCompression({ 61 | // verbose: true, 62 | // disable: false, 63 | // threshold: 10240, 64 | // algorithm: 'gzip', 65 | // ext: '.gz', 66 | // }), 67 | ], 68 | css: { 69 | preprocessorOptions: { 70 | scss: { 71 | additionalData: "@import '@/global/MainStyle.scss';", 72 | }, 73 | }, 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /flaskSystem/App.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - App.py 5 | # @修改时间 : 2023-03-13 11:42:05 6 | # @上次修改 : 2023/3/13 下午11:42 7 | import time 8 | from concurrent.futures import Future 9 | 10 | from flask import Flask, request 11 | from flask_cors import CORS 12 | 13 | from flaskSystem.src.Common.Concurrency import Downloader 14 | 15 | app: Flask = Flask(__name__, static_url_path="") 16 | 17 | CORS(app, resources=r'/*') 18 | 19 | 20 | @app.get("/") 21 | def index(): 22 | """ 23 | 返回主页面 24 | Returns: 25 | 26 | """ 27 | return app.redirect("/index.html") 28 | 29 | 30 | @app.get("/status") 31 | def appState(): 32 | return { 33 | 'code': 200 34 | } 35 | 36 | 37 | @app.post("/config") 38 | def configSave(): 39 | jsn = request.get_json() 40 | num = int(jsn['num']) 41 | location = jsn['folder'] 42 | c.set_folder(location) 43 | c.initPool(num) 44 | return { 45 | 'code': 200 46 | } 47 | 48 | 49 | @app.get("/getConfig") 50 | def getConfig(): 51 | return { 52 | 'num': c.getCurrentResize(), 53 | 'folder': c.get_folder() 54 | } 55 | 56 | 57 | @app.post("/download") 58 | def add(): 59 | from flaskSystem.src.Common.Tools import downSingle 60 | print("准备开始下载任务") 61 | jsn = request.get_json() 62 | music = jsn['music'] 63 | config = jsn['config'] 64 | # downSingle(jsn) 65 | c.addTask(done, downSingle, music, c.get_folder(), config) 66 | return { 67 | 'code': 200 68 | } 69 | 70 | 71 | c = Downloader() 72 | c.initPool(16) 73 | 74 | 75 | def done(ret: Future): 76 | """ 77 | 得到下载返回结果 78 | Args: 79 | ret: 80 | 81 | Returns: 82 | 83 | """ 84 | excepts = ret.exception() 85 | ret = ret.result() 86 | if ret['code'] != 200 or excepts: 87 | print(f"下载失败," + ret['msg'], excepts) 88 | else: 89 | print(f"下载成功。") 90 | 91 | 92 | def executeFn(a1: str, a2: bool): 93 | """ 94 | 测试线程池函数 实际上没有被调用 95 | Args: 96 | a1: 97 | a2: 98 | 99 | Returns: 100 | 101 | """ 102 | time.sleep(4) 103 | return a1 + "a2 True" if a2 else "a2 False" 104 | 105 | 106 | def Start(port): 107 | app.run( 108 | '0.0.0.0', 109 | port, 110 | debug=False 111 | ) 112 | -------------------------------------------------------------------------------- /md/DecompileFiles/QMDmain.java: -------------------------------------------------------------------------------- 1 | package DecompileFiles; 2 | 3 | class main { 4 | public static void main(String[] args) { 5 | EncryptAndDecrypt.decryptAndSetCookie(EncryptAndDecrypt.TestCookie); 6 | System.out.println(Cookie.getCookie()); 7 | System.out.println(getDeviceInfo()); 8 | } 9 | 10 | public static device getDeviceInfo() { 11 | // DeviceInfo v10 = new DeviceInfo( 12 | // SystemInfoUtil.getUID(), 13 | // SystemInfoUtil.getSystemModel(), 14 | // SystemInfoUtil.getDeviceBrand(), 15 | // SystemInfoUtil.getAppVersionName(), 16 | // SystemInfoUtil.getSystemVersion(), 17 | // SystemInfoUtil.getAppVersionCode() + "", 18 | // null, 19 | // 0x40, 20 | // null); 21 | // {"appVersion":"7.1.2","deviceBrand":"360","deviceModel":"QK1707-A01","ip":"JmUXjTe5jZ739hq/SNt1JeTKEmxAXZsSxrJcQNvQxN-3wrP6LSd6//Wk2W4COwBVBEty0UQ8-/-KsjB\n3ekz/e09nw==","systemVersion":"1.7.2","uid":"822a3b85-a5c9-438e-a277-a8da412e8265","versionCode":"76"} 22 | String uid = "822a3b85-a5c9-438e-a277-a8da412e8265", 23 | systemVersion = "1.7.2", 24 | versionCode = "76", 25 | deviceBrand = "360", 26 | deviceModel = "QK1707-A01", 27 | appVersion = "7.1.2", 28 | encIP = ""; 29 | device d = new device(uid, systemVersion, versionCode, deviceBrand, deviceModel, appVersion, encIP); 30 | encIP = EncryptAndDecrypt.encryptText( 31 | d.getEvalCode(), 32 | "F*ckYou!"); 33 | d.setIP(encIP); 34 | return d; 35 | } 36 | } 37 | 38 | class device { 39 | public String uid = "822a3b85-a5c9-438e-a277-a8da412e8265", 40 | systemVersion = "1.7.2", 41 | versionCode = "76", 42 | deviceBrand = "360", 43 | deviceModel = "QK1707-A01", 44 | appVersion = "7.1.2", 45 | encIP = ""; 46 | 47 | public device(String uid, 48 | String systemVersion, 49 | String versionCode, 50 | String deviceBrand, 51 | String deviceModel, 52 | String appVersion, 53 | String encIP) { 54 | this.uid = uid; 55 | this.systemVersion = systemVersion; 56 | this.versionCode = versionCode; 57 | this.deviceBrand = deviceBrand; 58 | this.deviceModel = deviceModel; 59 | this.appVersion = appVersion; 60 | this.encIP = encIP; 61 | } 62 | 63 | public void setIP(String ip) { 64 | this.encIP = ip; 65 | } 66 | 67 | public String getEvalCode() { 68 | return uid + deviceModel + deviceBrand + systemVersion + appVersion + versionCode; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /flaskSystem/src/Api/MiGu.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - MiGu.py 5 | # @修改时间 : 2023-03-13 11:07:40 6 | # @上次修改 : 2023/3/13 下午11:07 7 | from flaskSystem.src.Common import Http 8 | from flaskSystem.src.Common.Http import HttpRequest 9 | 10 | 11 | class MiGu(): 12 | httpClient: HttpRequest = None 13 | 14 | def __init__(self): 15 | self.httpClient = Http.HttpRequest() 16 | 17 | def getUrl(self, url: str, method=0, data=None): 18 | res = self.httpClient.getHttp2Json(url, method, data, { 19 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50", 20 | }) 21 | return res 22 | 23 | def search(self, searchKey="周杰伦", pageNum=1, pageSize=100): 24 | u = f'https://api.dog886.com/v1/getMiGuSearch?text={searchKey}&pageNo={pageNum}&pageSize={pageSize}' 25 | res = self.getUrl(u).json() 26 | data = res['data'] 27 | lst = [] 28 | for li in data['items']: 29 | kbpsList = li['kbpsList'] 30 | _kbps = {} 31 | lastKbps = -1 32 | for kbps in kbpsList: 33 | kbs = int(kbps['kbps'].replace("kbps", '')) 34 | if kbs > lastKbps: 35 | _kbps = kbps 36 | lastKbps = kbs 37 | 38 | extra = _kbps['suffix'] 39 | bitrate = _kbps['kbps'] 40 | it = { 41 | 'prefix': _kbps['type'], 42 | 'extra': extra, 43 | 'notice': "FLAC 无损音质" if extra == 'flac' else f'{extra.upper()} {bitrate}Kbps', 44 | 'mid': li['id'], 45 | 'musicid': li['id'], 46 | 'songmid': li['id'], 47 | 'size': "无", 48 | 'title': li['name'], 49 | 'singer': li['singer'], 50 | 'album': "无专辑", 51 | 'time_publish': "无", 52 | # 'hasLossless': li['hasLossless'], 53 | 'readableText': f"{li['singer']} - {li['name']}" 54 | } 55 | lst.append(it) 56 | return { 57 | 'data': lst, 58 | 'page': { 59 | 'size': data['total'], 60 | 'next': pageNum + 1, 61 | 'cur': pageNum, 62 | 'searchKey': searchKey 63 | } 64 | } 65 | 66 | def getDownloadLink(self, _id="0", _type="4"): 67 | u = f'https://api.dog886.com/v1/getMiGuSong?id={_id}&type={_type}' 68 | res = self.getUrl(u).json() 69 | if res['code'] == '200': 70 | return "https:" + res['data']['url'] 71 | else: 72 | return None 73 | -------------------------------------------------------------------------------- /WebSourceCode/src/component/UserInfo.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 30 | 31 | 109 | -------------------------------------------------------------------------------- /flaskSystem/src/Api/MyFreeMP3.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : 1925374620@qq.com 4 | # @文件 : 项目 [qqmusic] - MyFreeMP3.py 5 | # @修改时间 : 2023-07-24 06:21:40 6 | # @上次修改 : 2023/7/24 上午4:33 7 | import time 8 | from typing import Dict 9 | 10 | from flaskSystem.src.Common import Http 11 | from flaskSystem.src.Common.Http import HttpRequest 12 | 13 | 14 | class MyFreeMP3(): 15 | httpClient: HttpRequest = None 16 | 17 | def __init__(self): 18 | self.httpClient = Http.HttpRequest() 19 | 20 | def getUrl(self, url: str, method=0, data=None): 21 | res = self.httpClient.getHttp2Json(url, method, data, { 22 | "accept": "application/json, text/plain, */*", 23 | "content-type": "application/json", 24 | "origin": "https://tools.liumingye.cn", 25 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50" 26 | }) 27 | return res 28 | 29 | def search(self, data): 30 | u = f'https://api.liumingye.cn/m/api/search' 31 | res = self.getUrl(u, 1, data) 32 | res = res.json() 33 | if res['code'] != 200: 34 | res['data'] = { 35 | 'list': [], 36 | 'total': -1 37 | } 38 | _data = res['data'] 39 | lst = [] 40 | for li in _data['list']: 41 | kbpsList = li['quality'] 42 | lastKbps = -1 43 | for kbps in kbpsList: 44 | if type(kbps) is dict: 45 | kbs = int(kbps['name']) 46 | else: 47 | kbs = int(kbps) 48 | if kbs > lastKbps: 49 | lastKbps = kbs 50 | 51 | extra = "flac" if lastKbps >= 1000 else "mp3" 52 | 53 | album = li.get("album") 54 | mid = li.get('hash') 55 | if mid is None: 56 | mid = li.get("id") 57 | 58 | if mid is None: 59 | print("") 60 | it = { 61 | 'prefix': lastKbps, 62 | 'extra': extra, 63 | 'notice': "FLAC 无损音质" if extra == 'flac' else f'{extra.upper()} {lastKbps}Kbps', 64 | 'mid': mid, 65 | 'musicid': li['name'], 66 | 'songmid': li['name'], 67 | 'size': "无", 68 | 'title': li['name'], 69 | 'singer': "/".join([ 70 | it['name'] for it in li['artist'] 71 | ]), 72 | 'album': "" if album is None else album['name'], 73 | 'time_publish': "无", 74 | } 75 | lst.append(it) 76 | return { 77 | 'data': lst, 78 | 'page': { 79 | 'size': _data['total'], 80 | 'next': -1 if _data['total'] == 0 else data['page'] + 1, 81 | 'cur': data['page'], 82 | 'searchKey': data['text'] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /flaskSystem/API/es.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - es.py 5 | # @修改时间 : 2023-03-15 04:38:40 6 | # @上次修改 : 2023/3/15 上午4:38 7 | from flask import request 8 | from flaskSystem.src.Api.Netease import Netease 9 | from flaskSystem.App import app 10 | 11 | netes = Netease() 12 | 13 | 14 | @app.get("/es/qrLogin") 15 | def loginCode(): 16 | qrcode = netes.qrLogin() 17 | return { 18 | 'code': 200, 19 | 'qrcode': qrcode 20 | } 21 | 22 | 23 | @app.get("/es/checkLoginState/") 24 | def checkLoginState(unikey: str): 25 | state = netes.checkQrState(unikey) 26 | return state 27 | 28 | 29 | @app.get("/es/initAnonimous") 30 | def initAnonimous(): 31 | state = netes.anonimousLogin() 32 | return state 33 | 34 | 35 | @app.post("/es/setCookie") 36 | def setCookie(): 37 | ck = request.get_json()['cookie'] 38 | netes.set_cookie(ck) 39 | return { 40 | 'code': 200 41 | } 42 | 43 | 44 | @app.get("/es/getUserInfo") 45 | def getUserInfo(): 46 | state = netes.getUserDetail() 47 | return state 48 | 49 | 50 | @app.get("/es/getCloud") 51 | def getCloud(): 52 | state = netes.getAllMusicCloud(1000) 53 | return state 54 | 55 | 56 | @app.get("/es/esLogout") 57 | def logout(): 58 | state = netes.logoutUser() 59 | return state 60 | 61 | 62 | @app.post("/es/bindSid2Asid") 63 | def bindSid2Asid(): 64 | data = request.get_json() 65 | state = netes.matchMusicSid2ASid(data) 66 | return state 67 | 68 | 69 | @app.get("/es/getUserPlaylist/") 70 | def getUserPlaylist(userid: str): 71 | state = netes.getUserPlaylist(userid) 72 | return { 73 | 'code': 200, 74 | 'list': state 75 | } 76 | 77 | 78 | @app.get("/wyy/search///") 79 | def essearch(searchKey: str, page=1, size=100): 80 | page = int(page) 81 | size = int(size) 82 | offset = (page - 1) * size 83 | if offset < 0 or size <= 0: 84 | return { 85 | 'code': 400 86 | } 87 | lst = netes.searchMusic( 88 | searchKey, int(page), int(size)) # Mac端搜索接口 89 | page = lst['page'] 90 | return { 91 | 'code': 200, 92 | 'list': lst['data'], 93 | 'page': page 94 | } 95 | 96 | 97 | @app.get("/es/getMusicListByPlaylistID///") 98 | def getMusicListByPlaylistID(playListID: str, page: str, size: str): 99 | page = int(page) 100 | size = int(size) 101 | offset = (page - 1) * size 102 | if offset < 0 or size <= 0: 103 | return { 104 | 'code': 400 105 | } 106 | state = netes.getPlayListAllMusic(playListID, size, offset) 107 | if type(state) == int: 108 | if state == -1: 109 | return { 110 | 'code': 20001 111 | } 112 | return { 113 | 'code': 200, 114 | 'list': state 115 | } 116 | 117 | 118 | def init(): 119 | pass 120 | -------------------------------------------------------------------------------- /WebSourceCode/src/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-auto-import 5 | export {} 6 | declare global { 7 | const EffectScope: typeof import('vue')['EffectScope'] 8 | const computed: typeof import('vue')['computed'] 9 | const createApp: typeof import('vue')['createApp'] 10 | const customRef: typeof import('vue')['customRef'] 11 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 12 | const defineComponent: typeof import('vue')['defineComponent'] 13 | const effectScope: typeof import('vue')['effectScope'] 14 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 15 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 16 | const h: typeof import('vue')['h'] 17 | const inject: typeof import('vue')['inject'] 18 | const isProxy: typeof import('vue')['isProxy'] 19 | const isReactive: typeof import('vue')['isReactive'] 20 | const isReadonly: typeof import('vue')['isReadonly'] 21 | const isRef: typeof import('vue')['isRef'] 22 | const markRaw: typeof import('vue')['markRaw'] 23 | const nextTick: typeof import('vue')['nextTick'] 24 | const onActivated: typeof import('vue')['onActivated'] 25 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 26 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 27 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 28 | const onDeactivated: typeof import('vue')['onDeactivated'] 29 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 30 | const onMounted: typeof import('vue')['onMounted'] 31 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 32 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 33 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 34 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 35 | const onUnmounted: typeof import('vue')['onUnmounted'] 36 | const onUpdated: typeof import('vue')['onUpdated'] 37 | const provide: typeof import('vue')['provide'] 38 | const reactive: typeof import('vue')['reactive'] 39 | const readonly: typeof import('vue')['readonly'] 40 | const ref: typeof import('vue')['ref'] 41 | const resolveComponent: typeof import('vue')['resolveComponent'] 42 | const shallowReactive: typeof import('vue')['shallowReactive'] 43 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 44 | const shallowRef: typeof import('vue')['shallowRef'] 45 | const toRaw: typeof import('vue')['toRaw'] 46 | const toRef: typeof import('vue')['toRef'] 47 | const toRefs: typeof import('vue')['toRefs'] 48 | const triggerRef: typeof import('vue')['triggerRef'] 49 | const unref: typeof import('vue')['unref'] 50 | const useAttrs: typeof import('vue')['useAttrs'] 51 | const useCssModule: typeof import('vue')['useCssModule'] 52 | const useCssVars: typeof import('vue')['useCssVars'] 53 | const useSlots: typeof import('vue')['useSlots'] 54 | const watch: typeof import('vue')['watch'] 55 | const watchEffect: typeof import('vue')['watchEffect'] 56 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 57 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 58 | } 59 | // for type re-export 60 | declare global { 61 | // @ts-ignore 62 | export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode } from 'vue' 63 | } 64 | -------------------------------------------------------------------------------- /WebSourceCode/src/utils/type/CloudResponse.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 3 | * # @作者 : 秋城落叶(QiuChenly) 4 | * # @邮件 : qiuchenly@outlook.com 5 | * # @文件 : 项目 [qqmusic] - CloudResponse.ts 6 | * # @修改时间 : 2023-03-14 08:35:43 7 | * # @上次修改 : 2023/3/14 下午8:35 8 | */ 9 | 10 | export interface CloudResponse { 11 | count: number; 12 | hasMore: boolean; 13 | list: List[]; 14 | } 15 | 16 | export interface List { 17 | addTime: number; 18 | album: string; 19 | artist: string; 20 | bitrate: number; 21 | cover: number; 22 | coverId: string; 23 | fileName: string; 24 | fileSize: number; 25 | lyricId: string; 26 | simpleSong: SimpleSong; 27 | songId: number; 28 | songName: string; 29 | version: number; 30 | } 31 | 32 | export interface SimpleSong { 33 | a: any; 34 | al: Al; 35 | alia: string[]; 36 | ar: Ar[]; 37 | cd?: string; 38 | cf?: string; 39 | copyright: number; 40 | cp: number; 41 | crbt?: string; 42 | djId: number; 43 | dt: number; 44 | fee: number; 45 | ftype: number; 46 | h?: H; 47 | id: number; 48 | l?: L; 49 | m?: M; 50 | mark: number; 51 | mst: number; 52 | mv: number; 53 | name: string; 54 | no: number; 55 | noCopyrightRcmd?: NoCopyrightRcmd; 56 | originCoverType: number; 57 | originSongSimpleData?: OriginSongSimpleData; 58 | pop: number; 59 | privilege: Privilege; 60 | pst: number; 61 | publishTime: number; 62 | rt?: string; 63 | rtUrl: any; 64 | rtUrls: any[]; 65 | rtype: number; 66 | rurl: any; 67 | s_id: number; 68 | single: number; 69 | st: number; 70 | t: number; 71 | v: number; 72 | tns?: string[]; 73 | } 74 | 75 | export interface Al { 76 | id: number; 77 | name?: string; 78 | pic: number; 79 | picUrl: string; 80 | pic_str?: string; 81 | tns: string[]; 82 | } 83 | 84 | export interface Ar { 85 | alias: any[]; 86 | id: number; 87 | name?: string; 88 | tns: any[]; 89 | } 90 | 91 | export interface H { 92 | br: number; 93 | fid: number; 94 | size: number; 95 | vd: number; 96 | } 97 | 98 | export interface L { 99 | br: number; 100 | fid: number; 101 | size: number; 102 | vd: number; 103 | } 104 | 105 | export interface M { 106 | br: number; 107 | fid: number; 108 | size: number; 109 | vd: number; 110 | } 111 | 112 | export interface NoCopyrightRcmd { 113 | songId?: string; 114 | type: number; 115 | typeDesc: string; 116 | } 117 | 118 | export interface OriginSongSimpleData { 119 | albumMeta: AlbumMeta; 120 | artists: Artist[]; 121 | name: string; 122 | songId: number; 123 | } 124 | 125 | export interface AlbumMeta { 126 | id: number; 127 | name: string; 128 | } 129 | 130 | export interface Artist { 131 | id: number; 132 | name: string; 133 | } 134 | 135 | export interface Privilege { 136 | chargeInfoList?: ChargeInfoList[]; 137 | cp: number; 138 | cs: boolean; 139 | dl: number; 140 | dlLevel: string; 141 | downloadMaxBrLevel: string; 142 | downloadMaxbr: number; 143 | fee: number; 144 | fl: number; 145 | flLevel: string; 146 | flag: number; 147 | freeTrialPrivilege: FreeTrialPrivilege; 148 | id: number; 149 | maxBrLevel: string; 150 | maxbr: number; 151 | payed: number; 152 | pl: number; 153 | plLevel: string; 154 | playMaxBrLevel: string; 155 | playMaxbr: number; 156 | preSell: boolean; 157 | rscl: any; 158 | sp: number; 159 | st: number; 160 | subp: number; 161 | toast: boolean; 162 | } 163 | 164 | export interface ChargeInfoList { 165 | chargeMessage: any; 166 | chargeType: number; 167 | chargeUrl: any; 168 | rate: number; 169 | } 170 | 171 | export interface FreeTrialPrivilege { 172 | listenType: any; 173 | resConsumable: boolean; 174 | userConsumable: boolean; 175 | } 176 | -------------------------------------------------------------------------------- /WebSourceCode/src/components/Index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 43 | 44 | 61 | 62 | 164 | -------------------------------------------------------------------------------- /md/DecompileFiles/EncryptAndDecrypt.java: -------------------------------------------------------------------------------- 1 | package DecompileFiles; 2 | 3 | import java.util.Calendar; 4 | import java.util.List; 5 | import java.util.Random; 6 | import javax.crypto.Cipher; 7 | import javax.crypto.spec.IvParameterSpec; 8 | import javax.crypto.spec.SecretKeySpec; 9 | 10 | /* loaded from: /Users/qiuchenly/Downloads/d.dex */ 11 | public class EncryptAndDecrypt { 12 | public static final String TestCookie = "1AxPKhgzRbWbIt8TfqfajraPgxZWmMhAoSh9HlWlPhFHQyVedFYNSOsPofZ/vj|J2XTtzdDIAqupT1T5tYMrN/u/qniED56dcBaUZSgXG2lN10Nc1OZIN87TsxcLwZQ1/TolMZ7f+oNiqQMPHs1Ff/Q==%aa2Ef93/cpOC3DyRvsNohA=="; 13 | 14 | private static String GetFullNumber(int num) { 15 | if (num < 10) { 16 | return "00" + num; 17 | } else if (num < 100) { 18 | return "0" + num; 19 | } else { 20 | return num + ""; 21 | } 22 | } 23 | 24 | private static char[] reverse(char[] clist) { 25 | for (int i = 0; i < clist.length / 2; i++) { 26 | char c = clist[(clist.length - i) - 1]; 27 | clist[(clist.length - i) - 1] = clist[i]; 28 | clist[i] = c; 29 | } 30 | return clist; 31 | } 32 | 33 | private static String arrToStr(char[] cArr) { 34 | String str = ""; 35 | for (int i = 0; i < cArr.length; i++) { 36 | str = str + cArr[i]; 37 | } 38 | return str; 39 | } 40 | 41 | private static String listToStr(List cList) { 42 | String str = ""; 43 | for (int i = 0; i < cList.size(); i++) { 44 | str = str + cList.get(i); 45 | } 46 | return str; 47 | } 48 | 49 | public static String encryptDES(String text, String key) { 50 | try { 51 | Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); 52 | cipher.init(1, new SecretKeySpec(key.getBytes(), "DES"), new IvParameterSpec(key.getBytes())); 53 | return java.util.Base64.getEncoder().encodeToString(cipher.doFinal(text.getBytes())).trim(); 54 | } catch (Exception e) { 55 | return e.getMessage(); 56 | } 57 | } 58 | 59 | // ok 60 | public static String decryptDES(String text, String key) { 61 | if (text == null || key == null) { 62 | return null; 63 | } 64 | try { 65 | byte[] decode = java.util.Base64.getDecoder().decode(text); 66 | Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); 67 | cipher.init(2, new SecretKeySpec(key.getBytes(), "DES"), new IvParameterSpec(key.getBytes())); 68 | return new String(cipher.doFinal(decode), "utf-8"); 69 | } catch (Exception e) { 70 | return e.getMessage(); 71 | } 72 | } 73 | 74 | // ok 75 | public static boolean decryptAndSetCookie(String enStr) { 76 | String replace = enStr.replace("-", "").replace("|", ""); 77 | if (replace.length() < 10 || !replace.contains("%")) { 78 | return false; 79 | } 80 | String[] split = replace.split("%"); 81 | String str = split[0]; 82 | String decryptDES = decryptDES(split[1], str.substring(0, 8)); 83 | if (decryptDES.length() < 8) { 84 | decryptDES = decryptDES + "QMD"; 85 | } 86 | Cookie.setCookie(decryptDES(str, decryptDES.substring(0, 8)), decryptDES); 87 | return true; 88 | } 89 | 90 | public static String encryptText(String text, String qq) { 91 | String key = ("QMD" + qq).substring(0, 8); 92 | StringBuilder sb = new StringBuilder(encryptDES(text, key)); 93 | Random random = new Random((long) Calendar.getInstance().get(5)); 94 | int nextInt = random.nextInt(4) + 1; 95 | for (int i = 0; i < nextInt; i++) { 96 | sb.insert(random.nextInt(sb.length()), "-"); 97 | } 98 | return sb.toString(); 99 | } 100 | 101 | public static String encryptText(String text) { 102 | return encryptText(text, Cookie.getQQ()); 103 | } 104 | 105 | public static String decryptText(String text, String qq) { 106 | return decryptDES(text.replace("-", ""), ("QMD" + qq).substring(0, 8)); 107 | } 108 | 109 | public static String decryptText(String text) { 110 | return decryptText(text, Cookie.getQQ()); 111 | } 112 | } -------------------------------------------------------------------------------- /md/DecompileFiles/TingXIaMain.java: -------------------------------------------------------------------------------- 1 | import javax.crypto.Cipher; 2 | import javax.crypto.spec.IvParameterSpec; 3 | import javax.crypto.spec.SecretKeySpec; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.FileInputStream; 6 | import java.io.IOException; 7 | import java.util.Arrays; 8 | import java.util.zip.Inflater; 9 | 10 | import static javax.crypto.Cipher.DECRYPT_MODE; 11 | 12 | public class Main { 13 | public static void main(String[] args) { 14 | System.out.println("Hello world!"); 15 | String d = new String(unzip(readFileByBytes("/Users/qiuchenly/Downloads/response"))); 16 | System.out.println(d); 17 | 18 | d = new String(unzip(readFileByBytes("/Users/qiuchenly/Downloads/request"))); 19 | d = new String(hex2byte(d)); 20 | byte[] bytes = hex2byte(d); 21 | d = new String(AesDecrypt(bytes, "6480fedae539deb2".getBytes())); 22 | System.out.println(d); 23 | } 24 | 25 | public static byte[] readFileByBytes(String fileName) { 26 | try { 27 | //传入文件路径fileName,底层实现 new FileInputStream(new File(fileName));相同 28 | FileInputStream in = new FileInputStream(fileName); 29 | //每次读10个字节,放到数组里 30 | byte[] bytes = new byte[1024]; 31 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 32 | int c; 33 | while ((c = in.read(bytes)) != -1) { 34 | byteArrayOutputStream.write(bytes, 0, c); 35 | } 36 | return byteArrayOutputStream.toByteArray(); 37 | } catch (Exception e) { 38 | // TODO: handle exception 39 | } 40 | return new byte[0]; 41 | } 42 | 43 | private static byte[] hex2byte(String str) { 44 | if (str == null || str.length() < 2) { 45 | return new byte[0]; 46 | } 47 | String lowerCase = str.toLowerCase(); 48 | int length = lowerCase.length() / 2; 49 | byte[] bArr = new byte[length]; 50 | for (int i = 0; i < length; i++) { 51 | int i2 = i * 2; 52 | String key = lowerCase.substring(i2, i2 + 2); 53 | int b = Integer.parseInt(key, 16); 54 | int a = b & 255; 55 | bArr[i] = (byte) a; 56 | } 57 | return bArr; 58 | } 59 | 60 | public static byte[] AesEncrypt(byte[] bArr, byte[] key) { 61 | try { 62 | if (key.length != 16) { 63 | return null; 64 | } 65 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); 66 | new IvParameterSpec(key); 67 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); 68 | cipher.init(1, secretKeySpec); 69 | try { 70 | return cipher.doFinal(bArr); 71 | } catch (Exception e) { 72 | System.out.println(e); 73 | return null; 74 | } 75 | } catch (Exception e2) { 76 | System.out.println(e2); 77 | return null; 78 | } 79 | } 80 | 81 | public static byte[] AesDecrypt(byte[] bArr, byte[] key) { 82 | try { 83 | if (key.length != 16) { 84 | return null; 85 | } 86 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); 87 | new IvParameterSpec(key); 88 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); 89 | cipher.init(DECRYPT_MODE, secretKeySpec); 90 | try { 91 | return cipher.doFinal(bArr); 92 | } catch (Exception e) { 93 | System.out.println(e); 94 | return null; 95 | } 96 | } catch (Exception e2) { 97 | System.out.println(e2); 98 | return null; 99 | } 100 | } 101 | 102 | public static byte[] unzip(byte[] bArr) { 103 | Inflater inflater = new Inflater(); 104 | inflater.reset(); 105 | inflater.setInput(bArr); 106 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(bArr.length); 107 | try { 108 | byte[] bArr2 = new byte[1024]; 109 | while (!inflater.finished()) { 110 | byteArrayOutputStream.write(bArr2, 0, inflater.inflate(bArr2)); 111 | } 112 | bArr = byteArrayOutputStream.toByteArray(); 113 | try { 114 | byteArrayOutputStream.close(); 115 | } catch (IOException e) { 116 | e.printStackTrace(); 117 | } 118 | } catch (Exception e2) { 119 | e2.printStackTrace(); 120 | } catch (Throwable th) { 121 | try { 122 | byteArrayOutputStream.close(); 123 | } catch (IOException e3) { 124 | e3.printStackTrace(); 125 | } 126 | throw th; 127 | } 128 | inflater.end(); 129 | return bArr; 130 | } 131 | } -------------------------------------------------------------------------------- /WebSourceCode/src/utils/Http.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 3 | * # @作者 : 秋城落叶(QiuChenly) 4 | * # @邮件 : qiuchenly@outlook.com 5 | * # @文件 : 项目 [qqmusic] - Http.ts 6 | * # @修改时间 : 2023-03-20 02:05:20 7 | * # @上次修改 : 2023/3/20 上午2:05 8 | */ 9 | 10 | import axios, { AxiosResponse } from "axios"; 11 | import { 12 | InitAnonimous, 13 | SearchMusicResult, 14 | SearchMusicResultSingle, 15 | } from "@/utils/type/BasicType"; 16 | import { NetEaseUserInfo } from "@/utils/type/UserInfoDetail"; 17 | import { MusicPlaylist } from "@/utils/type/NeteaseMusicPlayList"; 18 | import { NeteasePlayListSongs } from "@/utils/type/NetEasePlayListSong"; 19 | import { CloudResponse, List as mList } from "@/utils/type/CloudResponse"; 20 | 21 | const userStore = () => { 22 | return { 23 | token: "test", 24 | }; 25 | }; 26 | 27 | const config = { 28 | baseURL: "", 29 | // baseURL: "http://localhost:8899", // 本地测试时使用 30 | timeout: 15000, 31 | headers: { 32 | "Content-Type": "multipart/form-data;application/json;charset=UTF-8;", 33 | }, 34 | }; 35 | 36 | const http = axios.create(config); 37 | 38 | //请求拦截器 39 | http.interceptors.request.use( 40 | (config) => { 41 | // 可使用async await 做异步操作 42 | const token = userStore().token; 43 | if (token) { 44 | config.headers["token"] = token; 45 | // console.log("config.method", config); 46 | // if (config.method === "POST") { 47 | // //@ts-ignore 48 | // config.data = JSON.stringify(config.data); 49 | } 50 | return config; 51 | }, 52 | (error) => { 53 | return Promise.resolve(error); 54 | } 55 | ); 56 | 57 | // 响应拦截器 58 | http.interceptors.response.use( 59 | (response) => { 60 | console.log(response); 61 | return response; 62 | }, 63 | (error) => { 64 | //未登录时清空缓存跳转 65 | // if (error.statusCode == 401) { 66 | // uni.clearStorageSync(); 67 | // uni.switchTab({ 68 | // url: "/pages/index/index.vue" 69 | // }) 70 | // } 71 | // uni.showToast({ 72 | // title: "网络开了小差~", 73 | // icon: "error", 74 | // mask: true, 75 | // }); 76 | return Promise.resolve(error); 77 | } 78 | ); 79 | export const Client = http; 80 | 81 | export const Api = { 82 | pack(res: AxiosResponse) { 83 | return res.data; 84 | }, 85 | async get(url: string) { 86 | const r = await Client.get(url); 87 | return this.pack(r); 88 | }, 89 | async post(url: string, data: object) { 90 | const r = await Client.post(url, data, { 91 | headers: { 92 | "Content-Type": "application/json", 93 | }, 94 | }); 95 | return this.pack(r); 96 | }, 97 | async status() { 98 | return this.get<{ 99 | code: Number; 100 | }>("/status"); 101 | }, 102 | async searchMusic(key: string, page: number, type = "qq", size = 30) { 103 | let url = "/" + type + "/search/" + key + "/" + page + "/" + size; 104 | return this.get(url); 105 | }, 106 | async searchMusicForMyFreeMp3( 107 | type = "myfreemp3", 108 | data: { 109 | page: number; 110 | text: string; 111 | token: string; 112 | type: string; 113 | v: string; 114 | } = { 115 | page: 1, 116 | text: "", 117 | token: "", 118 | type: "YQM", 119 | v: "beta", 120 | } 121 | ) { 122 | let url = "/" + type + "/search"; 123 | return this.post(url, data); 124 | }, 125 | postDownload(data: object, config: object) { 126 | return this.post<{ 127 | code: number; 128 | }>("/download", { 129 | music: data, 130 | config: config, 131 | }); 132 | }, 133 | setBaseConfig(param: { folder: string; num: number }) { 134 | return this.post("/config", param); 135 | }, 136 | getBaseConfig() { 137 | return this.get<{ folder: string; num: number }>("/getConfig"); 138 | }, 139 | getNeteaseQRCode() { 140 | return this.get<{ 141 | code: Number; 142 | qrcode: { 143 | url: string; 144 | b64: string; 145 | uniKey: string; 146 | }; 147 | }>("/es/qrLogin"); 148 | }, 149 | checkESState: (unikey: string) => 150 | Api.get<{ 151 | code: number; 152 | cookie: string; 153 | }>("/es/checkLoginState/" + unikey), 154 | getNetEaseUserInfo: () => Api.get("/es/getUserInfo"), 155 | /** 156 | * 把本地保存的cookie设置进去 防止二次登录 157 | * @param data 158 | */ 159 | setESCookie: (data: { cookie: string }) => 160 | Api.post<{ 161 | code: number; 162 | }>("/es/setCookie", data), 163 | initAnonimous: () => Api.get("/es/initAnonimous"), 164 | getUserPlaylist: (userid: string) => 165 | Api.get("/es/getUserPlaylist/" + userid), 166 | getMusicListByPlaylistID: (playListID: string, page: number, size: number) => 167 | Api.get( 168 | `/es/getMusicListByPlaylistID/${playListID}/${page}/${size}` 169 | ), 170 | getNeteaseCloud: () => Api.get(`/es/getCloud?${Date.now()}`), 171 | delNeteaseCloud: () => Api.get(`/es/getCloud`), 172 | bindSid2Asid: (data: { sid: number; asid: number; uid: number }) => 173 | Api.post<{ message: string; code: number }>(`/es/bindSid2Asid`, data), 174 | esLogout() { 175 | return Api.get("/es/esLogout"); 176 | }, 177 | }; 178 | -------------------------------------------------------------------------------- /md/DecompileFiles/ting.java: -------------------------------------------------------------------------------- 1 | this.f3212_GetMusicUtils1.getMusic(1, this.qqMid, GetMusicUtils.Type.f130qq, str4); 2 | public static final String f130qq = "qq"; 3 | 4 | public void m4150_1$(int i, String str, String str2) { 5 | String str3 = this.f3213_qmd1.mo2398(C1452.cookie, str2)[0]; 6 | String str4 = GetMusicUtils.Tone.mp3; 7 | if (i != 0 && i != 1) { 8 | str4 = i == 2 ? GetMusicUtils.Tone._320kmp3 : i == 3 ? GetMusicUtils.Tone.f129sq : ""; 9 | } 10 | this.f3212_GetMusicUtils1.mo3080(1, this.qqMid, GetMusicUtils.Type.f130qq, str4); 11 | } 12 | public static final String _320kmp3 = "320kmp3"; 13 | 14 | /* renamed from: hq */ 15 | public static final String f127hq = "hq"; 16 | 17 | /* renamed from: hr */ 18 | public static final String f128hr = "hr"; 19 | public static final String mp3 = "mp3"; 20 | 21 | /* renamed from: sq */ 22 | public static final String f129sq = "sq"; 23 | 24 | public static String byteToHexString(byte[] arr_b) { 25 | StringBuffer stringBuffer0 = new StringBuffer(); 26 | int v; 27 | for(v = 0; v < arr_b.length; ++v) { 28 | String s = Integer.toHexString(arr_b[v]).toUpperCase(); 29 | if(s.length() > 3) { 30 | stringBuffer0.append(s.substring(6)); 31 | } 32 | else if(s.length() < 2) { 33 | stringBuffer0.append("0" + s); 34 | } 35 | else { 36 | stringBuffer0.append(s); 37 | } 38 | } 39 | 40 | return stringBuffer0.toString(); 41 | } 42 | 43 | public static void getMusic(String s, String s1, String s2, Callback getMusicUtils$Callback0) { 44 | String s3 = Build.MODEL; 45 | int v = Build.VERSION.SDK_INT; 46 | String s4 = System.currentTimeMillis() / 1000L + ""; 47 | String s5 = GetMusicUtils.md5("f389249d91bd845c9b817db984054cfb" + s4 + "6562653262383463363633646364306534333663").toLowerCase(); 48 | String s6 = "{\\\"method\\\":\\\"GetMusicUrl\\\",\\\"platform\\\":\\\"" + s1 + "\\\",\\\"t1\\\":\\\"" + s + "\\\",\\\"t2\\\":\\\"" + s2 + "\\\"}"; 49 | String s7 = "{\\\"uid\\\":\\\"\\\",\\\"token\\\":\\\"\\\",\\\"deviceid\\\":\\\"84c599d711066ef740eb49109dac9782\\\",\\\"appVersion\\\":\\\"4.1.0.V4\\\",\\\"vercode\\\":\\\"4100\\\",\\\"device\\\":\\\"" + s3 + "\\\",\\\"osVersion\\\":\\\"" + v + "\\\"}"; 50 | String s8 = "{\n\t\"text_1\":\t\"" + s6 + "\",\n\t\"text_2\":\t\"" + s7 + "\",\n\t\"sign_1\":\t\"" + s5 + "\",\n\t\"time\":\t\"" + s4 + "\",\n\t\"sign_2\":\t\"" + GetMusicUtils.md5(s6.replace("\\", "") + s7.replace("\\", "") + s5 + s4 + "NDRjZGIzNzliNzEx").toLowerCase() + "\"\n}"; 51 | Log.d("GetMusicUtils", s8); 52 | String s9 = new String[]{"http://app.kzti.top:1030/client/cgi-bin/api.fcg", "http://119.91.134.171:1030/client/cgi-bin/api.fcg"}[new Random().nextInt(2)]; 53 | Log.d("GetMusicUtils", "getMusic: " + s9); 54 | new Thread(() -> { 55 | String s1 = new String(GetMusicUtils.unzip(new Request().url(s9).post().header("Connection", "Keep-Alive").header("Content-Type", "gcsp/stream").header("Accept-Encoding", "gzip").contentByte(GetMusicUtils.gzip(GetMusicUtils.byteToHexString(GetMusicUtils.byteToHexString(GetMusicUtils.AesEncrypt(s8.getBytes(), "6480fedae539deb2".getBytes())).getBytes()).getBytes())).exec().body().bytes())); 56 | new Handler(Looper.getMainLooper()).post(() -> try { 57 | Log.d("GetMusicUtils", "getMusic: " + s1); 58 | getMusicUtils$Callback0.onMusicUrl(new JSONObject(s1).getString("data")); 59 | } 60 | catch(Exception exception0) { 61 | exception0.printStackTrace(); 62 | getMusicUtils$Callback0.onMusicUrl(""); 63 | }); 64 | }).start(); 65 | } 66 | 67 | public static byte[] AesEncrypt(byte[] bArr, byte[] bArr2) { 68 | try { 69 | if (bArr2.length != 16) { 70 | return null; 71 | } 72 | SecretKeySpec secretKeySpec = new SecretKeySpec(bArr2, "AES"); 73 | new IvParameterSpec(bArr2); 74 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); 75 | cipher.init(1, secretKeySpec); 76 | try { 77 | return cipher.doFinal(bArr); 78 | } catch (Exception e) { 79 | System.out.println(e.toString()); 80 | return null; 81 | } 82 | } catch (Exception e2) { 83 | System.out.println(e2.toString()); 84 | return null; 85 | } 86 | } 87 | 88 | 89 | public static /* synthetic */ void lambda$getMusic$3(String str, byte[] bArr, final Callback callback) { 90 | final String str2 = new String(unzip(new HttpUtils.Request().url(str).post().header(HttpHeaders.HEAD_KEY_CONNECTION, "Keep-Alive") 91 | .header(HttpHeaders.HEAD_KEY_CONTENT_TYPE, "gcsp/stream") 92 | .header(HttpHeaders.HEAD_KEY_ACCEPT_ENCODING, "gzip") 93 | .contentByte(bArr) 94 | .exec() 95 | .body() 96 | .bytes())); 97 | new Handler(Looper.getMainLooper()).post(new Runnable() { // from class: com.e4a.runtime.components.impl.android.啾啾_GetMusicUtils类库.-$$Lambda$GetMusicUtils$ReMB7S0rzNwgvDlph_pGjbelpBU 98 | @Override // java.lang.Runnable 99 | public final void run() { 100 | GetMusicUtils.lambda$null$2(str2, callback); 101 | } 102 | }); 103 | } -------------------------------------------------------------------------------- /WebSourceCode/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 66 | 67 | 135 | 136 | 242 | -------------------------------------------------------------------------------- /md/README.md: -------------------------------------------------------------------------------- 1 | # 逆向一个 QMD QQ 音乐源下载软件 2 | 3 | 这个 Apk 主要是用来下载 QQ 音乐的无损数字音频文件,我为了把我 iMac 上的 mp3 音质音乐替换为 flac 或者 HiRes 4 | 无损,一个个去网上找文件。偶然间在网上发现了这个 app,有些好奇怎么实现的,于是做本篇分析文章。 5 | 6 | ## 第一步 反射大师脱壳 7 | 8 | 对于类似于这种神奇的软件作者总会加个壳加加固保护一下源代码,我有种直觉这玩意应该也是加了固的,果然打开 zip 9 | 文件一看:![](media/16589902696730/16589910239841.png) 10 | libjiagu.so 赫然在目。。。得,直接打开反射大师先来扒一层皮看看能不能看到里面。 11 | **反射大师脱壳过程不再赘述,直接导出内存 dex 即可。** 12 | 13 | ## 第二步 JEB 静态分析 14 | 15 | 可喜可贺,2021 年初还只有 Jeb 3.24,坐了一年牢出来发现竟然有 4.x 的版本更新了,好,很有精神! 16 | 17 | 首先我们打开 app 开始下载高解析度音频,看看加密如何。 18 | ![](media/16589902696730/16589915329732.jpg) 19 | 20 | 我们关注一下上图的/api/Download 请求。 21 | 这个包是用来获取 QQ 音乐的文件实际下载地址,我们来看看这个请求: 22 | 一个迷之数据,一个朴实无华的 http 请求,再无其他。 23 | 只有下面的一个http: 24 | //ws.stream.qqmusic.qq.com/RS01003zLSuX07Z0LB.flac接口关联他。那么为了得到这个qqmusic的源数据,我们要批量下载这些音乐就需要逆向出这个迷之数据到底是什么东西,我们如何生成它。 25 | 26 | ``` 27 | 接口表 28 | 29 | 获取音乐的高解析度下载地址 30 | /api/MusicLink/link 31 | ``` 32 | 33 | 那么话不多说,jeb 直接打开导出的 dex 看函数,搜索这个字符串。 34 | ![](media/16589902696730/16589918835312.jpg) 35 | 36 | 直接可以看到这个搜索结果了,很好,看来不需要再去找其他的 dex 文件了。 37 | tab 一下看看。 38 | ![](media/16589902696730/16589919873070.jpg) 39 | 关注以下函数: 40 | 41 | ```java 42 | public String getMusicLink(String arg4) { 43 | String v4 = EncryptAndDecrypt.encryptText(arg4); 44 | String v4_1 = new HttpManager("http://8.136.185.193/api/MusicLink/link").postDataWithResult("\"" + v4 + "\""); 45 | Logger.e(v4_1, new Object[0]); 46 | return v4_1; 47 | } 48 | ``` 49 | 50 | v4=arg4,arg4 则是一个 String 不足为惧,先看看这个写的非常漂亮的 Encryption 函数: 51 | 52 | ```java 53 | public static String encryptText(String arg1) { 54 | return EncryptAndDecrypt.encryptText(arg1, Cookie.getQQ()); 55 | } 56 | 57 | public static String encryptText(String arg4, String arg5) { 58 | if(!TextUtils.isEmpty(arg4) && !TextUtils.isEmpty(arg5)) { 59 | int v1 = 0; 60 | StringBuilder v5 = new StringBuilder(EncryptAndDecrypt.encryptDES(arg4, "QMD" + arg5.substring(0, 8))); 61 | Random v4 = new Random(((long)Calendar.getInstance().get(5))); 62 | int v0 = v4.nextInt(4) + 1; 63 | while(v1 < v0) { 64 | v5.insert(v4.nextInt(v5.length()), "-"); 65 | ++v1; 66 | } 67 | 68 | return v5.toString(); 69 | } 70 | 71 | return ""; 72 | } 73 | ``` 74 | 75 | 他调用了 encryptText(String arg4, String arg5)函数,那么我们就可知道 arg5 是作为密码而存在的,arg4 则是 String 的原文,那么 76 | Cookie.getQQ()这个代码则就相当的可疑。 77 | 跳转一下看看: 78 | 79 | ```java 80 | public class Cookie { 81 | private static String Mkey; 82 | private static String QQ; 83 | public static String getMkey() { return Cookie.Mkey; } 84 | public static String getQQ() { return Cookie.QQ; } 85 | public static void setCookie(String arg0, String arg1) { 86 | Cookie.Mkey = arg0; 87 | Cookie.QQ = arg1; 88 | } 89 | } 90 | ``` 91 | 92 | 一个静态实体类,直接看 setCookie 的交叉引用看看是从 decryptAndSetCookie 函数设置密码的: 93 | 94 | ```java 95 | public static boolean decryptAndSetCookie(String arg5) { 96 | String v5 = arg5.replace("-", "").replace("|", ""); 97 | if(v5.length() >= 10 && (v5.contains("%"))) { 98 | String[] v5_1 = v5.split("%"); 99 | String v0 = v5_1[0]; 100 | String v5_2 = EncryptAndDecrypt.decryptDES(v5_1[1], v0.substring(0, 8)); 101 | if(v5_2.length() < 8) { 102 | v5_2 = v5_2 + "QMD"; 103 | } 104 | 105 | Cookie.setCookie(EncryptAndDecrypt.decryptDES(v0, v5_2.substring(0, 8)), v5_2);//v5_2就是密码,由arg5参数分解而来。 106 | return true; 107 | } 108 | 109 | return false; 110 | } 111 | ``` 112 | 113 | 继续跟踪交叉引用: 114 | 115 | ```java 116 | public boolean getCookie() { 117 | String v0 = new HttpManager("http://8.136.185.193/api/Cookies").postDataWithResult(new Gson().toJson(SystemInfoUtil.getDeviceInfo())); 118 | return TextUtils.isEmpty(v0) ? false : EncryptAndDecrypt.decryptAndSetCookie(v0); 119 | } 120 | ``` 121 | 122 | 找到了,看来是从这个接口获取的数据,但是这个接口居然是 POST 提交,那么我们就有必要看看这个提交的数据 123 | SystemInfoUtil.getDeviceInfo()到底是什么东西: 124 | 125 | ```java 126 | public static final DeviceInfo getDeviceInfo() { 127 | DeviceInfo v10 = new DeviceInfo(SystemInfoUtil.getUID(), SystemInfoUtil.getSystemModel(), SystemInfoUtil.getDeviceBrand(), SystemInfoUtil.getAppVersionName(), SystemInfoUtil.getSystemVersion(), SystemInfoUtil.getAppVersionCode() + "", null, 0x40, null); 128 | v10.setIp(EncryptAndDecrypt.encryptText(v10.getUid() + v10.getDeviceModel() + v10.getDeviceBrand() + v10.getSystemVersion() + v10.getAppVersion() + v10.getVersionCode(), "F*ckYou!"));//密码是F*ckYou!,emmmm.... 129 | return v10; 130 | } 131 | ``` 132 | 133 | 获取了设备的一些信息,然后调用了一个 setIp 函数,这个加密看起来像是一个接口签名防止被抓包调用接口,提高逆向成本。 134 | ![](media/16589902696730/16589927417533.jpg) 135 | 直接抓包看数据,可以看出来其实就是几个字符串 appand 一起后加上密码 des,下面我们就开始先用 python 实现一下。 136 | 137 | 不过在此之前我们还要看一下 EncryptAndDecrypt.encryptText 函数,里面是如何处理的: 138 | 139 | ```java 140 | public static String encryptText(String arg4, String arg5) { 141 | if(!TextUtils.isEmpty(arg4) && !TextUtils.isEmpty(arg5)) { 142 | int v1 = 0; 143 | StringBuilder v5 = new StringBuilder(EncryptAndDecrypt.encryptDES(arg4, ("QMD" + arg5).substring(0, 8))); 144 | Random v4 = new Random(((long)Calendar.getInstance().get(5))); 145 | int v0 = v4.nextInt(4) + 1; 146 | while(v1 < v0) { 147 | v5.insert(v4.nextInt(v5.length()), "-"); 148 | ++v1; 149 | } 150 | 151 | return v5.toString(); 152 | } 153 | 154 | return ""; 155 | } 156 | ``` 157 | 158 | 清晰地看到又调用了 EncryptAndDecrypt.encryptDES 函数,还加上了“QMD”作为密码前置字符串,我们继续跟踪: 159 | 160 | ```java 161 | public static String encryptDES(String arg5, String arg6) { 162 | if(arg5 != null && arg6 != null) { 163 | try { 164 | Cipher v0 = Cipher.getInstance("DES/CBC/PKCS5Padding"); 165 | v0.init(1, new SecretKeySpec(arg6.getBytes(), "DES"), new IvParameterSpec(arg6.getBytes())); 166 | return Base64.encodeToString(v0.doFinal(arg5.getBytes()), 0).trim(); 167 | } 168 | catch(Exception v5) { 169 | return v5.getMessage(); 170 | } 171 | } 172 | 173 | return null; 174 | } 175 | ``` 176 | 177 | 到这里我们已经很清晰了,密码作为 iv,加密方式为 des/cbc/pkcs5padding 方式填充结果,那么我们用 python 实现一下这个加密函数: 178 | ![](media/16589902696730/16591253982291.jpg) 179 | ![](media/16589902696730/16591257371138.jpg) 180 | 181 | 到这里我们就用 python 写出了加密算法,接下来就可以用这个算法生成数据去请求 http 数据了。 182 | 183 | ## 第三步 测试接口访问 184 | 185 | ![](media/16589902696730/16591588597095.jpg) 186 | 187 | ![](media/16589902696730/16591604097922.jpg) 188 | ![](media/16589902696730/16591625487605.jpg) 189 | 190 | 于是为了下载 flac,我直接写了一个 python 脚本。 191 | 192 | ``` 193 | 项目地址 https://github.com/QiuChenly/python_down_jaychou 194 | ``` 195 | -------------------------------------------------------------------------------- /WebSourceCode/src/components/Cloud.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 93 | 94 | 207 | 208 | 216 | -------------------------------------------------------------------------------- /flaskSystem/src/Api/Kuwo.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - Kuwo.py 5 | # @修改时间 : 2023-04-23 03:31:41 6 | # @上次修改 : 2023/4/23 下午3:31 7 | import uuid 8 | import re 9 | 10 | from flaskSystem.src.Common.EncryptTools import KuwoDES 11 | from flaskSystem.src.Api.BaseApi import BaseApi 12 | from flaskSystem.src.Common import Http 13 | from flaskSystem.src.Common.Http import HttpRequest 14 | from flaskSystem.src.Common.Tools import subString 15 | from flaskSystem.src.Types.Types import Songs 16 | 17 | 18 | class KwApi(BaseApi): 19 | __csrf = '' 20 | 21 | httpClient: HttpRequest = None 22 | 23 | def __init__(self): 24 | self.httpClient = Http.HttpRequest() 25 | self.generateCSRFToken() 26 | self.__KuwoDES = KuwoDES() 27 | 28 | def search(self, searchKey: str) -> list[Songs]: 29 | pass 30 | 31 | def getUrl(self, url: str, method=0, data=None): 32 | res = self.httpClient.getHttp2Json(url, method, data, { 33 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50", 34 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 35 | 'csrf': self.__csrf, 36 | "Referer": "https://www.kuwo.cn/search/list?key=" # 如果不设置来源就会403禁止访问 37 | }) 38 | ck = res.headers.get("Set-Cookie") 39 | if ck is not None: 40 | kw_token = subString( 41 | ck, "kw_token=", ";" 42 | ) 43 | self.__csrf = kw_token 44 | print("kw_token已生成", kw_token) 45 | return res 46 | 47 | def getReqId(self): 48 | return uuid.uuid4().__str__() 49 | 50 | def generateCSRFToken(self): 51 | """ 52 | 由于网页端的限制 需要先生成csrf的值 53 | Returns: 54 | 55 | """ 56 | res = self.getUrl("https://www.kuwo.cn/search/list?key=%E5%91%A8") 57 | 58 | def getMusicInfo(self, mid: str): 59 | """ 60 | 获取歌曲详细信息 61 | Args: 62 | mid: 63 | 64 | Returns: 65 | 66 | """ 67 | u = f'https://www.kuwo.cn/api/www/music/musicInfo?mid={mid}&httpsStatus=1&reqId={self.getReqId()}' 68 | res = self.getUrl(u) 69 | return res.json() 70 | 71 | def search_kw_mac(self, searchKey: str, page_num: int = 1, page_size=100): 72 | url = 'http://search.kuwo.cn/r.s?' \ 73 | 'user=&idfa=&' \ 74 | 'openudid=&' \ 75 | 'uuid=&prod=kwplayer_mc_1.7.0&corp=kuwo&source=kwplayer_mc_1.7.0&' \ 76 | 'uid=&ver=kwplayer_mc_1.7.0&loginid=0&client=kt&cluster=0&strategy=2012&ver=mbox&' \ 77 | f'show_copyright_off=1&encoding=utf8&rformat=json&mobi=1&vipver=1&pn={page_num}&rn={page_size}&' \ 78 | f'all={searchKey}&ft=music' 79 | res = self.getUrl(url) 80 | res = res.json() 81 | lst = [] 82 | for li in res['abslist']: 83 | _format = li['MINFO'].split(';')[0].split(",") 84 | extra = _format[2].split(":")[1] 85 | bitrate = _format[1].split(":")[1] 86 | it = { 87 | 'prefix': bitrate, 88 | 'extra': extra, 89 | 'notice': "FLAC 无损音质" if extra == 'flac' else f'{extra.upper()} {bitrate}Kbps', 90 | 'mid': li['DC_TARGETID'], 91 | 'musicid': li['DC_TARGETID'], 92 | 'songmid': li['DC_TARGETID'], 93 | 'size': _format[3].split(":")[1].upper(), 94 | 'title': li['NAME'], 95 | 'singer': li['ARTIST'], 96 | 'album': li['ALBUM'], 97 | 'time_publish': "无", 98 | # 'hasLossless': li['hasLossless'], 99 | 'readableText': f"{li['ARTIST']} - {li['NAME']}" 100 | } 101 | # 如果要优化加载速度可以不要这个 102 | # time = self.getMusicInfo(it['mid']) 103 | # t = time['data']['releaseDate'] 104 | # it['time_publish'] = t 105 | lst.append(it) 106 | return { 107 | 'data': lst, 108 | 'page': { 109 | 'size': res['TOTAL'], 110 | 'next': page_num + 1, 111 | 'cur': res['PN'], 112 | 'searchKey': searchKey 113 | } 114 | } 115 | 116 | def search_kw(self, searchKey: str, page_num: int = 1, page_size=100): 117 | url = f"https://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key={searchKey}" \ 118 | f"&pn={page_num}&rn={page_size}&httpsStatus=1&reqId={self.getReqId()}" 119 | res = self.getUrl(url) 120 | res = res.json() 121 | data = res['data'] 122 | lst = [ 123 | { 124 | 'prefix': "无前缀信息", 125 | 'extra': "flac" if li['hasLossless'] is True else 'mp3', 126 | 'notice': "Flac无损音质" if li['hasLossless'] is True else '超高品320/192/128Kbps', 127 | 'mid': li['rid'], 128 | 'musicid': li['musicrid'], 129 | 'songmid': li['rid'], 130 | 'size': "无大小信息", 131 | 'title': li['name'], 132 | 'singer': li['artist'], 133 | 'album': li['album'], 134 | 'time_publish': li['releaseDate'], 135 | # 'hasLossless': li['hasLossless'], 136 | 'readableText': f"{li['releaseDate']} {li['artist']} - {li['name']}" 137 | } for li in data['list'] 138 | ] 139 | return { 140 | 'data': lst, 141 | 'page': { 142 | 'size': data['total'], 143 | 'next': page_num + 1, 144 | 'cur': page_num, 145 | 'searchKey': searchKey 146 | } 147 | } 148 | 149 | def getDownloadUrl(self, mid: int): 150 | """ 151 | 网页端接口 152 | Args: 153 | mid: 154 | 155 | Returns: 156 | 157 | """ 158 | # "N_MINFO": "level:ff,bitrate:2000,format:flac,size:29.97Mb;level:pp,bitrate:1000,format:ape, 159 | # size:29.74Mb;level:p,bitrate:320,format:mp3,size:10.29Mb;level:h,bitrate:128,format:mp3, 160 | # size:4.11Mb;level:s,bitrate:24,format:aac,size:816.79Kb;level:zp,bitrate:20000,format:zp,size:zpMb", 161 | url = f"https://www.kuwo.cn/api/v1/www/music/playUrl?mid={mid}" \ 162 | "&type=music&httpsStatus=1&reqId=" + self.getReqId() 163 | res = self.getUrl(url) 164 | res = res.json() 165 | return res['data']['url'] 166 | 167 | __KuwoDES: KuwoDES = None 168 | 169 | def getDownloadUrlV2(self, mid: str, br='1000kape'): 170 | """ 171 | 下载地址解析 172 | Args: 173 | mid: 音乐id 174 | br: 波特率类型 1000kape 320kmp3 192kmp3 128kmp3 175 | 176 | Returns: 177 | # { 178 | # "code": 200, 179 | # "msg": "success", 180 | # "url": "https://sy-sycdn.kuwo.cn/7e43dfa6b7295af0e4257a59e5007f6b/6404949a/resource/s1/4/85/520276467.ape" 181 | # } 182 | # or 'failed' 表示搜索不到这个波特率的歌曲 183 | """ 184 | # 1000kape 320kmp3 192kmp3 128kmp3 185 | # url = f'https://antiserver.kuwo.cn/anti.s?type=convert_url3&rid=82988488&format=mp3&response=url&br=320kmp3' 186 | # url = f'https://antiserver.kuwo.cn/anti.s?type=convert_url3&rid={mid}&br={br}' 187 | url = f'https://antiserver.kuwo.cn/anti.s?type=convert_url3&rid={mid}&format=mp3&response=url&br={br}' 188 | res = self.getUrl(url) 189 | return res 190 | 191 | def getDownloadUrlByApp(self, mid: str): 192 | """ 193 | 根据加密算法的到App协议的直链接 194 | 195 | 感谢@helloplhm-qwq(https://github.com/helloplhm-qwq)的提交 196 | Args: 197 | mid: 媒体id 198 | 199 | Returns: 200 | 直链地址 201 | """ 202 | willEnc = f'corp=kuwo&p2p=1&type=convert_url2&format=flac|mp3|aac&rid={mid}' 203 | url = f'''http://nmobi.kuwo.cn/mobi.s?f=kuwo&q={self.__KuwoDES.base64_encrypt(willEnc)}''' 204 | res = self.getUrl(url) 205 | link = subString(res.text, "url=", "\r\n") 206 | return link 207 | -------------------------------------------------------------------------------- /flaskSystem/src/Common/Tools.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : 1925374620@qq.com 4 | # @文件 : 项目 [qqmusic] - Tools.py 5 | # @修改时间 : 2023-07-24 04:28:10 6 | # @上次修改 : 2023/7/24 上午4:27 7 | 8 | # 部分函数功能优化,错误修复 9 | # @作者 : QingXuDw 10 | # @邮件 : wangjingye55555@outlook.com 11 | import base64 12 | import os 13 | import threading 14 | 15 | import requests 16 | 17 | from flaskSystem.API.qq import QQApi 18 | 19 | 20 | def subString(text: str, left: str, right: str): 21 | """ 22 | 取文本中间 23 | Args: 24 | text: 完整文本 25 | left: 左边文本 26 | right: 右边文本 27 | 28 | Returns: 29 | 返回中间的文本 30 | 31 | """ 32 | leftInx = text.find(left) 33 | leftInx += len(left) 34 | rightInx = text.find(right, leftInx) 35 | txt = text[leftInx:rightInx] 36 | return txt 37 | 38 | 39 | threadLock = threading.Lock() # 多线程锁 防止同时创建同一个文件夹冲突 40 | 41 | 42 | def fixWindowsFileName2Normal(texts=''): 43 | """ 44 | 修正windows的符号问题\n 45 | 限制规则:https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file (2023/03/13) 46 | 47 | @作者: QingXuDw\n 48 | @邮件: wangjingye55555@outlook.com 49 | 50 | 参数: 51 | texts (str, optional): 通常类型字符串. 默认值为 ''. 52 | 53 | 返回值: 54 | str: 替换字符后的结果 55 | """ 56 | RESERVED_CHARS = [ord(c) for c in list('<>:\"/\\|?*')] # Reserved characters in Windows 57 | CONTROL_CHARS = list(range(0, 32, 1)) # Control characters of ascii 58 | REP_RESERVED_CHARS = [ord(c) for c in 59 | list('《》:“、、-?+')] # Replace reserved characters in Windows with similar characters 60 | # noinspection PyTypeChecker 61 | TRANS_DICT = dict(zip(CONTROL_CHARS + RESERVED_CHARS, [None] * 32 + REP_RESERVED_CHARS)) 62 | RESTRICT_STRS = ['con', 'prn', 'aux', 'nul', 'com0', 'com1', # Restricted file names in Windows 63 | 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 64 | 'com8', 'com9', 'lpt0', 'lpt1', 'lpt2', 'lpt3', 65 | 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'] 66 | trans_table = str.maketrans(TRANS_DICT) 67 | texts = texts.translate(trans_table) 68 | equal_text = texts.casefold() 69 | for restrict_str in RESTRICT_STRS: 70 | if equal_text == restrict_str: 71 | texts = f'_{texts}_' 72 | break 73 | return texts.strip() 74 | 75 | 76 | def handleKuwo(mid: str, type: str): 77 | from flaskSystem.API.kw import kw 78 | # url = kw.getDownloadUrlV2(mid, type) 79 | # if url.text == 'failed' or url.text == 'res not found': 80 | # return None 81 | # return url.json()['url'] 82 | 83 | url = kw.getDownloadUrlByApp(mid) 84 | if len(url) < 10: # 这里会返回一个很长的网址 所以一定超过10判定成功 85 | return None 86 | return url 87 | 88 | 89 | def handleMigu(mid: str, _type: str): 90 | from flaskSystem.API.kw import mg 91 | url = mg.getDownloadLink(mid, _type) 92 | if url is None: 93 | return None 94 | return url 95 | 96 | 97 | def handleWyy(mid): 98 | from flaskSystem.API.es import netes 99 | url = netes.getMusicUrl(mid) 100 | print("解析网易云歌曲下载接口:", url) 101 | if url['br'] == -1: 102 | return None 103 | return url['url'] 104 | 105 | 106 | def handleQQ(music, musicFileInfo): 107 | songmid = music['songmid'] 108 | # musicid = music['musicid'] 109 | # link = getQQMusicDownloadLinkByMacApp(file, songmid) 110 | # link = getQQMusicDownloadLinkV1(file, songmid) # 早期方法 可食用 111 | # vkey = link['purl'] 112 | # link = f'http://ws.stream.qqmusic.qq.com/{vkey}&fromtag=140' 113 | # if vkey == '': 114 | # print(f"找不到资源文件! 解析歌曲下载地址失败!{musicFileInfo}") 115 | # return False 116 | 117 | # 自动匹配歌曲类型 118 | sourceSelect = "hr" if music['prefix'] == "RS01" else "sq" if music['prefix'] == "F000" else \ 119 | "hq" if music['prefix'] == "M800" else "mp3" 120 | 121 | link = QQApi.getQQMusicDownloadLinkByTrdServer(songmid, sourceSelect) 122 | if link.find('stream.qqmusic.qq.com') == -1: 123 | print(f"无法加载资源文件!解析歌曲下载地址失败!{musicFileInfo},错误细节:" + link) 124 | link = None 125 | return link 126 | 127 | 128 | def downSingle(music, download_home, config): 129 | """ 130 | 多渠道下载 131 | Args: 132 | music: kwid or qqmusicobject 133 | download_home: 134 | config: 135 | 136 | Returns: 137 | 138 | """ 139 | # platform: qq kw wyy mg myfreemp3 140 | platform = config['platform'] 141 | onlyShowSingerSelfSongs = config['onlyMatchSearchKey'] 142 | musicAlbumsClassification = config['classificationMusicFile'] 143 | 144 | header = {} 145 | if platform == 'qq': 146 | musicid = music['musicid'] 147 | file = QQApi.getQQMusicFileName(music['prefix'], music['mid'], music['extra']) 148 | musicFileInfo = f"{music['singer']} - {music['title']} [{music['notice']}] {music['size']} - {file}" 149 | link = handleQQ(music, musicFileInfo) 150 | elif platform == 'kw': 151 | link = handleKuwo(music['mid'], '1000kape') # music['prefix'] + 'k' + music['extra'] 152 | musicFileInfo = f"{music['singer']} - {music['title']} [{music['notice']}]" 153 | elif platform == 'mg': 154 | miguMusicInfo = handleMigu(music['mid'], music['prefix']) 155 | link = miguMusicInfo['url'] # music['prefix'] + 'k' + music['extra'] 156 | musicFileInfo = f"{music['singer']} - {music['title']} [{music['notice']}]" 157 | elif platform == 'wyy': 158 | link: str = handleWyy(music['mid']) 159 | if link is not None: 160 | music['extra'] = 'flac' if link.find(".flac?") != -1 else 'mp3' 161 | music['singer'] = music['author_simple'] 162 | music["album"] = music['album'] 163 | musicFileInfo = f"{music['author_simple']} - {music['title']}" 164 | elif platform == 'myfreemp3': 165 | link = music['prefix'] 166 | musicFileInfo = f"{music['singer']} - {music['title']} [{music['notice']}]" 167 | header = { 168 | "accept": "application/json, text/plain, */*", 169 | "content-type": "application/json", 170 | "origin": "https://tools.liumingye.cn", 171 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.50" 172 | } 173 | else: 174 | link = None 175 | musicFileInfo = '' 176 | 177 | # 测试歌词下载保存接口代码 178 | # lyric = getQQMusicMediaLyric(songmid) # 早期方法 已弃用 179 | # lyric = getQQMusicLyricByMacApp(musicid) 180 | # lyric = getQQMusicLyricByWeb(musicid) 181 | # lyrics = base64.b64decode(lyric['lyric']) 182 | # with open("lyric.txt", 'wb') as code: 183 | # code.write(lyrics) 184 | # code.flush() 185 | # 测试歌词下载代码结束 186 | 187 | if link is None: 188 | return { 189 | 'msg': f"无法加载资源文件!解析歌曲下载地址失败!", 190 | 'code': "-1" 191 | } 192 | 193 | # prepare 194 | localFile = fixWindowsFileName2Normal(f"{music['singer']} - {music['title']}.{music['extra']}") 195 | localLrcFile = fixWindowsFileName2Normal(f"{music['singer']} - {music['title']}.lrc") 196 | mShower = localFile 197 | my_path = download_home + fixWindowsFileName2Normal(music['singer']) + '/' 198 | 199 | threadLock.acquire() # 多线程上锁解决同时创建一个mkdir的错误 200 | if musicAlbumsClassification: 201 | if not os.path.exists(my_path): 202 | os.mkdir(f"{my_path}") 203 | 204 | my_path = f"{my_path}{fixWindowsFileName2Normal(music['album']) if musicAlbumsClassification else ''}" 205 | 206 | try: 207 | if not os.path.exists(my_path): 208 | os.mkdir(f"{my_path}") 209 | except: 210 | pass 211 | threadLock.release() 212 | localFile = os.path.join(my_path, f"{localFile}") 213 | localLrcFile = os.path.join(my_path, f"{localLrcFile}") 214 | 215 | # 下载歌词 216 | if not os.path.exists(localLrcFile) and platform == 'qq': # 只下载qq来源 217 | print(f"本地歌词文件不存在,准备自动下载: [{localLrcFile}].") 218 | # lyric = getQQMusicMediaLyric(songmid) # lyric trans 219 | lyric = QQApi.getQQMusicLyricByMacApp(musicid) 220 | if lyric['lyric'] != '': 221 | # "retcode": 0, 222 | # "code": 0, 223 | # "subcode": 0, 224 | # {'retcode': -1901, 'code': -1901, 'subcode': -1901} 225 | # 外语歌曲有翻译 但是👴不需要! 226 | lyric = base64.b64decode(lyric['lyric']) 227 | try: 228 | with open(localLrcFile, 'wb+') as code: 229 | code.write(lyric) 230 | code.flush() 231 | except: 232 | print("歌词获取出错了!") 233 | else: 234 | print(f"歌词获取失败!服务器上搜索不到此首 [{music['singer']} - {music['title']}] 歌曲歌词!") 235 | 236 | # 下载歌曲 237 | if os.path.exists(localFile): 238 | if platform != 'qq': 239 | print(f"本地已下载,跳过下载 [{music['album']} / {mShower}].") 240 | return { 241 | 'code': 200, 242 | 'msg': "本地已下载,跳过下载" 243 | } 244 | sz = os.path.getsize(localFile) 245 | sz = f"%.2fMB" % (sz / 1024 / 1024) 246 | if sz == music['size']: 247 | print(f"本地已下载,跳过下载 [{music['album']} / {mShower}].") 248 | return { 249 | 'code': 200, 250 | 'msg': "本地已下载,跳过下载" 251 | } 252 | else: 253 | print( 254 | f"本地文件尺寸不符: {os.path.getsize(localFile)}/{music['size']},开始覆盖下载 [{mShower}].") 255 | print(f'正在下载 | {music["album"]} / {musicFileInfo}') 256 | f = requests.get(link, headers=header) 257 | with open(localFile, 'wb') as code: 258 | code.write(f.content) 259 | code.flush() 260 | return { 261 | 'code': 200, 262 | 'msg': "下载完成" 263 | } 264 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 大纲 2 | 3 | 4 | 5 | * [大纲](#大纲) 6 | * [项目介绍](#项目介绍) 7 | * [已知问题](#已知问题) 8 | * [关于网易云登录功能的声明:](#关于网易云登录功能的声明) 9 | * [前后端源码文件夹](#前后端源码文件夹) 10 | * [特别功能](#特别功能) 11 | * [使用方法](#使用方法) 12 | * [0. 代码更新频繁,切记及时更新](#0-代码更新频繁切记及时更新) 13 | * [1. 安装环境](#1-安装环境) 14 | * [2. 进入软件包目录下启动软件](#2-进入软件包目录下启动软件) 15 | * [高级搜索方法](#高级搜索方法) 16 | * [新增网易云盘歌曲信息纠错](#新增网易云盘歌曲信息纠错) 17 | * [确认匹配](#确认匹配) 18 | * [取消匹配](#取消匹配) 19 | * [Docker镜像部署](#docker镜像部署) 20 | * [自定义端口设置](#自定义端口设置) 21 | * [免责声明](#免责声明) 22 | * [致谢](#致谢) 23 | * [其他资料](#其他资料) 24 | * [名词解释](#名词解释) 25 | 26 | 27 | 28 | # 项目介绍 29 | 30 | Create & Design By QiuChenly. 31 | 32 | 这是一个批量下载 QQ 音乐/酷我音乐/网易云会员无损音质歌曲的脚本,技术含量并不是很大,仅供参考。 33 | 34 | 参考是让你参考代码,不是让你想着法儿去白嫖。 35 | 今天你不愿意掏198的数字专辑费用,明天你就会失去赚到198万的机会。 36 | 37 | 眼界决定了人生未来,格局决定了人生上限。哥们主打的就是一个三观。 38 | 39 | 鲁迅曾经说过:舍小我成大我,我不入地狱谁入地狱? 40 | 41 | 所以白嫖的事情我来,付钱你来。 42 | 43 | ```any 44 | 前端技术: 45 | Vue3+TS+Pinia+ElementUI Plus 46 | 47 | 后端技术: 48 | Python3.11.2 + Flask + Concurrency协程 49 | 50 | 开发环境: 51 | BenQ Laptop Core Duo 双核 1.2Ghz + 2GB DDR3 52 | 53 | OS : Windows 7 x64 54 | Env: Python 3.11.2 55 | IDE: Windows Notepad 56 | ``` 57 | 58 | 教程只能解决使用方法上的问题,解决不了dinner的问题。 59 | 60 | # 已知问题 61 | 62 | 1. Windows用户可能体验不是很好,因为很多适配坑。 63 | 2. QQ音乐搜不到,是因为搜索频率过快。别问我为什么才搜两次就频繁了,你问qq。 64 | 3. 下载歌曲后前端页面没有提示,看命令行窗口就知道有没有下载成功了。 65 | 4. 昨天碰到有人因为MIME严格模式导致无法正常加载js文件,最终页面打开空白的bug,这里声明一下:垃圾Windows是这样的。解决办法如下: 66 | 报错信息类似于: 67 | ![image](https://user-images.githubusercontent.com/24793281/235321487-6593d996-a616-4236-ae1f-4fa10211671e.png) 68 | 69 | 给后面用Windows碰到一样报错问题的同学解决方法: 70 | flak框架下运行仍然报错,这是因为加载xxx.js文件默认为text/plain格式,不能正常解析,解决方法如下图所示,修改注册表即可,将图中Content 71 | Type由原来的text/plain改为 application/javascript,然后重新打开项目即可: 72 | ![image](https://user-images.githubusercontent.com/24793281/235321358-2888adb3-d571-48e0-88fc-a2836211232d.png) 73 | 74 | 另外再次赞美windows的天才设计 75 | 76 | ## 关于网易云登录功能的声明: 77 | 78 | - 第三方网易云API服务器来自于互联网,开发者不承认且不担保其具有官方服务器性质,并对该服务器可能会泄露用户信息的可能性保持怀疑。 79 | - 此服务器不可信,请使用完后修改网易云账号密码避免账号失窃。 80 | - 本项目不会保存任何个人隐私信息,但是不对使用的第三方服务器安全性/隐私性做保证。 81 | - 使用者因使用本服务导致的数据泄露账号失窃等问题由使用者独自承担后果,开发者概不负责。 82 | - 使用本服务即表示你同意以上条款,对于使用者在明知或不知以上条款情况下使用本项目所造成的任何数据/泄露隐私泄漏的后果由使用者自行承担,开发者不对任何数据安全性作保证。 83 | - 本项目为完全公益性项目,谢绝任何形式打赏与钱财赠与,以前不会接受以后也不会接受。 84 | - 使用本项目所造成的一切数据泄露,账号失窃,财产损失等严重后果有用户自行承担风险,开发者已经对用户可能所受到的攻击和安全问题做到了尽可能的提示与告知。 85 | - 如果本项目侵犯了你的合法权益,请电子邮件到qiuchenly@outlook.com,我将第一时间删库跑路。 86 | - 总结: 号没了跟我无关 87 | 88 | # 前后端源码文件夹 89 | 90 | 前端界面源代码: 91 | [WebSourceCode](WebSourceCode) 92 | 93 | 后端Flask主代码: 94 | [flaskSystem](flaskSystem) 95 | 96 | 提交代码时源码顶部的版权注释尽量不要修改,这是我IDE自动生成的,每次都会自动更新。 97 | 如果你需要保留你的修改记录,可以将你的个人信息附加到我的版权注释信息之下(注意有一行空格): 98 | ![img.png](img.png) 99 | 100 | 或者通过Google风格的注释附加在函数注释上: 101 | 102 | ![img_4.png](img_4.png) 103 | 104 | 或者通过IDE配置自动更新你的版权信息: 105 | 106 | ![img_5.png](img_5.png) 107 | 108 | ## 特别功能 109 | 110 | | 功能 | 状态 | 附加说明 | 111 | |--------------------|-----|--------------------------------------| 112 | | 网易云会员歌曲搜索&歌单下载 | 未修复 | 版权问题灰色歌曲没有CDN资源缓存 无法下载 | 113 | | 酷我音乐无损音质下载 | 已修复 | 支持Flac和320KbpsMP3下载 根据网友梨花喵的加密算法获取解析 | 114 | | 咪咕无损下载 | 未修复 | 可以下载Flac/320KbpsMP3歌曲 | 115 | | QQ音乐无损会员/高解析度无损下载 | 已完成 | 已经修复官方无损解析接口 | 116 | | FreeMyMP3/高解析度无损下载 | 已修复 | 狠狠的加密 v2算法已破解 | 117 | 118 | 基于web的友好界面出来啦 119 | 120 | ![img_2.png](md/media/img_2.png) 121 | ![img_2.png](img_2.png) 122 | ![img_3.png](img_3.png) 123 | --- 124 | 125 | # 使用方法 126 | 127 | ### 0. 代码更新频繁,切记及时更新 128 | 129 | 有问题先下载最新的代码再看看是不是已经被解决了,而不是拿着几天前的代码问我为什么启动不了。 130 | 你怎么不问问你自己一年前追的女神怎么今天跟别人酒店里泡芙都流出来了,为什么你还没追到呢? 131 | 132 | 楼主一般情况下脾气是很温和的,但是架不住小可爱太多了。提的也是可爱dinner问题,令人忍俊不禁莞尔一笑。 133 | 134 | 只有你学校的老师才会耐心解决你的问题,我只对我的学生负责,我不对小可爱负责。 135 | 136 | ### 1. 安装环境 137 | 138 | 首先安装最新的 python3.11.2 到你的操作系统里。 139 | 140 | Windows用户强制安装此版本: [点击下载 -> Python3.11.2](https://www.python.org/ftp/python/3.11.2/python-3.11.2-amd64.exe) 141 | 142 | ```any 143 | Windows用户须知: 不是Windows用户不用看这一段小作文 144 | 145 | 首先我在此明确表示我的观点: Windows就是全宇宙最优秀的操作系统。 146 | 147 | 1.卸载电脑上所有的python版本,哪怕是3.10都不行. 148 | 如果你会玩python,那么你可以不用看这个提示. 149 | 如果你就是个纯纯的新手,我的建议是认真看一下. 150 | 151 | 2.Windows应用商店的残疾Python版本不要安装. 152 | 装完出问题然后还要明知故问,我会为你送上优美的赞歌. 153 | 计算机技术基础不应该由我给你补. 154 | 155 | 3.不要把项目放在文件夹路径有空格的目录下 156 | 比如你非要把下载好的代码文件放到 157 | D:/Program Files(x86)/Test 123/AB CD/ 158 | 这种存在空格的路径下面,运行报错再来问我那你确实注定要被我赞美称颂. 159 | 160 | 4.题外话 161 | 你要实在不会用,就真别用了吧. 162 | 但是你用不了就别到处发"这东西根本没吊用"之类的言论 163 | 你dinner不代表别人dinner。 164 | 球球了,折磨你自己可以,别折磨我. 165 | 166 | windows用户如果有任何体验上的问题,请提交代码Merge。不要一句“不行没法用”来反馈。哥们看到这种可爱issues直接给你关了。 167 | 让我们共建和谐互联网大家庭。 168 | 最后,Microsoft Windows我测你的码。 169 | ``` 170 | 171 | macOS用户: 系统自带的python3.9版本就够了 不需要另外安装 172 | 173 | 以下所有操作**皆默认假设CMD/Powershell/Bash/zsh的当前目录**在: 174 | 175 | ``` 176 | (Windows) 177 | D:/Downloads/QQFlacMusicDownloader-master/ 178 | 在文件夹空白处按住shift+鼠标右击,点击"在此处打开PowerShell"。 179 | 如果你很懂,cd命令进入上面的目录也行。 180 | 181 | (macOS/Unix/Linux) 182 | ~/Download/QQFlacMusicDownloader-master/ 183 | 终端cd进入上面的目录. 184 | ``` 185 | 186 | 安装依赖包如果出现 404 错误或者太慢,可以用下面的代码切换到清华大学服务器安装。 187 | 188 | ```bash 189 | # 设置python的依赖安装镜像服务器为清华大学服务器 190 | pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple 191 | ``` 192 | 193 | ```bash 194 | # 安装软件依赖必须包 195 | pip3 install -r requirements.txt 196 | ``` 197 | 198 | ### 2. 进入软件包目录下启动软件 199 | 200 | 终端/控制台 进入到本文件所在的目录 执行以下指令: 201 | 202 | ```bash 203 | python3 MainServer.py 204 | ``` 205 | 206 | 由于Windows的可爱设计,在Windows上可能是 207 | 208 | ```commandline 209 | python MainServer.py 210 | ``` 211 | 212 | 如果你是windows10/11,那么系统很有可能会让你安装Windows应用商店的Python残疾版本,如果你不知道该不该安装的话,看看上面Windows用户须知。 213 | 214 | 启动后应该能看到这些信息,红色警告不是错误,无知可以但不要怀疑哥们的实力。看到类似于下面的输出提示即表示你启动成功。没看到类似下面的信息说明没成功。 215 | 216 | 没成功你也别急,截图+操作环境+操作步骤详细给出一份报告,而不是用几个字"本地环境实测无法启动"来概括。 217 | 恕我直言,你不是文曲星,没那本事几个字概括核心要点就不要概括,我理都不想理你。 218 | ![img_1.png](img_1.png) 219 | 220 | 成功后google chrome/Microsoft Edge/Safari之类的浏览器打开[http://127.0.0.1:8899](http://127.0.0.1:8899)即可打开新世界 221 | 222 | ### 高级搜索方法 223 | 224 | 目前 **仅支持** QQ音乐搜索。 225 | 226 | | 指令 | 例子 | id从哪找 | 作用 | 227 | |--------------------|---------------------------|---------------------------|---------------------------------------------------------------------------------| 228 | | (album) b:+专辑ID | ![img_7.png](img_7.png) | ![img_6.png](img_6.png) | 获取一张专辑里的歌曲 | 229 | | (playlist) p:+歌单ID | ![img_9.png](img_9.png) | ![img_8.png](img_8.png) | 获取一张歌单里的所有歌曲,需要注意的是列表下面的分页是无效的 不要切换页面 因为他一次性是加载的整个歌单列表 | 230 | | (id) id:+歌曲ID | ![img_11.png](img_11.png) | ![img_10.png](img_10.png) | 有时候有些二逼非主流歌曲名称是及其之难以用输入法扣出来的,这个时候可以用id直接获取到这首歌。不想用那些二逼非主流歌曲做演示,用我最爱的杰伦演示给你们看看得了 | 231 | | (toplist) t:+榜单ID | ![img_17.png](img_17.png) | ![img_16.png](img_16.png) | 获取一张榜单里的歌曲 | 232 | 233 | 2023.4.14 234 | 235 | - 修复歌曲关键词过滤不可定制化搜索的问题,满足搜索部分土嗨神曲的需求。现在可以在搜索界面关闭关键词过滤或者在首页自定义过滤关键词。 236 | 237 | ### 新增网易云盘歌曲信息纠错 238 | 239 | 可以搜索歌曲或指定歌曲ID来关联你上传到网易云盘里的歌曲。 240 | 在使用前需要打开网易云登录界面一次更新Cookie才可以访问到你的云盘数据。 241 | 加载时由于一次加载1000条数据,所以加载缓慢,请等几秒钟。 242 | 243 | #### 确认匹配 244 | 245 | 通过你设置的ID或者搜索到的歌曲来绑定这首歌的实际歌曲数据。 246 | 如果你想取消某首歌的匹配可以点击取消匹配。 247 | 248 | #### 取消匹配 249 | 250 | 取消网易云自动/手动匹配的错误歌曲信息。有时候我们上传的歌曲信息网易云会识别错误,所以我们可以取消匹配或者手动更新。 251 | 252 | ![img_12.png](img_12.png) 253 | 254 | ![img_13.png](img_13.png) 255 | 256 | 歌曲ID如何获得? 257 | 258 | ![img_14.png](img_14.png) 259 | 260 | ![img_15.png](img_15.png) 261 | 262 | ### Docker镜像部署 263 | 264 | 可以通过自己自己打包 Docker 进行部署,也可以使用本项目打包好的容器进行部署 265 | 266 | 自己打包 Docker 进行部署执行以下命令: 267 | 268 | ```bash 269 | docker build -t dockerimage . 270 | ``` 271 | 272 | 使用本项目打包好的容器进行部署: 273 | 274 | ```bash 275 | docker pull registry.cn-hangzhou.aliyuncs.com/music_downloader/qq_flac_music_downloader 276 | ``` 277 | 278 | Docker 镜像部署需要进行端口映射,可以采用以下命令进行端口映射: 279 | 280 | (注意:用你的本地使用目录替换下方“本地目录” 如 E:\music) 281 | 282 | ```bash 283 | docker run -p 127.0.0.1:8899:8899 -v 本地目录:/workspace/music -it dockerimage:latest 284 | ``` 285 | 286 | 更新方式:先运行 ```docker ps -a ```查看容器名称 287 | 288 | 然后 289 | 290 | ``` 291 | docker stop 容器名称 292 | docker rm 容器名称 293 | ``` 294 | 295 | 最后,上面```docker pull```和```docker run```的代码重新执行一遍 296 | 297 | docker-compose部署方式 298 | 299 | 本地新建txt,重命名为docker-compose.yml (不会修改后缀请百度) 300 | 301 | 复制以下内容,同样注意替换“本地目录” 302 | 303 | 或者你直接下载项目中的docker-compose.yml,然后自行修改本地目录 304 | 305 | ``` 306 | version: "3" 307 | services: 308 | downloader: 309 | image: registry.cn-hangzhou.aliyuncs.com/music_downloader/qq_flac_music_downloader 310 | container_name: music 311 | network_mode: bridge 312 | volumes: 313 | - 本地目录:/workspace/music 314 | ports: 315 | - "127.0.0.1:8899:8899" 316 | restart: always 317 | ``` 318 | 319 | 然后 打开cmd命令行,cd到docker-compose.yml所在目录 ```docker-compose up -d``` 320 | 321 | 需要更新的时候,也是cd到docker-compose.yml所在目录 322 | 323 | ``` 324 | docker-compose pull 325 | docker-compose up -d 326 | ``` 327 | 328 | 相对docker,更新比较简单,所以个人比较推荐使用docker-compose的方式 329 | 330 | ### 自定义端口设置 331 | 332 | 部分设备会存在 8889 端口被占用的情况,部署时可自定义端口,终端/控制台 进入到本文件所在的目录 执行以下指令: 333 | 334 | ```bash 335 | # []内为可选参数 336 | python3 MainServer.py [--port 8999] 337 | ``` 338 | 339 | docker 部署可自行切换映射本地端口,以解决端口被占用情况。 340 | 341 | # 免责声明 342 | 343 | 禁止任何形式的商业用途,包括但不仅限于售卖/打赏/获利,不得使用本代码进行任何形式的牟利/贩卖/传播,再次强调仅供个人私下研究学习技术使用,有条件者请支持正版音乐! 344 | 律师函请发给提供这些音乐资源解析服务的网站运营方,本项目仅以纯粹的技术目的去学习研究,如有侵犯到任何人的合法权利,请致信qiuchenly@outlook.com,我将在第一时间删库跑路 345 | 346 | 本项目基于 GPL V3.0 许可证发行,以下协议是对于 GPL V3.0 的补充,如有冲突,以以下协议为准。 347 | 348 | 词语约定:本协议中的“本项目”指QQFlacMusicDownloader项目;“使用者”指签署本协议的使用者;“官方音乐平台”指对本项目内置的包括酷我、网易云、QQ音乐、咪咕等音乐源的官方平台统称;“版权数据”指包括但不限于图像、音频、名字等在内的他人拥有所属版权的数据。 349 | 350 | 本项目的数据来源原理是从各官方音乐平台的公开服务器中拉取数据,经过对数据简单地筛选与合并后进行展示,因此本项目不对数据的准确性负责。 351 | 使用本项目的过程中可能会产生版权数据,对于这些版权数据,本项目不拥有它们的所有权,为了避免造成侵权,使用者务必在24小时内清除使用本项目的过程中所产生的版权数据。 352 | 本项目内的官方音乐平台别名为本项目内对官方音乐平台的一个称呼,不包含恶意,如果官方音乐平台觉得不妥,可联系本项目更改或移除。 353 | 本项目内使用的部分包括但不限于字体、图片等资源来源于互联网,如果出现侵权可联系本项目移除。 354 | 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。 355 | 本项目完全免费,仅供个人私下小范围研究交流学习 python 356 | 技术使用, 且开源发布于 GitHub 357 | 面向全世界人用作对技术的学习交流,本项目不对项目内的技术可能存在违反当地法律法规的行为作保证,禁止在违反当地法律法规的情况下使用本项目,对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。 358 | 若你使用了本项目,将代表你接受以上协议。 359 | 360 | 音乐平台不易,请尊重版权,支持正版。 361 | 362 | # 致谢 363 | 364 | 感谢大家的支持。以下月底加急名单排名不分先后: 365 | 366 | | 贡献人员 | ? | 367 | |---------------|--------------------| 368 | | QiuChenly | Maintance | 369 | | QingXuDw | Technology Support | 370 | | helloplhm-qwq | Technology Support | 371 | 372 | # 其他资料 373 | 374 | [早期接口 QMD Apk的逆向过程](./md/README.md) 375 | 376 | # 名词解释 377 | 378 | 1. dinner: 低能 379 | 2. 我测你m: 一眼顶真 鉴定为纯纯的范剑 380 | -------------------------------------------------------------------------------- /flaskSystem/src/Api/Netease.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - Netease.py 5 | # @修改时间 : 2023-03-20 12:44:59 6 | # @上次修改 : 2023/3/20 上午12:44 7 | import json 8 | import os 9 | import time 10 | 11 | from flaskSystem.src.Api.BaseApi import BaseApi 12 | from flaskSystem.src.Common import Http 13 | from flaskSystem.src.Types.Types import Songs 14 | 15 | 16 | class Netease(BaseApi): 17 | __baseUrl = 'http://cloud-music.pl-fe.cn' 18 | 19 | def __init__(self): 20 | self.__httpServer = Http.HttpRequest() 21 | 22 | def http(self, url, method=0, data={}): 23 | return self.httpwj(self.__baseUrl + url, method, data) 24 | 25 | def httpwj(self, url, method=0, data={}): 26 | return self.__httpServer.getHttp2Json(url, method, data) 27 | 28 | def httpw(self, url, method=0, data=b'', head={}): 29 | return self.__httpServer.getHttp(url, method, data, header=head) 30 | 31 | def search(self, searchKey: str) -> list[Songs]: 32 | pass 33 | 34 | def cookie(self): 35 | dt = self.__httpServer.getSession().cookies.get_dict() 36 | return dt 37 | 38 | def set_cookie(self, ck: dict): 39 | self.__httpServer.setCookie(ck) 40 | 41 | def checkQrState(self, unikey: str): 42 | u = '/login/qr/check?key=' + unikey + f"&time={time.time_ns()}" 43 | log = self.http(u).json() 44 | log['cookie'] = self.cookie() 45 | print(u, log) 46 | return log 47 | 48 | def uploadMusic2NetEaseCloud(self, fileLocate: str): 49 | """ 50 | 上传歌曲到网易云云盘,此函数还未经过代码测试 51 | Args: 52 | fileLocate: 53 | 54 | Returns: 55 | 56 | """ 57 | # TODO 需要测试此处代码 58 | u = f'/cloud?time={time.time_ns()}' 59 | name = os.path.basename(fileLocate) 60 | with open(fileLocate, "rb") as conf: 61 | upFile = { 62 | 'songFile': (name, conf) 63 | } 64 | res = self.__httpServer.getSession().post(u, files=upFile, headers={ 65 | 'Content-Type': 'multipart/form-data' 66 | }) 67 | res = res.json() 68 | print(res) 69 | 70 | def matchMusicSid2ASid(self, data: dict): 71 | u = f'/cloud/match?uid={data["uid"]}&sid={data["sid"]}&asid={data["asid"]}&time={time.time_ns()}' 72 | res = self.http(u).json() 73 | print(res) 74 | # {'code': 400, 'message': '纠错后的文件已在云盘存在', 'data': False} 75 | return res 76 | 77 | def getAllMusicCloud(self, size=30): 78 | u = f'/user/cloud?limit={size}×tamp={time.time_ns()}' 79 | r = self.http(u) 80 | res = r.json() 81 | # fee: enum, 82 | # 0: 免费或无版权 83 | # 1: VIP 歌曲 84 | # 4: 购买专辑 85 | # 8: 非会员可免费播放低音质,会员可播放高音质及下载 86 | # fee 为 1 或 8 的歌曲均可单独购买 2 元单曲 87 | # t: enum, 88 | # 0: 一般类型 89 | # 1: 通过云盘上传的音乐,网易云不存在公开对应 90 | # 如果没有权限将不可用,除了歌曲长度以外大部分信息都为null。 91 | # 网页端打开会看到404画面。 92 | # 2: 通过云盘上传的音乐,网易云存在公开对应 93 | # 如果没有权限则只能看到信息,但无法直接获取到文件。 94 | if res['code'] == 200: 95 | return { 96 | 'list': res['data'], 97 | 'count': res['count'], 98 | 'hasMore': res['hasMore'] 99 | } 100 | return { 101 | 'list': [], 102 | 'count': 0, 103 | 'hasMore': False 104 | } 105 | 106 | def logoutUser(self): 107 | u = '/logout' 108 | return self.http(u).json() 109 | 110 | def getUserDetail(self): 111 | u = '/user/account' 112 | return self.http(u).json() 113 | 114 | def getUserLikeList(self, uid: str): 115 | """ 116 | 传入用户 id, 可获取已喜欢音乐 id 列表(id 数组) 不是必须 117 | Args: 118 | uid: 119 | 120 | Returns: 121 | 122 | """ 123 | u = f'/likelist?uid={uid}' + f"&time={time.time_ns()}" 124 | res = self.http(u).json() 125 | return res['ids'] 126 | 127 | userPlaylist = [] 128 | 129 | def getUserPlaylist(self, uid: str): 130 | """ 131 | 获取用户的收藏或创建的歌单 132 | Args: 133 | uid: 134 | 135 | Returns: 136 | 137 | """ 138 | u = f'/user/playlist?uid={uid}' + f"&time={time.time_ns()}" 139 | res = self.http(u).json() 140 | userPlaylist = [ 141 | { 142 | 'userId': l['userId'], 143 | 'trackCount': l['trackCount'], 144 | 'name': l['name'], 145 | 'id': l['id'], 146 | 'coverImgUrl': l['coverImgUrl'] 147 | } for l in res['playlist'] 148 | ] 149 | # print("用户所有歌单") 150 | return userPlaylist 151 | 152 | def getPlayListAllMusic(self, playId, size=1000, offset=0): 153 | """ 154 | 获取歌单里所有音乐 155 | Args: 156 | playId: 157 | size: 158 | offset: 159 | 160 | Returns: 161 | 162 | """ 163 | u = f'/playlist/track/all?id={playId}&limit={size}&offset={offset}' + f"&time={time.time_ns()}" 164 | res = self.http(u) 165 | if res.status_code != 200: 166 | return [] 167 | if res.text.find(":400}") != -1: 168 | return [] 169 | js = res.json() 170 | privileges = js['privileges'] 171 | songs = js['songs'] 172 | code = js['code'] 173 | if code == 20001: 174 | return -1 175 | 176 | lst = [] 177 | inx = 0 178 | for song in songs: 179 | lst.append({ 180 | "title": song['name'], 181 | "mid": song['id'], 182 | 'author_simple': song['ar'][0]['name'], # li['ar'][0]['name'] if len(li['ar']) == 1 else 183 | "author": song['ar'], # 数组[{'id': 472822, 'name': 'JJD', 'tns': [], 'alias': []}] 184 | 'publishTime': song['publishTime'], 185 | 'album': song['al']['name'], 186 | 'fee': song['fee'], 187 | 'copyright': song['copyright'], 188 | # 是否为云盘歌曲 189 | 'cloud': False if song.get('pc') is None else True, 190 | 'extra': 'flac' if song['sq'] is not None or song['hr'] is not None else 'mp3', 191 | 'privileges': privileges[inx] 192 | }) 193 | inx += 1 194 | 195 | return lst 196 | 197 | def anonimousLogin(self): 198 | u = "/register/anonimous" + f"?time={time.time_ns()}" 199 | res = self.http(u) 200 | res = res.json() 201 | return res 202 | 203 | def getUserLevel(self): 204 | u = "/user/level" + f"?time={time.time_ns()}" 205 | res = self.http(u) 206 | res = res.json() 207 | return res 208 | 209 | def qrLogin(self): 210 | u = '/login/qr/key?' + f"time={time.time_ns()}" 211 | res = self.http(u) 212 | uniKey = res.json()['data']['unikey'] 213 | print("uniKey", uniKey) 214 | 215 | u = '/login/qr/create?key=' + uniKey + "&qrimg=1" 216 | res = self.http(u).json()['data'] 217 | b64 = res['qrimg'] 218 | url = res['qrurl'] 219 | return { 220 | 'url': url, 221 | 'b64': b64, 222 | 'uniKey': uniKey 223 | } 224 | # img = base64.b64decode(b64.split(",")[1]) 225 | # with open("./login.png", "wb+") as p: 226 | # p.write(img) 227 | # p.flush() 228 | # 229 | # img = cv2.imread("./login.png") 230 | # cv2.imshow("", img) 231 | # cv2.waitKey(0) 232 | # 233 | # res = self.checkQrState(unikey) 234 | # res = res.json()['code'] 235 | # if res == 803: 236 | # # Login Success 237 | # return True 238 | # print("登录失败。") 239 | # return False 240 | 241 | def save_local(self, reinit=False): 242 | """ 243 | 保存cookie到本地 避免重复登录造成账户异常 244 | Args: 245 | reinit: True则清空本地cookie重置。 246 | 247 | Returns: 248 | 249 | """ 250 | with open("./NetEase.cfg", 'wb+') as p: 251 | p.write(json.dumps({ 252 | 'cookie': '' if reinit else self.cookie() 253 | # 'likes': mySubCount 254 | }).encode()) 255 | p.flush() 256 | 257 | def read_local(self): 258 | """ 259 | 登录成功返回True 否则返回False 260 | Returns: 261 | 262 | """ 263 | if os.path.exists("./NetEase.cfg"): 264 | with open("./NetEase.cfg", 'r') as p: 265 | s = p.read() 266 | dt = json.loads(s) 267 | if dt['cookie'] == '': 268 | return False 269 | self.set_cookie(dt['cookie']) 270 | isLogin = self.getUserDetail()['code'] == 200 271 | return isLogin 272 | return False 273 | 274 | def getMusicUrl(self, id=''): 275 | u = 'http://music.fy6b.com/index/mp3orflac' 276 | d = f'type=netease&id={id}&option=flac' 277 | # { 278 | # "url": "http:\/\/m704.music.126.net\/20230305234939\/ccfe8832df4e3431dbe25dfea1118f1e\/jdymusic\/obj\/wo3DlMOGwrbDjj7DisKw\/22975550396\/2dbf\/d87f\/e6f2\/11dac549d861ba74f1d599d2f4f45cea.flac?authSecret=00000186b25ff97815c80aaba23719fb", 279 | # "size": 3278433, 280 | # "br": 512.575 281 | # } 282 | r = self.httpw(u, 1, d.encode('utf-8'), { 283 | "Content-Type": "application/x-www-form-urlencoded", 284 | "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 8.1.0; MI 5s Build/OPM1.171019.018)" 285 | }) 286 | r = r.json() 287 | return r 288 | 289 | def searchMusicByTrd(self, searchKey="周杰伦", pageNum=1, pageSize=100): 290 | """ 291 | 第三方接口搜索网易云歌曲 292 | Args: 293 | searchKey: 294 | pageNum: 295 | pageSize: 296 | 297 | Returns: 298 | 299 | """ 300 | u = 'http://music.fy6b.com/' 301 | d = f'type=netease&keyword={searchKey}&page={pageNum}&limit={pageSize}' 302 | r = self.httpw(u, 1, d.encode('utf-8'), { 303 | "Content-Type": "application/x-www-form-urlencoded", 304 | "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 13; MI 5s Build/TQ1A.230105.002.A1)" 305 | }) 306 | r = r.json() 307 | lst = [] 308 | for li in r: 309 | it = { 310 | 'prefix': "", 311 | 'extra': "flac", 312 | 'notice': "暂无", 313 | 'mid': li['id'], 314 | 'musicid': li['id'], 315 | 'songmid': li['id'], 316 | 'size': "无", 317 | 'title': li['name'], 318 | 'singer': li['singer'], 319 | 'album': "无专辑", 320 | 'time_publish': "无", 321 | # 'hasLossless': li['hasLossless'], 322 | 'readableText': f"{li['singer']} - {li['name']}" 323 | } 324 | lst.append(it) 325 | return { 326 | 'data': lst, 327 | 'page': { 328 | 'size': 10000, # 这个接口不反悔这个字段 所以只能固定一万了 329 | 'next': pageNum + 1, 330 | 'cur': pageNum, 331 | 'searchKey': searchKey 332 | } 333 | } 334 | 335 | def searchMusic(self, searchKey="周杰伦", pageNum=1, pageSize=100): 336 | """ 337 | 官方接口搜索网易云歌曲 338 | Args: 339 | searchKey: 340 | pageNum: 341 | pageSize: 342 | 343 | Returns: 344 | 345 | """ 346 | u = f'/cloudsearch?keywords={searchKey}&offset={pageNum}&limit={pageSize}' + f"&time={time.time_ns()}" 347 | res = self.http(u) 348 | res = res.json() 349 | 350 | result = res['result'] 351 | lst = [] 352 | for li in result['songs']: 353 | au_l = li['l'] 354 | au_m = li['m'] 355 | au_h = li['h'] 356 | au_sq = li['sq'] 357 | au_hr = li['hr'] 358 | 359 | if au_hr is not None: 360 | size = au_hr['size'] 361 | extra = 'flac' 362 | bitrate = round(int(au_hr['br']) / 1000) 363 | elif au_sq is not None: 364 | size = au_sq['size'] 365 | extra = 'flac' 366 | bitrate = round(int(au_sq['br']) / 1000) 367 | elif au_h is not None: 368 | size = au_h['size'] 369 | extra = 'mp3' 370 | bitrate = round(int(au_h['br']) / 1000) 371 | elif au_m is not None: 372 | size = au_m['size'] 373 | extra = 'mp3' 374 | bitrate = round(int(au_m['br']) / 1000) 375 | else: 376 | size = au_l['size'] 377 | extra = 'aac' 378 | bitrate = round(int(au_l['br']) / 1000) 379 | 380 | it = { 381 | 'prefix': "", 382 | 'extra': extra, 383 | 'notice': "FLAC 无损音质" if extra == 'flac' else f'{extra.upper()} {bitrate}Kbps', 384 | 'mid': li['id'], 385 | 'musicid': li['id'], 386 | 'songmid': li['id'], 387 | 'size': "%.2fMB" % (int(size) / 1024 / 1024), 388 | 'title': li['name'], 389 | 'singer': "/".join([ 390 | it['name'] for it in li['ar'] 391 | ]), 392 | 'author_simple': li['ar'][0]['name'], 393 | 'album': li['al']['name'], 394 | 'time_publish': li['publishTime'], 395 | # 'hasLossless': li['hasLossless'], 396 | # 'readableText': f"{li['singer']} - {li['name']}" 397 | } 398 | lst.append(it) 399 | return { 400 | 'data': lst, 401 | 'page': { 402 | 'size': result['songCount'], # 这个接口不反悔这个字段 所以只能固定一万了 403 | 'next': pageNum + 1, 404 | 'cur': pageNum, 405 | 'searchKey': searchKey 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /WebSourceCode/src/components/SearchMusic.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 245 | 246 | 347 | 348 | 510 | -------------------------------------------------------------------------------- /WebSourceCode/src/components/Netease.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 266 | 267 | 441 | 442 | 580 | -------------------------------------------------------------------------------- /flaskSystem/src/Common/EncryptTools.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023. 秋城落叶, Inc. All Rights Reserved 2 | # @作者 : 秋城落叶(QiuChenly) 3 | # @邮件 : qiuchenly@outlook.com 4 | # @文件 : 项目 [qqmusic] - EncryptTools.py 5 | # @修改时间 : 2023-04-23 03:41:34 6 | # @上次修改 : 2023/4/23 下午3:41 7 | import json 8 | import random 9 | import zlib 10 | from hashlib import md5 11 | from Crypto.Cipher import AES 12 | import base64 13 | import time as tm 14 | 15 | from flaskSystem.src.Common import Http 16 | 17 | 18 | # des解密 19 | 20 | 21 | def decryptDES(strs: str, key: str): return des( 22 | key, CBC, key, padmode=PAD_PKCS5).decrypt(base64.b64decode(str(strs))) 23 | 24 | 25 | # des加密 26 | def encryptDES(text: str, key: str): return str(base64.b64encode( 27 | des(key, CBC, key, padmode=PAD_PKCS5).encrypt(text)), 'utf-8') 28 | 29 | 30 | # 加密字符串 31 | def encryptText(text: str, qq: str): 32 | key = (qq)[0:8] 33 | return encryptDES(text, key) 34 | 35 | 36 | # 解密字符串 37 | def decryptText(text: str, qq: str): return str(decryptDES( 38 | text.replace("-", ""), (qq)[0:8]), 'utf-8') 39 | 40 | 41 | def AESEncrypt(data: str, key: str): 42 | password = key.encode("utf-8") # 秘钥,b就是表示为bytes类型 43 | aes = AES.new(password, AES.MODE_ECB) # 创建一个aes对象 44 | # AES.MODE_ECB 表示模式是ECB模式 45 | text = pad(AES.block_size, data) 46 | text = text.encode("utf-8") # 需要加密的内容,bytes类型 47 | en_text = aes.encrypt(text) # 加密明文 48 | return en_text 49 | 50 | 51 | def pad(length, text): 52 | """ 53 | 填充函数,使被加密数据的字节码长度是block_size的整数倍 54 | """ 55 | return text + (length - len(text) % length) * chr(length - len(text) % length) 56 | 57 | 58 | # def AESDecrypt(data: bytes, key: str): 59 | # password = key.encode("utf-8") # 秘钥,b就是表示为bytes类型 60 | # aes = AES.new(password, AES.MODE_ECB) # 创建一个aes对象 61 | # # AES.MODE_ECB 表示模式是ECB模式 62 | # data = unpad(data) 63 | # en_text = aes.decrypt(data) 64 | # en_text = en_text.decode() 65 | # print("密文:", en_text) # 加密明文,bytes类型 66 | # return en_text 67 | 68 | # 69 | 70 | class KuwoDES(): 71 | """ 72 | 酷我直链解析加密算法\n 73 | 来自网友 彭狸花喵 贡献,在此感谢他的贡献。\n 74 | 75 | update: 秋城落叶\n 76 | author: 彭狸花喵(https://github.com/helloplhm-qwq)\n 77 | 78 | \n原作者repo:\n 79 | 80 | 试着修了一下酷我的下载,结果自己太渣看不懂qwq\n 81 | 就加了个算法和链接,之后的就请秋城落叶大佬做了qwq\n 82 | **不要在加密前数据中加br=xxx,不然是错误提示音频**\n 83 | 不知道能用多久,先用着吧\n 84 | 酷狗音乐的signature算法也可以提供(我是[洛雪音乐助手](https://github.com/lyswhut/lx-music-desktop)那边的贡献者 ~~虽然我是个废物~~)\n 85 | 自行把音乐id改成对应数字\n 86 | 原来的antiserver接口还可以用,但是不能高音质了(只有128k)\n 87 | 请使用KuwoDES.base64_encrypt函数加密然后这么拼url\n 88 | http://mobi.kuwo.cn/mobi.s?f=kuwo&q=加密后数据\n 89 | 90 | 请求User-Agent是`okhttp/3.10.0`\n 91 | 92 | 示例: 93 | 上面的返回:\n 94 | ``` 95 | format=mp3 96 | bitrate=1 97 | url=http://ar.player.ra05.sycdn.kuwo.cn/2b9c3159b8e9d681d9b0db67870a4a14/64435f6e/resource/n2/320/74/46/2352675463.mp3?bitrate$1&format$mp3&source$kwplayer_ar_9.3.1.3_qq.apk&type$convert_url2&user$0 98 | sig=9625213586057656967 99 | rid=260839262 100 | type=1 101 | ```\n 102 | 示例返回(320k)\n 103 | ``` 104 | format=mp3 105 | bitrate=320 106 | url=http://nx01.sycdn.kuwo.cn/a1a1c32ae1bd9fdcbf873f4a0270cfa1/64435bd1/resource/n1/66/9/1107056446.mp3?bitrate$320&format$mp3&type$convert_url2 107 | sig=4754771234559730982 108 | rid=51513854 109 | type=0 110 | ```\n 111 | 加密前数据(320k):\n 112 | ``` 113 | user=0&android_id=0&prod=kwplayer_ar_9.3.1.3&corp=kuwo&newver=3&vipver=9.3.1.3&source=kwplayer_ar_9.3.1.3_qq.apk&p2p=1¬race=0&type=convert_url2&format=flac|mp3|aac&sig=0&rid=音乐id&priority=bitrate&loginUid=0&network=WIFI&loginSid=0&mode=download 114 | ``` 115 | 示例返回(flac)\n 116 | ``` 117 | format=flac 118 | bitrate=1 119 | url=http://other.player.rc03.sycdn.kuwo.cn/4394f83b5051e5a24693fe836613d5c3/64435da2/resource/s2/85/49/1592794249.flac?bitrate$1&format$flac&type$convert_url2 120 | sig=6840999210798105689 121 | rid=260839262 122 | type=1 123 | ``` 124 | 加密前数据(flac):\n 125 | ``` 126 | corp=kuwo&p2p=1&type=convert_url2&format=flac&rid=音乐id 127 | ``` 128 | 示例返回(128k):\n 129 | ``` 130 | format=mp3 131 | bitrate=1 132 | url=http://ew.sycdn.kuwo.cn/d4cdcf1a811814d166d7d126d646ac99/6443611a/resource/n1/38/24/1285019596.mp3?bitrate$1&format$mp3&type$convert_url2 133 | sig=5519117143462714854 134 | rid=260839262 135 | type=1 136 | ``` 137 | 加密前数据(128k):\n 138 | ``` 139 | corp=kuwo&p2p=1&type=convert_url2&format=mp3|aac&rid=音乐id 140 | ``` 141 | """ 142 | DES_MODE_DECRYPT = 1 143 | 144 | arrayE = [ 145 | 31, 0, DES_MODE_DECRYPT, 2, 3, 4, -1, -1, 3, 4, 5, 6, 7, 8, -1, -1, 7, 8, 9, 10, 11, 12, -1, -1, 11, 12, 13, 14, 146 | 15, 16, -1, -1, 15, 16, 17, 18, 19, 20, -1, -1, 19, 20, 21, 22, 23, 24, -1, -1, 23, 24, 25, 26, 27, 28, -1, -1, 147 | 27, 28, 29, 30, 31, 30, -1, -1 148 | ] 149 | 150 | arrayIP = [ 151 | 57, 49, 41, 33, 25, 17, 9, DES_MODE_DECRYPT, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 152 | 55, 47, 39, 31, 23, 15, 7, 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 153 | 12, 4, 62, 54, 46, 38, 30, 22, 14, 6 154 | ] 155 | 156 | arrayIP_1 = [ 157 | 39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 158 | 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, DES_MODE_DECRYPT, 41, 9, 49, 17, 159 | 57, 25, 32, 0, 40, 8, 48, 16, 56, 24 160 | ] 161 | arrayLs = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1] 162 | arrayLsMask = [0, 0x100001, 0x300003] 163 | arrayMask = [2 ** i for i in range(64)] 164 | arrayMask[-1] *= -1 165 | arrayP = [ 166 | 15, 6, 19, 20, 28, 11, 27, 16, 167 | 0, 14, 22, 25, 4, 17, 30, 9, 168 | 1, 7, 23, 13, 31, 26, 2, 8, 169 | 18, 12, 29, 5, 21, 10, 3, 24, 170 | ] 171 | arrayPC_1 = [ 172 | 56, 48, 40, 32, 24, 16, 8, 0, 173 | 57, 49, 41, 33, 25, 17, 9, 1, 174 | 58, 50, 42, 34, 26, 18, 10, 2, 175 | 59, 51, 43, 35, 62, 54, 46, 38, 176 | 30, 22, 14, 6, 61, 53, 45, 37, 177 | 29, 21, 13, 5, 60, 52, 44, 36, 178 | 28, 20, 12, 4, 27, 19, 11, 3, 179 | ] 180 | arrayPC_2 = [ 181 | 13, 16, 10, 23, 0, 4, -1, -1, 182 | 2, 27, 14, 5, 20, 9, -1, -1, 183 | 22, 18, 11, 3, 25, 7, -1, -1, 184 | 15, 6, 26, 19, 12, 1, -1, -1, 185 | 40, 51, 30, 36, 46, 54, -1, -1, 186 | 29, 39, 50, 44, 32, 47, -1, -1, 187 | 43, 48, 38, 55, 33, 52, -1, -1, 188 | 45, 41, 49, 35, 28, 31, -1, -1, 189 | ] 190 | matrixNSBox = [[ 191 | 14, 4, 3, 15, 2, 13, 5, 3, 192 | 13, 14, 6, 9, 11, 2, 0, 5, 193 | 4, 1, 10, 12, 15, 6, 9, 10, 194 | 1, 8, 12, 7, 8, 11, 7, 0, 195 | 0, 15, 10, 5, 14, 4, 9, 10, 196 | 7, 8, 12, 3, 13, 1, 3, 6, 197 | 15, 12, 6, 11, 2, 9, 5, 0, 198 | 4, 2, 11, 14, 1, 7, 8, 13, ], [ 199 | 15, 0, 9, 5, 6, 10, 12, 9, 200 | 8, 7, 2, 12, 3, 13, 5, 2, 201 | 1, 14, 7, 8, 11, 4, 0, 3, 202 | 14, 11, 13, 6, 4, 1, 10, 15, 203 | 3, 13, 12, 11, 15, 3, 6, 0, 204 | 4, 10, 1, 7, 8, 4, 11, 14, 205 | 13, 8, 0, 6, 2, 15, 9, 5, 206 | 7, 1, 10, 12, 14, 2, 5, 9, ], [ 207 | 10, 13, 1, 11, 6, 8, 11, 5, 208 | 9, 4, 12, 2, 15, 3, 2, 14, 209 | 0, 6, 13, 1, 3, 15, 4, 10, 210 | 14, 9, 7, 12, 5, 0, 8, 7, 211 | 13, 1, 2, 4, 3, 6, 12, 11, 212 | 0, 13, 5, 14, 6, 8, 15, 2, 213 | 7, 10, 8, 15, 4, 9, 11, 5, 214 | 9, 0, 14, 3, 10, 7, 1, 12, ], [ 215 | 7, 10, 1, 15, 0, 12, 11, 5, 216 | 14, 9, 8, 3, 9, 7, 4, 8, 217 | 13, 6, 2, 1, 6, 11, 12, 2, 218 | 3, 0, 5, 14, 10, 13, 15, 4, 219 | 13, 3, 4, 9, 6, 10, 1, 12, 220 | 11, 0, 2, 5, 0, 13, 14, 2, 221 | 8, 15, 7, 4, 15, 1, 10, 7, 222 | 5, 6, 12, 11, 3, 8, 9, 14, ], [ 223 | 2, 4, 8, 15, 7, 10, 13, 6, 224 | 4, 1, 3, 12, 11, 7, 14, 0, 225 | 12, 2, 5, 9, 10, 13, 0, 3, 226 | 1, 11, 15, 5, 6, 8, 9, 14, 227 | 14, 11, 5, 6, 4, 1, 3, 10, 228 | 2, 12, 15, 0, 13, 2, 8, 5, 229 | 11, 8, 0, 15, 7, 14, 9, 4, 230 | 12, 7, 10, 9, 1, 13, 6, 3, ], [ 231 | 12, 9, 0, 7, 9, 2, 14, 1, 232 | 10, 15, 3, 4, 6, 12, 5, 11, 233 | 1, 14, 13, 0, 2, 8, 7, 13, 234 | 15, 5, 4, 10, 8, 3, 11, 6, 235 | 10, 4, 6, 11, 7, 9, 0, 6, 236 | 4, 2, 13, 1, 9, 15, 3, 8, 237 | 15, 3, 1, 14, 12, 5, 11, 0, 238 | 2, 12, 14, 7, 5, 10, 8, 13, ], [ 239 | 4, 1, 3, 10, 15, 12, 5, 0, 240 | 2, 11, 9, 6, 8, 7, 6, 9, 241 | 11, 4, 12, 15, 0, 3, 10, 5, 242 | 14, 13, 7, 8, 13, 14, 1, 2, 243 | 13, 6, 14, 9, 4, 1, 2, 14, 244 | 11, 13, 5, 0, 1, 10, 8, 3, 245 | 0, 11, 3, 5, 9, 4, 15, 2, 246 | 7, 8, 12, 15, 10, 7, 6, 12, ], [ 247 | 13, 7, 10, 0, 6, 9, 5, 15, 248 | 8, 4, 3, 10, 11, 14, 12, 5, 249 | 2, 11, 9, 6, 15, 12, 0, 3, 250 | 4, 1, 14, 13, 1, 2, 7, 8, 251 | 1, 2, 12, 15, 10, 4, 0, 3, 252 | 13, 14, 6, 9, 7, 8, 9, 6, 253 | 15, 1, 5, 12, 3, 10, 14, 5, 254 | 8, 7, 11, 0, 4, 13, 2, 11, ], 255 | ] 256 | 257 | SECRET_KEY = b'ylzsxkwm' 258 | 259 | def bit_transform(self, arr_int, n, l): 260 | l2 = 0 261 | for i in range(n): 262 | if arr_int[i] < 0 or (l & self.arrayMask[arr_int[i]] == 0): 263 | continue 264 | l2 |= self.arrayMask[i] 265 | return l2 266 | 267 | def DES64(self, longs, l): 268 | out = 0 269 | SOut = 0 270 | pR = [0] * 8 271 | pSource = [0, 0] 272 | sbi = 0 273 | t = 0 274 | L = 0 275 | R = 0 276 | out = self.bit_transform(self.arrayIP, 64, l) 277 | pSource[0] = 0xFFFFFFFF & out 278 | pSource[1] = (-4294967296 & out) >> 32 279 | for i in range(16): 280 | R = pSource[1] 281 | R = self.bit_transform(self.arrayE, 64, R) 282 | R ^= longs[i] 283 | for j in range(8): 284 | pR[j] = 255 & R >> j * 8 285 | SOut = 0 286 | for sbi in range(7, -1, -1): 287 | SOut <<= 4 288 | SOut |= self.matrixNSBox[sbi][pR[sbi]] 289 | 290 | R = self.bit_transform(self.arrayP, 32, SOut) 291 | L = pSource[0] 292 | pSource[0] = pSource[1] 293 | pSource[1] = L ^ R 294 | pSource = pSource[::-1] 295 | out = -4294967296 & pSource[1] << 32 | 0xFFFFFFFF & pSource[0] 296 | out = self.bit_transform(self.arrayIP_1, 64, out) 297 | return out 298 | 299 | def sub_keys(self, l, longs, n): 300 | l2 = self.bit_transform(self.arrayPC_1, 56, l) 301 | for i in range(16): 302 | l2 = ((l2 & self.arrayLsMask[self.arrayLs[i]]) << 28 - 303 | self.arrayLs[i] | (l2 & ~self.arrayLsMask[self.arrayLs[i]]) >> self.arrayLs[i]) 304 | longs[i] = self.bit_transform(self.arrayPC_2, 64, l2) 305 | j = 0 306 | while n == 1 and j < 8: 307 | l3 = longs[j] 308 | longs[j], longs[15 - j] = longs[15 - j], longs[j] 309 | j += 1 310 | 311 | def encrypt(self, msg, key=SECRET_KEY): 312 | if isinstance(msg, str): 313 | msg = msg.encode() 314 | if isinstance(key, str): 315 | key = key.encode() 316 | assert (isinstance(msg, bytes)) 317 | assert (isinstance(key, bytes)) 318 | 319 | # 处理密钥块 320 | l = 0 321 | for i in range(8): 322 | l = l | key[i] << i * 8 323 | 324 | j = len(msg) // 8 325 | # arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了 326 | arrLong1 = [0] * 16 327 | self.sub_keys(l, arrLong1, 0) 328 | # arrLong2 存放的是前部分的明文 329 | arrLong2 = [0] * j 330 | for m in range(j): 331 | for n in range(8): 332 | arrLong2[m] |= msg[n + m * 8] << n * 8 333 | 334 | # 用于存放密文 335 | arrLong3 = [0] * ((1 + 8 * (j + 1)) // 8) 336 | # 计算前部的数据块(除了最后一部分) 337 | for i1 in range(j): 338 | arrLong3[i1] = self.DES64(arrLong1, arrLong2[i1]) 339 | 340 | # 保存多出来的字节 341 | arrByte1 = msg[j * 8:] 342 | l2 = 0 343 | for i1 in range(len(msg) % 8): 344 | l2 |= arrByte1[i1] << i1 * 8 345 | # 计算多出的那一位(最后一位) 346 | arrLong3[j] = self.DES64(arrLong1, l2) 347 | 348 | # 将密文转为字节型 349 | arrByte2 = [0] * (8 * len(arrLong3)) 350 | i4 = 0 351 | for l3 in arrLong3: 352 | for i6 in range(8): 353 | arrByte2[i4] = (255 & l3 >> i6 * 8) 354 | i4 += 1 355 | return arrByte2 356 | 357 | def base64_encrypt(self, msg): 358 | b1 = self.encrypt(msg) 359 | b2 = bytearray(b1) 360 | s = base64.encodebytes(b2) 361 | return s.replace(b'\n', b'').decode() 362 | 363 | 364 | hexs = [a for a in "0123456789abcdef"] 365 | 366 | 367 | def hex2Str(hx: str): 368 | a = hx.lower() 369 | length = int(len(a) / 2) 370 | bt = bytearray() 371 | for i in range(0, length - 1): 372 | i2 = i * 2 373 | b = int(a[i2:i2 + 2], 16) & 255 374 | bt.append(b) 375 | return bytes(bt) 376 | 377 | 378 | def byte2hex(bt: bytes): 379 | strs = "" 380 | for i in bt: 381 | s = hex(i)[2:].upper() 382 | if len(s) > 3: 383 | strs += s[6:] 384 | elif len(s) < 2: 385 | strs += '0' + s 386 | else: 387 | strs += s 388 | return strs 389 | 390 | 391 | def hashMd5(s: str): 392 | return md5(s.encode("utf-8")).hexdigest() 393 | 394 | 395 | mHttp = Http.HttpRequest() 396 | 397 | 398 | def testGetLink(qqmusicID='003cI52o4daJJL', platform='qq', quality='sq'): 399 | t1_MusicID = qqmusicID 400 | platform = platform 401 | t2 = quality 402 | device = 'MI 14 Pro Max' 403 | osVersion = '27' 404 | time = str(int(tm.time())) 405 | # f389249d91bd845c9b817db984054cfb 1678713735 6562653262383463363633646364306534333663 406 | lowerCase = hashMd5("f389249d91bd845c9b817db984054cfb" + time + "6562653262383463363633646364306534333663").lower() 407 | 408 | s6 = "{\\\"method\\\":\\\"GetMusicUrl\\\",\\\"platform\\\":\\\"" + platform + "\\\",\\\"t1\\\":\\\"" + t1_MusicID + "\\\",\\\"t2\\\":\\\"" + t2 + "\\\"}" 409 | s7 = "{\\\"uid\\\":\\\"\\\",\\\"token\\\":\\\"\\\",\\\"deviceid\\\":\\\"84ac82836212e869dbeea73f09ebe52b\\\",\\\"appVersion\\\":\\\"4.1.0.V4\\\",\\\"vercode\\\":\\\"4100\\\",\\\"device\\\":\\\"" + device + "\\\",\\\"osVersion\\\":\\\"" + osVersion + "\\\"}" 410 | s8 = "{\n\t\"text_1\":\t\"" + s6 + "\",\n\t\"text_2\":\t\"" + s7 + "\",\n\t\"sign_1\":\t\"" + lowerCase + "\",\n\t\"time\":\t\"" + time + "\",\n\t\"sign_2\":\t\"" + hashMd5( 411 | s6.replace("\\", "") + s7.replace("\\", "") + lowerCase + time + "NDRjZGIzNzliNzEx").lower() + "\"\n}" 412 | 413 | # 资源大师接口 414 | # s5 = hashMd5( 415 | # "2c6d031981d1b6920fefd537043fd6eb" + time + "6562653262383463363633646364306534333663").lower() 416 | # s6 = "{\\\"method\\\":\\\"GetMusicUrl\\\",\\\"platform\\\":\\\"" + platform + "\\\",\\\"t1\\\":\\\"" + t1_MusicID + "\\\",\\\"t2\\\":\\\"" + t2 + "\\\"}" 417 | # s7 = "{\\\"uid\\\":\\\"\\\",\\\"token\\\":\\\"\\\",\\\"deviceid\\\":\\\"84c599d711066ef740eb49109dac9782\\\",\\\"appVersion\\\":\\\"4.0.9.V1\\\",\\\"vercode\\\":\\\"4090\\\",\\\"device\\\":\\\"" + device + "\\\",\\\"osVersion\\\":\\\"" + osVersion + "\\\"}" 418 | # s8 = "{\n\t\"text_1\":\t\"" + s6 + "\",\n\t\"text_2\":\t\"" + s7 + "\",\n\t\"sign_1\":\t\"" + s5 + "\",\n\t\"time\":\t\"" + time + "\",\n\t\"sign_2\":\t\"" + hashMd5( 419 | # s6.replace("\\", "") + s7.replace("\\", "") + s5 + time + "NDRjZGIzNzliNzEx").lower() + "\"\n}" 420 | # 资源大师接口结束 421 | 422 | s8 = AESEncrypt(s8, "6480fedae539deb2") 423 | s8 = byte2hex(s8) 424 | s8 = byte2hex(s8.encode("utf-8")).encode("utf-8") 425 | s8 = zlib.compress(s8) 426 | url = [ 427 | "http://app.kzti.top:1030/client/cgi-bin/api.fcg", 428 | "http://119.91.134.171:1030/client/cgi-bin/api.fcg", 429 | "http://106.52.68.150:1030/client/cgi-bin/api.fcg" # 资源大师接口 430 | ] 431 | url = url[0] 432 | res = mHttp.getHttp(url, 1, s8) 433 | try: 434 | res = zlib.decompress(res.content).decode("utf-8") 435 | except: 436 | print("下载失败,无法获取原始文件链接。") 437 | return "下载失败,无法获取原始文件链接。" 438 | res = json.loads(res) 439 | if res['code'] == '403': 440 | print("下载失败,解析服务器返回403错误代码。") 441 | return "下载失败,解析服务器返回403错误代码。" 442 | # print(res['data']) 443 | return res['data'] 444 | 445 | 446 | # testGetLink() 447 | 448 | 449 | def testUnzip(): 450 | file = '/Users/qiuchenly/Downloads/response' 451 | # file = '/Users/qiuchenly/Downloads/request' 452 | with open(file, 'rb+') as p: 453 | bt: bytes = p.read() 454 | bt = zlib.decompress(bt) 455 | res = bt.decode("utf-8") 456 | 457 | # mode = 1 458 | # if mode is 1: 459 | # code = hex2Str(hex2Str(res).decode("utf-8")) 460 | # code = AESUtils.AESUtil().decrypt(code, "6480fedae539deb2") 461 | print(res) 462 | 463 | # testUnzip() 464 | --------------------------------------------------------------------------------