├── public ├── robots.txt ├── 1.webp └── logo_small.webp ├── assets └── main.css ├── server ├── tsconfig.json └── api │ ├── getPlaylist.get.ts │ ├── getSongLyric.get.ts │ ├── getSongInfo.get.ts │ └── getSongUrl.get.ts ├── tsconfig.json ├── pages ├── single.vue ├── test.vue └── index.vue ├── .gitignore ├── components ├── appHeader.vue └── notification.vue ├── .prettierrc ├── app.vue ├── package.json ├── nuxt.config.ts ├── types └── index.ts ├── README.md └── utils └── downloadSong.ts /public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin "daisyui"; 3 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /public/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sooooooooooooooooootheby/Netease_analyze/HEAD/public/1.webp -------------------------------------------------------------------------------- /public/logo_small.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sooooooooooooooooootheby/Netease_analyze/HEAD/public/logo_small.webp -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /pages/single.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pages/test.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /components/appHeader.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"], 3 | "printWidth": 900, 4 | "tabWidth": 4, 5 | "useTabs": false, 6 | "semi": true, 7 | "singleQuote": false, 8 | "quoteProps": "as-needed", 9 | "jsxSingleQuote": false, 10 | "trailingComma": "es5", 11 | "bracketSpacing": true, 12 | "bracketSameLine": true, 13 | "arrowParens": "always", 14 | "endOfLine": "lf", 15 | "rangeStart": 0, 16 | "requirePragma": false, 17 | "insertPragma": false, 18 | "proseWrap": "preserve", 19 | "htmlWhitespaceSensitivity": "css" 20 | } 21 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netease-analyze", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "postinstall": "nuxt prepare" 11 | }, 12 | "dependencies": { 13 | "@nuxt/icon": "^1.13.0", 14 | "@tailwindcss/vite": "^4.1.7", 15 | "daisyui": "^5.0.37", 16 | "jszip": "^3.10.1", 17 | "nuxt": "^3.16.2", 18 | "prettier": "^3.5.3", 19 | "prettier-plugin-tailwindcss": "^0.6.11", 20 | "tailwindcss": "^4.1.7", 21 | "vue": "^3.5.13", 22 | "vue-router": "^4.5.0" 23 | }, 24 | "pnpm": { 25 | "onlyBuiltDependencies": [ 26 | "@tailwindcss/oxide" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from "@tailwindcss/vite"; 2 | 3 | export default defineNuxtConfig({ 4 | compatibilityDate: "2024-11-01", 5 | devtools: { enabled: true }, 6 | 7 | modules: ["@nuxt/icon"], 8 | 9 | vite: { 10 | plugins: [tailwindcss()], 11 | }, 12 | 13 | css: ["~/assets/main.css"], 14 | 15 | app: { 16 | head: { 17 | title: "网易云无损解析", 18 | htmlAttrs: { 19 | lang: "zh-cn", 20 | }, 21 | meta: [ 22 | { name: "description", content: "一个网易云无损解析站点" }, 23 | ], 24 | link: [{ rel: "icon", type: "image/x-icon", href: "/logo_small.webp" }], 25 | }, 26 | }, 27 | 28 | runtimeConfig: { 29 | cookie: process.env.COOKIE, 30 | }, 31 | 32 | imports: { 33 | dirs: ["types/*.ts"], 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | export interface author { 2 | id: number; 3 | name: string; 4 | } 5 | 6 | export interface album { 7 | id: number; 8 | name: string; 9 | cover: string; 10 | } 11 | 12 | export interface song { 13 | name: string; 14 | id: number; 15 | author: author[]; 16 | album: album; 17 | } 18 | 19 | export interface creator { 20 | uid: number; 21 | avatar: string; 22 | name: string; 23 | } 24 | 25 | export interface playlist { 26 | code: number; 27 | name: string; 28 | coverImage: string; 29 | songCount: number; 30 | description: string; 31 | tags: Array; 32 | creator: creator; 33 | list: Array<{ id: number }>; 34 | } 35 | 36 | export interface lyric { 37 | code: number; 38 | lyric: string; 39 | } 40 | 41 | export interface url { 42 | code: string; 43 | url: string; 44 | size: number; 45 | type: string; 46 | time: number; 47 | } 48 | 49 | export interface download { 50 | id: number; 51 | name: string; 52 | cover: string; 53 | status: string; 54 | } 55 | -------------------------------------------------------------------------------- /components/notification.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # !声明 ! 2 | 3 | 本项目为开源软件,遵循MIT许可证。任何个人或组织均可自由使用、修改和分发本项目的源代码。然而,我们明确声明,本项目及其任何衍生作品不得用于任何商业或付费项目。任何违反此声明的行为都将被视为对本项目许可证的侵犯。我们鼓励大家在遵守开源精神和许可证的前提下,积极贡献和分享代码。 4 | 5 | # Netease_analyze 6 | 7 | 这是一个网易云无损解析站点, 因为我发现很多解析网站都没有批量下载的功能, 索性我自己写了一个. 8 | 9 | 之前有一个旧版本的是纯vue前端的, 通过调用 [Netease_url](https://github.com/Suxiaoqinx/Netease_url) & [NeteaseCloudMusicApi](https://github.com/sooooooooooooooooootheby/NeteaseCloudMusicApi) 两个项目的接口实现批量下载歌单歌曲. 10 | 11 | 但是因为接口都部署在服务器上, 不是很好维护, 所以我花了一点时间研究了一下源码移植出来使用 nuxt 打包到一起. 12 | 13 | 后端接口代码位于`/server/api`下. 14 | 15 | ## 部署 16 | 17 | 本地部署 18 | 19 | ```bash 20 | git clone https://github.com/sooooooooooooooooootheby/Netease_analyze.git 21 | 22 | cd Netease_analyze 23 | 24 | npm i 25 | 26 | npm run dev 27 | ``` 28 | 29 | 设置环境变量 30 | 31 | 在根目录新建一个`.env`文件, 填入黑胶会员账号的cookie. 32 | 33 | ``` 34 | COOKIE="MUSIC_U=你获取到的MUSIC_U值;appver=8.9.75;" 35 | ``` 36 | 37 | # 感谢以下项目 38 | 39 | [![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=Suxiaoqinx&repo=Netease_url)](https://github.com/Suxiaoqinx/Netease_url) 40 | 41 | [![Readme Card](https://github-readme-stats.vercel.app/api/pin/?username=Binaryify&repo=NeteaseCloudMusicApi)](https://github.com/Binaryify/NeteaseCloudMusicApi) 42 | -------------------------------------------------------------------------------- /server/api/getPlaylist.get.ts: -------------------------------------------------------------------------------- 1 | import { playlist } from "~/types"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const { id } = getQuery(event) as { id: string }; 5 | 6 | //12763433746, 13246943556 7 | try { 8 | const result: any = await $fetch(`https://music.163.com/api/v6/playlist/detail?id=${id}`, { 9 | parseResponse: JSON.parse, 10 | }); 11 | 12 | if (result.code !== 200) { 13 | throw createError({ 14 | statusCode: result.code, 15 | message: result, 16 | }); 17 | } 18 | 19 | const playlist: playlist = { 20 | code: result.code, 21 | name: result.playlist.name, 22 | coverImage: result.playlist.coverImgUrl, 23 | songCount: result.playlist.trackCount, 24 | description: result.playlist.description, 25 | tags: result.playlist.tags, 26 | creator: { 27 | uid: result.playlist.creator.userId, 28 | avatar: result.playlist.creator.avatarUrl, 29 | name: result.playlist.creator.nickname, 30 | }, 31 | list: result.playlist.trackIds.map((item: any) => ({ 32 | id: item.id, 33 | })), 34 | }; 35 | 36 | return playlist; 37 | } catch (error: any) { 38 | throw createError({ 39 | statusCode: 500, 40 | message: error, 41 | }); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /server/api/getSongLyric.get.ts: -------------------------------------------------------------------------------- 1 | import { lyric } from "~/types"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const config = useRuntimeConfig(); 5 | const { id } = getQuery(event) as { id: string }; 6 | 7 | const url = "https://music.163.com/api/song/lyric"; 8 | const headers = { 9 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 10 | Referer: "https://music.163.com/", 11 | Origin: "https://music.163.com", 12 | "Content-Type": "application/x-www-form-urlencoded", 13 | Cookie: config.cookie, 14 | }; 15 | const params = new URLSearchParams({ 16 | id, 17 | cp: "false", 18 | tv: "0", 19 | lv: "0", 20 | rv: "0", 21 | kv: "0", 22 | yv: "0", 23 | ytv: "0", 24 | yrv: "0", 25 | }); 26 | 27 | try { 28 | const response: any = await $fetch(url, { 29 | method: "POST", 30 | headers, 31 | body: params.toString(), 32 | parseResponse: (text) => text, 33 | }); 34 | const firstJson = response.match(/^\{.*?\}(?=\{|$)/s)?.[0]; 35 | const result = firstJson ? JSON.parse(firstJson) : null; 36 | 37 | if (result.code !== 200) { 38 | throw createError({ 39 | statusCode: result.code, 40 | message: result.msg, 41 | }); 42 | } 43 | 44 | const lyric: lyric = { 45 | code: result.code, 46 | lyric: result.lrc.lyric, 47 | }; 48 | 49 | return lyric; 50 | } catch (error) { 51 | console.error("[NCM API] 获取歌曲信息失败:", error); 52 | throw error; 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /server/api/getSongInfo.get.ts: -------------------------------------------------------------------------------- 1 | import { song } from "~/types"; 2 | 3 | export default defineEventHandler(async (event) => { 4 | const config = useRuntimeConfig(); 5 | const { id } = getQuery(event) as { id: string }; 6 | 7 | const url = "https://music.163.com/api/v3/song/detail"; 8 | const headers = { 9 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", 10 | Referer: "https://music.163.com/", 11 | Origin: "https://music.163.com", 12 | "Content-Type": "application/x-www-form-urlencoded", 13 | Cookie: config.cookie, 14 | }; 15 | const params = new URLSearchParams(); 16 | params.append("c", JSON.stringify([{ id: id, v: 0 }])); 17 | 18 | try { 19 | const response: any = await $fetch(url, { 20 | method: "POST", 21 | headers, 22 | body: params.toString(), 23 | parseResponse: (text) => text, 24 | }); 25 | const firstJson = response.match(/^\{.*?\}(?=\{|$)/s)?.[0]; 26 | const result = firstJson ? JSON.parse(firstJson) : null; 27 | 28 | if (result.code !== 200) { 29 | throw createError({ 30 | statusCode: result.code, 31 | message: result.msg, 32 | }); 33 | } 34 | 35 | const song: song = { 36 | name: result.songs[0].name, 37 | id: result.songs[0].id, 38 | author: result.songs[0].ar.map((item: any) => ({ 39 | id: item.id, 40 | name: item.name, 41 | })), 42 | album: { 43 | id: result.songs[0].al.id, 44 | name: result.songs[0].al.name, 45 | cover: result.songs[0].al.picUrl, 46 | }, 47 | }; 48 | 49 | return song; 50 | } catch (error) { 51 | console.error("[NCM API] 获取歌曲信息失败:", error); 52 | throw error; 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /utils/downloadSong.ts: -------------------------------------------------------------------------------- 1 | import JSZip from "jszip"; 2 | 3 | const formatLRC = (lyric: string) => { 4 | return `[00:00.00]${lyric}\n`; 5 | }; 6 | 7 | export default async function (id: number, name: string, cover: string, level: string) { 8 | const zip = new JSZip(); 9 | const folder = zip.folder(name)!; 10 | 11 | try { 12 | // 下载封面 13 | const coverImage: any = await $fetch(cover); 14 | const coverBlob = await coverImage.arrayBuffer(); 15 | folder.file(`${name}.jpg`, coverBlob); 16 | 17 | // 下载歌词 18 | const getLyric = await $fetch(`/api/getSongLyric?id=${id}`); 19 | if (getLyric.code !== 200) { 20 | return getLyric; 21 | } 22 | const lyric = formatLRC(getLyric.lyric); 23 | folder.file(`${name}.lrc`, lyric); 24 | 25 | // 下载歌曲 26 | const url = await $fetch(`/api/getSongUrl?id=${id}&level=${level}`); 27 | if (Number(url.code) !== 200) { 28 | return url; 29 | } 30 | console.log(url); 31 | const secureUrl = url.url.replace("http://", "https://"); 32 | console.log(secureUrl); 33 | const song: any = await $fetch(secureUrl); 34 | const songBlob = await song.arrayBuffer(); 35 | const fileName = `${name}.${url.type}`; 36 | folder.file(fileName, songBlob); 37 | 38 | // 打包下载 39 | zip.generateAsync({ type: "blob" }) 40 | .then((zipBlob) => { 41 | const url = URL.createObjectURL(zipBlob); 42 | const a = document.createElement("a"); 43 | a.href = url; 44 | a.download = `${name}.zip`; 45 | document.body.appendChild(a); 46 | a.click(); 47 | document.body.removeChild(a); 48 | URL.revokeObjectURL(url); 49 | }) 50 | .catch((error) => { 51 | console.error({ content: "下载文件失败: " + error }); 52 | }); 53 | } catch (error) { 54 | console.error(error); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/api/getSongUrl.get.ts: -------------------------------------------------------------------------------- 1 | import { randomInt, createHash, createCipheriv } from "node:crypto"; 2 | import { url } from "~/types"; 3 | 4 | function hexDigest(data: Buffer): string { 5 | return data.toString("hex"); 6 | } 7 | 8 | function hashDigest(text: string): Buffer { 9 | return createHash("md5").update(text, "utf8").digest(); 10 | } 11 | 12 | function hashHexDigest(text: string): string { 13 | return hexDigest(hashDigest(text)); 14 | } 15 | 16 | function parseCookie(text = ""): Record { 17 | return text.split(";").reduce((acc, item) => { 18 | const [key, value] = item.trim().split("="); 19 | return key ? { ...acc, [key]: value || "" } : acc; 20 | }, {}); 21 | } 22 | 23 | async function url_v1(id: string, level: string, cookie: Record) { 24 | const url = "https://interface3.music.163.com/eapi/song/enhance/player/url/v1"; 25 | const AES_KEY = Buffer.from("e82ckenh8dichen8", "utf8"); 26 | 27 | // 构造请求参数 28 | const config = { 29 | os: "pc", 30 | appver: "", 31 | osver: "", 32 | deviceId: "pyncm!", 33 | requestId: randomInt(20_000_000, 30_000_000).toString(), 34 | }; 35 | 36 | const payload: Record = { 37 | ids: [id], 38 | level, 39 | encodeType: "flac", 40 | header: JSON.stringify(config), 41 | }; 42 | 43 | if (level === "sky") payload.immerseType = "c51"; 44 | 45 | // 生成加密参数 46 | const apiPath = new URL(url).pathname.replace("/eapi/", "/api/"); 47 | const digest = hashHexDigest(`nobody${apiPath}use${JSON.stringify(payload)}md5forencrypt`); 48 | const params = `${apiPath}-36cd479b6b5-${JSON.stringify(payload)}-36cd479b6b5-${digest}`; 49 | 50 | // AES 加密 51 | const cipher = createCipheriv("aes-128-ecb", AES_KEY, null); 52 | const encryptedParams = Buffer.concat([cipher.update(params, "utf8"), cipher.final()]).toString("hex"); 53 | 54 | // 构造请求 55 | const mergedCookies = { 56 | os: "pc", 57 | appver: "", 58 | osver: "", 59 | deviceId: "pyncm!", 60 | ...cookie, 61 | }; 62 | 63 | return await $fetch(url, { 64 | method: "POST", 65 | headers: { 66 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/2.10.2.200154", 67 | "Content-Type": "application/x-www-form-urlencoded", 68 | Cookie: Object.entries(mergedCookies) 69 | .map(([k, v]) => `${k}=${v}`) 70 | .join("; "), 71 | }, 72 | body: `params=${encryptedParams}`, 73 | }); 74 | } 75 | 76 | export default defineEventHandler(async (event) => { 77 | const config = useRuntimeConfig(); 78 | const { id, level } = getQuery(event); 79 | if (!id || !level) { 80 | throw createError({ 81 | statusCode: 400, 82 | message: "Missing id or level parameter", 83 | }); 84 | } 85 | 86 | try { 87 | const cookies = parseCookie(config.cookie); 88 | const result: any = await url_v1(id.toString(), level.toString(), cookies); 89 | 90 | const url: url = { 91 | code: result.code, 92 | url: result.data[0].url, 93 | size: result.data[0].size, 94 | type: result.data[0].type, 95 | time: result.data[0].time, 96 | }; 97 | 98 | $fetch(`https://api.s22y.moe/count/get?name=neteasy`); 99 | 100 | return url; 101 | } catch (error) { 102 | throw createError({ 103 | statusCode: 500, 104 | message: (error as Error).message || "Internal server error", 105 | }); 106 | } 107 | }); 108 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 141 | 142 | 237 | --------------------------------------------------------------------------------