说明

为什么我要部署这个项目?
答案很简单:听歌不想花钱。
在没有部署之前,我是用的酷我提供的API。【感谢酷我】
但是近半年来,酷我的API越来越不稳定了。所以,在此基础上,我使用了GD Studio's Online Music Platform API,本文也是根据此api获取的音乐播放链接。

开始之前

你需要准备:

  • 一个能访问github的电脑。
  • 安装了node.js 24版本或以上
  • 配置好了npm镜像以及下载了pnpm模块

后端的部署

首先,后端部署比较简单只需要访问


将项目源码下载到你的电脑,然后按照教程,在项目目录里执行

pnpm i

然后等待模块儿全部安装完毕
image.png
当你看到如上图一样的就表示你安装成功了,接下来就可以执行

# 默认端口 3000
node app.js

执行后就会显示
image.png
访问后端,你就会看到
image.png
到此,后端部署完毕【当然,到这里还是使用的网易云音乐的api,无法获取付费音乐,如何修改我们放在第三步来说明】

前端的部署

前端我试了很多的网易云项目,如AlgerMusicPlayer、SPlayer、YesPlayMusic等。
YesPlayMusic界面比较素(太素了,啥也没有),所以没有使用;AlgerMusicPlayer没有手机端适配,没有使用;最终选择了SPlayer作为前端。


同样的,将项目下载下来,在项目目录执行

pnpm install

安装完依赖后,复制 .env.example 为 .env 并按需修改,如:将 .env 文件中的 VITE_API_URL 改为第一步得到的 API 地址VITE_API_URL = http://localhost:3000;【最后不能/】
执行编译打包之前,需要再添加一句SKIP_NATIVE_BUILD=true
所以最后你的.env文件样式可能是

# WEB 端口
VITE_WEB_PORT=14558
# API 端口
VITE_SERVER_PORT=25884
# API 地址 - 结尾不要加 /
VITE_API_URL=http://localhost:3000

SKIP_NATIVE_BUILD=true

执行编译打包

pnpm build

文件输出在out/renderer 目录,本地测试可以按照下面的方法

测试

cd out/renderer
npx http-server -p 4000

访问http://127.0.0.1:4000/,就可以发现已经部署成功了!
image.png

修改后端Api

因为版权限制,无法听vip音乐,除非你账号本来就有vip【不过既然你有vip了还部署这个干嘛】

目前网站默认关闭试听,需要在设置里打开
image.png
而且试听会有提醒,需要去掉。
我们需要做的有这几点:

  • 默认打开播放试听
  • 去除试听弹窗
  • 修改音乐播放链接使用的api

    实施计划

    默认打开播放试听

    在SPlayer里,src/stores/setting.ts中的第552行

    playSongDemo: false,
    ->
    playSongDemo: true,

    这样就实现了默认播放试听【如果你不打开,它不让你播放】

    去除试听弹窗

    在SPlayer里,src/core/player/SongManager.ts中的第552行【?行数还一样了】

    if (isTrial) window.$message.warning("当前歌曲仅可试听");
    ->
    // if (isTrial) window.$message.warning("当前歌曲仅可试听");

    注释掉就行了。修改完不要忘记再次执行

    pnpm build
    npx http-server -p 4000

    修改音乐播放链接使用的api

    回到api-enhanced,首先安装axios【跨域请求第三方api】

    pnpm add axios

    修改文件module/song_url_v1.js【这次改的有点多】

    //module/song_url_v1.js
    // 歌曲链接 - v1
    // 此版本不再采用 br 作为音质区分的标准
    // 而是采用 standard, exhigh, lossless, hires, jyeffect(高清环绕声), sky(沉浸环绕声), jymaster(超清母带) 进行音质判断
    // 当unblock为true时, 会尝试使用unblockmusic-utils进行解锁, 同时音质设置不会生效, 但仍然为必须传入参数
    
    const logger = require('../util/logger.js')
    const createOption = require('../util/option.js')
    const axios = require('axios')
    
    module.exports = async (query, request) => {
    const {
      matchID,
    } = require('@neteasecloudmusicapienhanced/unblockmusic-utils')
    require('dotenv').config()
    
    if (query.unblock === 'true') {
      try {
        const result = await matchID(query.id, query.source)
        logger.info('Starting unblock(uses modules unblock):', query.id, result)
        const useProxy = process.env.ENABLE_PROXY || 'false'
        let proxyUrl = ''
        if (result.data.url && result.data.url.includes('kuwo')) {
          proxyUrl =
            useProxy === 'true' && process.env.PROXY_URL
              ? process.env.PROXY_URL + result.data.url
              : result.data.url
        }
        return {
          status: 200,
          body: {
            code: 200,
            msg: 'Warning: Customizing unblock sources is not supported on this endpoint. Please use `/song/url/match` instead.',
            data: [
              {
                id: Number(query.id),
                url: result.data.url,
                type: 'flac',
                level: query.level,
                freeTrialInfo: 'null',
                fee: 0,
                proxyUrl: proxyUrl || '',
              },
            ],
          },
          cookie: [],
        }
      } catch (e) {
        console.error('Error in unblocking music:', e)
      }
    }
    
    const ids = String(query.id).split(',')
    let br = 999
    const level = query.level || 'lossless'
    if (level === 'standard') br = 128
    else if (level === 'higher') br = 192
    else if (level === 'exhigh') br = 320
    else if (level === 'lossless' || level === 'hires' || level === 'sky' || level === 'jyeffect' || level === 'jymaster') br = 999
    
    const tasks = ids.map(async (id) => {
      try {
        const res = await axios.get('https://music-api.gdstudio.xyz/api.php', {
          params: {
            types: 'url',
            source: 'netease',
            id: id,
            br: br,
          },
        })
        const data = res.data
        return {
          id: Number(id),
          url: data.url,
          br: data.br * 1000,
          size: data.size,
          md5: null,
          code: 200,
          expi: 1200,
          type: data.url ? data.url.split('.').pop() : 'mp3',
          gain: 0,
          fee: 0,
          payed: 0,
          flag: 0,
          canExtend: false,
          freeTrialInfo: null,
          level: level,
          encodeType: data.url ? data.url.split('.').pop() : 'mp3',
        }
      } catch (e) {
        return {
          id: Number(id),
          url: null,
          br: 0,
          size: 0,
          code: 200,
          freeTrialInfo: null,
        }
      }
    })
    
    const result = await Promise.all(tasks)
    result.sort((a, b) => {
      return ids.indexOf(String(a.id)) - ids.indexOf(String(b.id))
    })
    
    return {
      status: 200,
      body: {
        code: 200,
        data: result,
      },
    }
    }
    
    

    修改文件module/song_url.js

    //module/song_url.js
    // 歌曲链接
    const createOption = require('../util/option.js')
    const axios = require('axios')
    
    module.exports = async (query, request) => {
    const ids = String(query.id).split(',')
    let br = 999
    const queryBr = parseInt(query.br || 999000)
    if (queryBr < 192000) br = 128
    else if (queryBr < 320000) br = 192
    else if (queryBr < 740000) br = 320
    else if (queryBr < 999000) br = 740
    
    const tasks = ids.map(async (id) => {
      try {
        const res = await axios.get('https://music-api.gdstudio.xyz/api.php', {
          params: {
            types: 'url',
            source: 'netease',
            id: id,
            br: br,
          },
        })
        const data = res.data
        return {
          id: Number(id),
          url: data.url,
          br: data.br * 1000,
          size: data.size,
          md5: null,
          code: 200,
          expi: 1200,
          type: data.url ? data.url.split('.').pop() : 'mp3',
          gain: 0,
          fee: 0,
          payed: 0,
          flag: 0,
          canExtend: false,
          freeTrialInfo: null,
          level: 'standard',
          encodeType: data.url ? data.url.split('.').pop() : 'mp3',
        }
      } catch (e) {
        return {
          id: Number(id),
          url: null,
          br: 0,
          size: 0,
          code: 200,
          freeTrialInfo: null,
        }
      }
    })
    
    const result = await Promise.all(tasks)
    result.sort((a, b) => {
      return ids.indexOf(String(a.id)) - ids.indexOf(String(b.id))
    })
    return {
      status: 200,
      body: {
        code: 200,
        data: result,
      },
    }
    }
    

    总结

  • 引入 axios : 使用 axios 库来请求外部 API ( music-api.gdstudio.xyz )。
  • 替换获取逻辑 :
  • 移除了原有的调用网易云官方接口 ( /api/song/enhance/player/url ) 的代码。
  • 改为并行请求外部 API,支持同时获取多个歌曲 ID 的链接。
  • 保留了原有的响应数据结构 ( { code: 200, data: [...] } ),确保对现有客户端的兼容性。
  • 参数映射 :
  • 旧版 ( song_url.js ) : 将传入的 br (码率,如 320000) 映射为 API 支持的档位 (128, 192, 320, 740, 999)。
  • 新版 ( song_url_v1.js ) : 将传入的 level (音质等级,如 standard, exhigh, lossless) 映射为 API 支持的 br 档位。

    • standard -> 128
    • higher -> 192
    • exhigh -> 320
    • lossless , hires , sky , jyeffect , jymaster -> 999

      注意事项

      部署在服务器时,你不能进行以下操作
      单独前端启用 HTTPS或单独后端启用 HTTPS,这会报错【现代浏览器会阻止这种混合内容(Mixed Content)请求】,你将无法在前端获取任何内容。

      写在最后

      不用重复造轮子太爽了,本站右上角的音乐使用的就是此API.

      enjoy yourself!

      后续

      将SPlayer与api-enhanced合并打包为了镜像,一键部署前后端,将SPlayer打包后的out文件夹放在api-enhanced根目录。
      需要修改的地方:

  • SPlayer:
    • .env
  • api-enhanced:
    • Dockerfile
    • .dockerignore
    • server.js

      #.env
      # WEB 端口
      VITE_WEB_PORT=14558
      # API 端口
      VITE_SERVER_PORT=25884
      # API 地址 - 结尾不要加 /
      VITE_API_URL=
      
      SKIP_NATIVE_BUILD=true
      
      #Dockerfile
      FROM node:lts-alpine
      
      RUN apk add --no-cache tini
      
      ENV NODE_ENV=production
      USER node
      
      WORKDIR /app
      
      # 先只拷贝 package.json 和 yarn.lock(如果有),利用 Docker 缓存机制加速安装
      COPY --chown=node:node package.json yarn.lock* ./
      
      # 配置国内淘宝镜像源,极大地加快下载速度
      RUN yarn config set registry https://registry.npmmirror.com && yarn install --production --network-timeout=100000
      
      # 然后再拷贝其余代码
      COPY --chown=node:node . ./
      
      EXPOSE 3000
      
      CMD [ "/sbin/tini", "--", "node", "app.js" ]
      
      #.dockerignore
      /**
      !/module
      !/plugins
      !/public
      !/static
      !/util
      !/app.js
      !/server.js
      !/package.json
      !/index.js
      !/generateConfig.js
      !/main.js
      !/data
      !/.env
      !/pnpm-lock.yaml
      !/out/renderer
      //server.js
      //140行添加
      app.use(express.static(path.join(__dirname, 'out/renderer')))
      //340行添加
      app.use((req, res, next) => {
      if (!req.path.includes('.')) {
         res.sendFile(path.join(__dirname, 'out/renderer/index.html'))
      } else {
        res.status(404).end()
      }
      })

      最后在api-enhanced根目录执行build_and_export.bat

      @echo off
      chcp 65001 >nul
      setlocal
      
      :: Define Variables
      set IMAGE_NAME=ncm-api-splayer
      set TAR_FILE=%IMAGE_NAME%.tar
      
      echo ========================================================
      echo        NCM API Enhanced + SPlayer Docker Build Script
      echo ========================================================
      
      :: 1. Build Docker Image
      echo [1/3] Building Docker Image...
      docker build -t %IMAGE_NAME% .
      if %errorlevel% neq 0 (
      echo [ERROR] Docker build failed!
      pause
      exit /b %errorlevel%
      )
      
      :: 2. Export Image
      echo [2/3] Exporting image to %TAR_FILE% ...
      docker save -o %TAR_FILE% %IMAGE_NAME%
      if %errorlevel% neq 0 (
      echo [ERROR] Image export failed!
      pause
      exit /b %errorlevel%
      )
      
      :: 3. Complete
      echo [3/3] Success!
      echo.
      echo Image saved to: %CD%\%TAR_FILE%
      echo File size:
      for %%I in ("%TAR_FILE%") do echo %%~zI bytes
      echo.
      echo ========================================================
      echo Next steps:
      echo 1. Upload %TAR_FILE% to your server
      echo 2. Run on server: docker load -i %TAR_FILE%
      echo 3. Run container: docker run -d -p 3000:3000 --restart=always %IMAGE_NAME%
      echo ========================================================
      
      pause
      

      文件存放在api-enhanced根目录ncm-api-splayer.tar,上传到服务器导入镜像后执行

      docker run -d -p 4000:3000 ncm-api-splayer

      4000可改为任意端口,直接访问ip:4000即可部署完毕。