项目:https://github.com/EveElseIf/MusicParty

当前的项目目录结构

MusicParty/
│
├── BilibiliApi/
│	├── BilibiliApi.cs
│	└── BilibiliApi.csproj
│
├── MusicApiContract/
│	├── IMusicApi.cs
│	├── LoginException.cs
│	├── Music.cs
│	├── MusicApiContract.csproj
│	├── MusicServiceUser.cs
│	├── PlayList.cs
│	└── PlayableMusic.cs
│
├── MusicParty/
│	├── Controllers/
│	│	└── ApiController.cs
│	├── Hub/
│	│	└── MusicHub.cs
│	├── Properties/
│	│	└── launchSettings.json
│	├── Extensions.cs
│	├── MusicBroadcaster.cs
│	├── MusicParty.csproj
│	├── MusicProxyMiddleware.cs
│	├── PreprocessMiddleware.cs
│	├── Program.cs
│	├── User.cs
│	├── UserManager.cs
│	├── appsettings.Development.json
│	└── appsettings.json
│
├── NeteaseCloudMusicApi/
│	├── NeteaseCloudMusicApi.cs
│	└── NeteaseCloudMusicApi.csproj
│
├── QQMusicApi/
│	├── QQMusicApi.cs
│	└── QQMusicApi.csproj
│
├── music-party/
│	├── pages/
│	│	├── api/
│	│	│	└── hello.ts
│	│	├── _app.tsx
│	│	└── index.tsx
│	├── public/
│	│	├── favicon.ico
│	│	└── vercel.svg
│	├── src/
│	│	├── api/
│	│	│	├── api.ts
│	│	│	└── musichub.ts
│	│	├── components/
│	│	│	├── bilibilibinder.tsx
│	│	│	├── musicplayer.tsx
│	│	│	├── musicqueue.tsx
│	│	│	├── musicselector.tsx
│	│	│	├── myplaylist.tsx
│	│	│	├── neteasebinder.tsx
│	│	│	├── playlist.tsx
│	│	│	└── qqmusicbinder.tsx
│	│	└── utils/
│	│		 └── toast.ts
│	├── styles/
│	│	├── Home.module.css
│	│	└── globals.css
│	├── README.md
│	├── next.config.js
│	├── package.json
│	├── pnpm-lock.yaml
│	├── test.json
│	└── tsconfig.json
│
├── Dockerfile-backend
├── Dockerfile-frontend
├── Dockerfile-neteaseapi
├── Dockerfile-qqmusicapi
├── MusicParty.sln
├── README.md
└── docker-compose.yml

当前的修改

https://github.com/EveElseIf/MusicParty/blob/main/BilibiliApi/BilibiliApi.cs

51行

https://api.bilibili.com/x/web-interface/nav

91行

https://api.bilibili.com/x/player/wbi/playurl?bvid={ids[0]}&cid={ids[1]}&fnval=16

最后在45行添加

_http.DefaultRequestHeaders.Add("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0");

重构docker镜像

docker build -t mpmp:latest --file Dockerfile-backend .

docker build -t musicparty-backend:latest --file Dockerfile-backend .
docker build -t musicparty-neteaseapi:latest --file Dockerfile-neteaseapi .
docker build -t musicparty-qqmusicapi:latest --file Dockerfile-qqmusicapi .

删除本地镜像

查看
docker images

单个
docker rmi ubuntu:latest

多个
docker rmi limeve/musicparty-qqmusicapi limeve/musicparty-backend limeve/musicparty-frontend mpmp

强制删除(正在运行也删)
docker rmi -f <IMAGE_ID_OR_NAME>
如果镜像正在被容器使用,Docker 默认不会允许你删除该镜像。此时,你可以使用 -f 或 --force 选项强制删除镜像,但这可能会导致正在运行的容器出现问题。

<none>                         <none>    74f8a2e3ef18   4 days ago           708MB
删除悬空标签REPOSITORY和TAG都是<none>的镜像
docker image prune

可以改的正式点musicparty-backend ,实际生成的mpmp

查看本地镜像docker images

root@RainYun-vUQMSsZ0:~/data/docker_data/MusicP# docker images
REPOSITORY                     TAG       IMAGE ID       CREATED         SIZE
mpmp                           latest    846cc72094b5   3 minutes ago   1.23GB
limeve/musicparty-qqmusicapi   latest    baf6ed79e644   6 weeks ago     383MB
limeve/musicparty-backend      latest    58e8cf9d722c   6 weeks ago     1.23GB
limeve/musicparty-neteaseapi   latest    37b3535762ab   6 weeks ago     279MB
limeve/musicparty-frontend     latest    ae5f0bbb85e5   6 weeks ago     705MB
ghcr.io/mhsanaei/3x-ui         latest    c19a7d905a1f   2 months ago    206MB
halohub/halo                   2.19.3    9bf980655048   4 months ago    411MB
snowdreamtech/frps             latest    60a3451461c8   4 months ago    33.2MB
vectorim/element-web           latest    2fb6f3356553   5 months ago    75.8MB
matrixdotorg/synapse           latest    f9b69fb10ce7   5 months ago    426MB
halohub/halo                   2         6b205c032662   5 months ago    411MB
mysql                          8.0       7cd8c3640577   6 months ago    573MB
jc21/nginx-proxy-manager       latest    28147ecda659   6 months ago    1.09GB
rustdesk/rustdesk-server       latest    5e9792e3a4c0   8 months ago    23.5MB
dko0/lsky-pro                  latest    c89f58b471b2   2 years ago     838MB
mysql                          8.0.31    7484689f290f   2 years ago     538MB

修改docker-compose.ymlbackend: 的 镜像

version: "3.9"
services:
  neteaseapi:
    image: limeve/musicparty-neteaseapi:latest
    expose:
      - 3000
  qqmusicapi:
    image: limeve/musicparty-qqmusicapi:latest
    expose:
      - 3300
  frontend:
    image: limeve/musicparty-frontend:latest
    expose:
      - 3000
  backend:
    image: mpmp:latest  # 或者仅仅是 mpmp,取决于你本地镜像的标签

现状

现在可登录b站、网易云,同时可正常选歌曲,并且有了音量控件

可能需要的功能:

  • 强制切换下一首歌

  • 删除列表中的某一首歌

  • 点歌界面配置一下复选框与复选框全选框

  • 一键添加整个歌单,需要弹窗说明有多少歌曲同时再次确认

  • 增加点歌历史(或者叫播放历史)

  • 感觉反应有点慢,或许可以用更快的架构和语言进行重构?此项目就当做音乐接口的启蒙教程

后端

改完重构docker镜像后,B站可以登录了,可以正常获取收藏夹,但是无法点歌,还需要进一步的查看相关代码进行修改,45行添加完成后就可用了,现在可登录b站,网易云,同时可正常选歌曲

前端

增加了音量控件,不然声音太大了,每次去调电脑的音量合成器有点麻烦

前端用的是nextjs,但是docker镜像的制作不用考虑这些

前端是nextjs

https://github.com/vercel/next.js/

安装,不用安装,music-party\node_modules目录下已经有react与next了,这是项目所有者已经构建过的配置

npm install -g next

先安装环境,项目已经有了,不用子再下载了

npm install

构建所用代码

npm run build

不要用下面这句,会报错,真是莫名其妙,耽误了好久,气死,或许是要用music-party\node_modules目录下的那个版本next
//next build

npm start与npm run dev的区别

npm startnpm run dev 都是用来启动开发或生产环境的脚本命令,但它们的目的和执行方式有所不同。下面是它们的区别和可能导致你遇到的问题的原因。

1. npm run dev

  • 指定端口启动

npm run dev -- --port=3456

若无效,则修改文件package.json

  "scripts": {
    "dev": "next dev -p 3456",
  • 用途npm run dev 通常用于开发环境,启动开发服务器,支持热更新和快速的开发迭代。

  • 执行内容:通常会在 package.json 文件的 scripts 字段中定义为类似于:

    "scripts": {
      "dev": "next dev"
    }
    

    这条命令会启动 Next.js 的开发服务器,监听文件更改并自动刷新页面。

  • 特点:开发模式下,服务器会自动启用热重载(Hot Reloading),并且通常不会对构建和性能进行优化,目的是便于开发时快速查看效果。

2. npm start

  • 用途npm start 通常用于启动生产环境的服务器,它一般用于部署时运行已经构建好的项目。

  • 执行内容:它通常会在 package.json 文件的 scripts 字段中定义为:

    "scripts": {
      "start": "next start"
    }
    

    这个命令会启动 Next.js 的生产服务器,假设你已经通过 next build 构建了生产环境的应用。

  • 特点:生产模式下,Next.js 会优化项目并启用服务器端渲染(SSR)等功能,以提高性能。它不会启用热重载,且仅在构建后运行。

3. 为什么 npm run dev 可以启动,而 npm start 报错?

这个错误的原因通常是因为在运行 npm start 之前没有先执行 next build 构建应用。在生产环境中,next start 需要 .next 目录来运行应用,而该目录是在 next build 时生成的。

当你运行 npm run dev 时,开发服务器不需要构建文件,它会直接从源码运行,因此不会遇到缺少 .next 目录的问题。

解决办法:

  • 运行 next build:如果你想在生产环境中使用 npm start 启动应用,首先需要运行 next build 来构建项目。

    next build
    
  • 然后再运行 npm start

    npm start
    

这样,npm start 就会启动生产环境的服务器,且不会再报错。

总结:

  • npm run dev 用于开发环境,自动启动开发服务器并支持热更新。

  • npm start 用于生产环境,需要先执行 next build 生成生产环境的构建文件。

  • 你需要确保在运行 npm start 之前运行 next build,否则会出现缺少构建文件的错误。

next build 具体应该怎么使用

next build 是 Next.js 用于构建生产环境应用的命令。它会根据你的项目源代码生成优化后的应用,包含静态文件和服务端渲染的页面。next build 会在 .next 目录下生成构建产物,这是生产环境运行的必要文件。

使用步骤

  1. 确保项目已安装依赖: 在执行 next build 之前,确保你的项目依赖已经安装好。如果没有安装依赖,可以先执行:

    npm install
    
  2. 运行 next build 构建项目: 在项目根目录下运行 next build 命令。该命令会根据 next.config.js 的配置、页面、组件等源代码,生成用于生产环境的构建文件。

    next build
    

    或者如果你使用的是 npm,可以通过 npm run build 来执行:

    npm run build
    

    构建完成后,会在项目根目录下生成 .next 文件夹,里面包含了所有构建产物。

  3. 查看构建日志: 执行 next build 时,控制台会输出构建过程中的信息,包括生成的页面、静态文件、构建优化情况等。如果构建中有任何错误,它也会显示详细信息,帮助你调试。

  4. 构建完成后运行生产服务器: 构建完成后,你可以启动 Next.js 的生产环境服务器,使用以下命令:

    next start
    

    或者:

    npm start
    

    这会启动一个生产环境服务器,通常是运行在 http://localhost:3000 上,且不会启用开发时的热重载功能。

构建过程中的注意事项

  1. 环境变量

    • 确保你在构建时使用了正确的环境变量。如果你在开发过程中使用了 .env.local 或其他环境变量配置,生产环境的构建可能会用不同的环境变量文件。你可以根据需要手动设置环境变量。

    • 你可以通过 next build 时传递环境变量,或者通过 .env.production 来管理生产环境的环境变量。

  2. 分析构建输出

    • Next.js 会输出一些构建优化的分析数据,帮助你了解应用的打包情况。例如,你可能会看到页面拆分的大小、静态资源的大小、以及是否有过多的 JavaScript 被打包等信息。

    • 可以通过 next build 后加上 --profile 来查看详细的构建性能分析:

      next build --profile
      
  3. 部署前的检查

    • 在生产构建完成后,可以先在本地运行 next start 来检查应用是否正常工作。确保在开发模式下没有出现的问题不会出现在生产环境中。

  4. 构建产物

    • .next 文件夹包含了生产环境所需的构建文件,包括:

      • static:静态文件,如图片、字体等。

      • server:用于服务端渲染的页面和 API 路由。

      • cache:缓存的构建信息,用于加速构建过程。

常见问题

  • 构建失败: 如果你在运行 next build 时遇到错误,通常是因为代码中存在问题(例如,语法错误或模块导入问题)。你可以根据错误信息来定位并修复问题。

  • 构建时间长: 对于较大的项目,next build 可能会花费一些时间。可以通过代码分割、优化图片等方式来减少构建时间。

总结

  1. 构建项目: 运行 next build(或 npm run build)来生成生产环境构建文件。

  2. 运行生产服务器: 使用 next start 启动生产环境服务器。

  3. 检查构建和部署: 在构建后运行应用并确保一切正常,特别是在生产环境下测试应用的表现。

通过以上步骤,你可以成功构建和部署你的 Next.js 应用。

windows版部署启动

网页一起听歌 Windows服务器部署教程_哔哩哔哩_bilibili

可能需要修改的部分

01先前修改相关

51行的函数相关,登录基本信息

https://github.com/SocialSisterYi/bilibili-API-collect/blob/e5fbfed42807605115c6a9b96447f6328ca263c5/docs/login/login_info.md

91行的函数相关,视频流URL

https://github.com/SocialSisterYi/bilibili-API-collect/blob/e5fbfed42807605115c6a9b96447f6328ca263c5/docs/video/videostream_url.md

02合集

还有视频合集的种类又丰富了很多,这个音趴项目毕竟是两年前的,已经很久没维护了

添加的功能对文件的改动

对前端docker镜像的制作

docker build -t musicparty-frontend:latest --file Dockerfile-frontend .

删除列表音乐

改动的文件有两个

musicqueue.tsx
C:\Dev\temp\MusicParty\music-party\src\components\musicqueue.tsx

index.tsx
C:\Dev\temp\MusicParty\music-party\pages\index.tsx

改动后的文件

musicqueue.tsx

import { TriangleUpIcon, DeleteIcon } from '@chakra-ui/icons';
import {
  Text,
  Card,
  CardHeader,
  Heading,
  CardBody,
  OrderedList,
  ListItem,
  Box,
  Highlight,
  Flex,
  Tooltip,
  IconButton,
  useToast,
} from '@chakra-ui/react';
import { MusicOrderAction } from '../api/musichub';

export const MusicQueue = (props: {
  queue: MusicOrderAction[];
  top: (actionId: string) => void;
  remove: (actionId: string) => void; // 添加 remove 函数
}) => {
  const t = useToast();

  const handleRemove = async (actionId: string) => {
    try {
      await props.remove(actionId);
      toastInfo(t, `歌曲已从队列中删除`);
    } catch (err) {
      toastError(t, `删除歌曲失败`);
      console.error(err);
    }
  };

  return (
    <Card mt={4}>
      <CardHeader>
        <Heading size={'lg'}>播放队列</Heading>
      </CardHeader>
      <CardBody>
        <OrderedList>
          {props.queue.length > 0 ? (
            props.queue.map((v) => (
              <ListItem key={v.actionId} fontSize={'lg'}>
                <Flex alignItems="center">
                  <Box flex={1}>
                    {v.music.name} - {v.music.artists}
                    <Text fontSize={'sm'} fontStyle={'italic'}>
                      由 {v.enqueuerName} 点歌
                    </Text>
                  </Box>
                  <Flex>
                    <Tooltip hasArrow label={'将此歌曲至于队列顶端'}>
                      <IconButton
                        onClick={() => props.top(v.actionId)}
                        aria-label={'置顶'}
                        icon={<TriangleUpIcon />}
                        mr={2}
                      />
                    </Tooltip>
                    <Tooltip hasArrow label={'从队列中删除此歌曲'}>
                      <IconButton
                        onClick={() => handleRemove(v.actionId)}
                        aria-label={'删除'}
                        icon={<DeleteIcon />}
                        colorScheme="red"
                      />
                    </Tooltip>
                  </Flex>
                </Flex>
              </ListItem>
            ))
          ) : (
            <Text size={'md'}>
              <Highlight
                query={'点歌'}
                styles={{ px: '2', py: '1', rounded: 'full', bg: 'teal.100' }}
              >
                播放队列为空,请随意点歌吧~
              </Highlight>
            </Text>
          )}
        </OrderedList>
      </CardBody>
    </Card>
  );
};

index.tsx

import React, { useEffect, useRef, useState } from "react";
import Head from 'next/head';
import { Connection, Music, MusicOrderAction } from '../src/api/musichub';
import {
  Text,
  Button,
  Card,
  CardBody,
  CardHeader,
  Grid,
  GridItem,
  Heading,
  Input,
  ListItem,
  OrderedList,
  Tab,
  TabList,
  TabPanel,
  TabPanels,
  Tabs,
  useToast,
  Stack,
  Popover,
  PopoverArrow,
  PopoverBody,
  PopoverCloseButton,
  PopoverContent,
  PopoverFooter,
  PopoverHeader,
  PopoverTrigger,
  Portal,
  UnorderedList,
  Flex,
  Highlight,
  Box,
} from '@chakra-ui/react';
import { MusicPlayer } from '../src/components/musicplayer';
import { getMusicApis, getProfile } from '../src/api/api';
import { NeteaseBinder } from '../src/components/neteasebinder';
import { MyPlaylist } from '../src/components/myplaylist';
import { toastEnqueueOk, toastError, toastInfo } from '../src/utils/toast';
import { MusicSelector } from '../src/components/musicselector';
import { QQMusicBinder } from '../src/components/qqmusicbinder';
import { MusicQueue } from '../src/components/musicqueue'; // 确保正确导入
import { BilibiliBinder } from '../src/components/bilibilibinder';

export default function Home() {
  const [src, setSrc] = useState('');
  const [playtime, setPlaytime] = useState(0);
  const [nowPlaying, setNowPlaying] = useState<{
    music: Music;
    enqueuer: string;
  }>();
  const [queue, setQueue] = useState<MusicOrderAction[]>([]);
  const [userName, setUserName] = useState('');
  const [newName, setNewName] = useState('');
  const [onlineUsers, setOnlineUsers] = useState<
    { id: string; name: string }[]
  >([]);
  const [inited, setInited] = useState(false);
  const [chatContent, setChatContent] = useState<
    { name: string; content: string }[]
  >([]);
  const [chatToSend, setChatToSend] = useState('');
  const [apis, setApis] = useState<string[]>([]);
  const t = useToast();

  const conn = useRef<Connection>();
  useEffect(() => {
    if (!conn.current) {
      conn.current = new Connection(
        `${window.location.origin}/music`,
        async (music: Music, enqueuerName: string, playedTime: number) => {
          console.log(music);
          setSrc(music.url);
          setNowPlaying({ music, enqueuer: enqueuerName });
          setPlaytime(playedTime);
        },
        async (actionId: string, music: Music, enqueuerName: string) => {
          setQueue((q) => q.concat({ actionId, music, enqueuerName }));
        },
        async () => {
          setQueue((q) => q.slice(1));
        },
        async (actionId: string, operatorName: string) => {
          setQueue((q) => {
            const target = q.find((x) => x.actionId === actionId)!;
            toastInfo(
              t,
              `歌曲 "${target.music.name}-${target.music.artists}" 被 ${operatorName} 置顶了`
            );
            return [target].concat(q.filter((x) => x.actionId !== actionId));
          });
        },
        async (operatorName: string, _) => {
          toastInfo(t, `${operatorName} 切到了下一首歌`);
        },
        async (id: string, name: string) => {
          setOnlineUsers((u) => u.concat({ id, name }));
        },
        async (id: string) => {
          setOnlineUsers((u) => u.filter((x) => x.id !== id));
        },
        async (id: string, newName: string) => {
          setOnlineUsers((u) =>
            u.map((x) => (x.id === id ? { id, name: newName } : x))
          );
        },
        async (name: string, content: string) => {
          setChatContent((c) => c.concat({ name, content }));
        },
        async (content: string) => {
          // todo
          console.log(content);
        },
        async (msg: string) => {
          console.error(msg);
          toastError(t, msg);
        }
      );
      conn.current
        .start()
        .then(async () => {
          try {
            const queue = await conn.current!.getMusicQueue();
            setQueue(queue);
            const users = await conn.current!.getOnlineUsers();
            setOnlineUsers(users);
          } catch (err: any) {
            toastError(t, err);
          }
        })
        .catch((e) => {
          console.error(e);
          toastError(t, '请刷新页面重试');
        });

      getProfile()
        .then((u) => {
          setUserName(u.name);
        })
        .catch((e) => {
          console.log(e);
          toastError(t, '请刷新页面重试');
        });

      getMusicApis().then((as) => setApis(as));

      setInited(true);
    }
  }, []);

  const handleRemove = async (actionId: string) => {
    try {
      await conn.current!.removeMusic(actionId);
      setQueue((q) => q.filter((x) => x.actionId !== actionId));
      toastInfo(t, `歌曲已从队列中删除`);
    } catch (err) {
      toastError(t, `删除歌曲失败`);
      console.error(err);
    }
  };

  return (
    <Grid templateAreas={`"nav main"`} gridTemplateColumns={'2fr 5fr'} gap='1'>
      <Head>
        <title>🎵 音趴 🎵</title>
        <meta name='description' content='享受音趴!' />
        <link rel='icon' href='/favicon.ico' />
        <meta name='referrer' content='never' />
      </Head>
      <GridItem area={'nav'}>
        <Stack m={4} spacing={4}>
          <Card>
            <CardHeader>
              <Heading>{`欢迎, ${userName}!`}</Heading>
            </CardHeader>
            <CardBody>
              <Stack>
                <Popover>
                  {({ onClose }) => (
                    <>
                      <PopoverTrigger>
                        <Button>修改名字</Button>
                      </PopoverTrigger>
                      <Portal>
                        <PopoverContent>
                          <PopoverArrow />
                          <PopoverHeader>修改名字</PopoverHeader>
                          <PopoverCloseButton />
                          <PopoverBody>
                            <Input
                              value={newName}
                              placeholder={'输入新名字'}
                              onChange={(e) => setNewName(e.target.value)}
                            ></Input>
                          </PopoverBody>
                          <PopoverFooter>
                            <Button
                              colorScheme='blue'
                              onClick={async () => {
                                if (newName === '') return;
                                await conn.current!.rename(newName);
                                const user = await getProfile();
                                setUserName(user.name);
                                onClose();
                                setNewName('');
                              }}
                            >
                              确认
                            </Button>
                          </PopoverFooter>
                        </PopoverContent>
                      </Portal>
                    </>
                  )}
                </Popover>
                {apis.includes('NeteaseCloudMusic') && <NeteaseBinder />}
                {apis.includes('QQMusic') && <QQMusicBinder />}
                {apis.includes('Bilibili') && <BilibiliBinder />}
              </Stack>
            </CardBody>
          </Card>
          <Card>
            <CardHeader>
              <Heading>在线</Heading>
            </CardHeader>
            <CardBody>
              <UnorderedList>
                {onlineUsers.map((u) => {
                  return <ListItem key={u.id}>{u.name}</ListItem>;
                })}
              </UnorderedList>
            </CardBody>
          </Card>
          <Card>
            <CardHeader>
              <Heading>聊天</Heading>
            </CardHeader>
            <CardBody>
              <Flex>
                <Input
                  flex={1}
                  value={chatToSend}
                  onChange={(e) => setChatToSend(e.target.value)}
                  onKeyDown={async (e) => {
                    if (e.key === "Enter") {
                      if (chatToSend === '') return;
                      await conn.current?.chatSay(chatToSend);
                      setChatToSend('');
                    }
                  }}
                />
                <Button
                  ml={2}
                  onClick={async () => {
                    if (chatToSend === '') return;
                    await conn.current?.chatSay(chatToSend);
                    setChatToSend('');
                  }}
                >
                  发送
                </Button>
              </Flex>
              <UnorderedList>
                {chatContent.map((s) => (
                  <ListItem key={Math.random() * 1000}>
                    {`${s.name}: ${s.content}`}
                  </ListItem>
                ))}
              </UnorderedList>
            </CardBody>
          </Card>
        </Stack>
      </GridItem>

      <GridItem area={'main'}>
        <Tabs>
          <TabList>
            <Tab>播放列表</Tab>
            <Tab>从音乐ID点歌</Tab>
            <Tab>从歌单点歌</Tab>
          </TabList>
          <TabPanels>
            <TabPanel>
              <Flex flexDirection={'row'} mb={4} alignItems={'flex-end'}>
                {nowPlaying ? (
                  <>
                    <Heading>
                      {`正在播放:\n ${nowPlaying?.music.name} - ${nowPlaying?.music.artists}`}
                    </Heading>
                    <Text size={'md'} fontStyle={'italic'} ml={2}>
                      {`由 ${nowPlaying?.enqueuer} 点歌`}
                    </Text>
                  </>
                ) : (
                  <Heading>暂无歌曲正在播放</Heading>
                )}
              </Flex>

              <MusicPlayer
                src={src}
                playtime={playtime}
                nextClick={() => {
                  conn.current?.nextSong();
                }}
                reset={() => {
                  console.log('reset');
                  conn.current!.requestSetNowPlaying();
                  conn.current!.getMusicQueue().then((q) => {
                    setQueue(q);
                  });
                }}
              />

              <MusicQueue
                queue={queue}
                top={(actionId) => {
                  conn.current!.topSong(actionId);
                }}
                remove={handleRemove} // 添加 remove 函数
              />
            </TabPanel>
            <TabPanel>
              <MusicSelector apis={apis} conn={conn.current!} />
            </TabPanel>
            <TabPanel>
              {!inited ? (
                <Text>初始化...</Text>
              ) : (
                <MyPlaylist
                  apis={apis}
                  enqueue={(id, apiName) => {
                    conn
                      .current!.enqueueMusic(id, apiName)
                      .then(() => {
                        toastEnqueueOk(t);
                      })
                      .catch(() => {
                        toastError(t, `音乐 {id: ${id}} 加入队列失败`);
                      });
                  }}
                />
              )}
            </TabPanel>
          </TabPanels>
        </Tabs>
      </GridItem>
    </Grid>
  );
}

结果报错

 => ERROR [5/6] RUN pnpm build                                                                                     11.9s 
------                                                                                                                   
 > [5/6] RUN pnpm build:                                                                                                 
1.937                                                                                                                    1.937 > music-party@0.1.0 build /                                                                                        
1.937 > next build                                                                                                       
1.937                                                                                                                    
2.369 (node:18) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
2.369 (Use `node --trace-deprecation ...` to show where the warning was created)
2.557 Attention: Next.js now collects completely anonymous telemetry regarding usage.
2.559 This information is used to shape Next.js' roadmap and prioritize features.
2.559 You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
2.559 https://nextjs.org/telemetry
2.559 
2.715 info  - Linting and checking validity of types...
11.58 Failed to compile.
11.58 
11.58 ./pages/index.tsx:155:27
11.58 Type error: Property 'removeMusic' does not exist on type 'Connection'.
11.58 
11.58   153 |   const handleRemove = async (actionId: string) => {
11.58   154 |     try {
11.58 > 155 |       await conn.current!.removeMusic(actionId);
11.58       |                           ^
11.58   156 |       setQueue((q) => q.filter((x) => x.actionId !== actionId));
11.58   157 |       toastInfo(t, `歌曲已从队列中删除`);
11.58   158 |     } catch (err) {
11.66  ELIFECYCLE  Command failed with exit code 1.
------
Dockerfile-frontend:6
--------------------
   4 |     RUN npm install -g pnpm
   5 |     RUN pnpm install
   6 | >>> RUN pnpm build
   7 |     ENTRYPOINT [ "pnpm" , "start" ]
   8 |     
--------------------
ERROR: failed to solve: process "/bin/sh -c pnpm build" did not complete successfully: exit code: 1

添加音量控件

改动的文件有一个

musicplayer.tsx
C:\Dev\temp\MusicParty\music-party\src\components\musicplayer.tsx

改动后的文件内容

musicplayer.tsx

import {
  Button,
  Flex,
  Icon,
  IconButton,
  Modal,
  ModalBody,
  ModalContent,
  ModalFooter,
  ModalHeader,
  ModalOverlay,
  Progress,
  Slider,
  SliderTrack,
  SliderFilledTrack,
  SliderThumb,
  Text,
  Tooltip,
  useDisclosure,
  useToast,
} from "@chakra-ui/react";
import { ArrowRightIcon } from "@chakra-ui/icons";
import React, { useEffect, useRef, useState } from "react";

export const MusicPlayer = (props: {
  src: string;
  playtime: number;
  nextClick: () => void;
  reset: () => void;
}) => {
  const audio = useRef<HTMLAudioElement>();
  const [length, setLength] = useState(100);
  const [time, setTime] = useState(0);
  const [volume, setVolume] = useState(100); // 音量状态,范围是 0 到 100
  const t = useToast();
  const { isOpen, onOpen, onClose } = useDisclosure();

  useEffect(() => {
    if (!audio.current) {
      audio.current = new Audio();
      audio.current.addEventListener("durationchange", () => {
        setLength(audio.current!.duration);
      });
      audio.current.addEventListener("timeupdate", () => {
        setTime(audio.current!.currentTime);
      });
    }
    if (props.src === "") return;
    audio.current.src = props.src;
    if (props.playtime !== 0) audio.current.currentTime = props.playtime;
    audio.current.play().catch((e: DOMException) => {
      if (
        e.message ===
        "The play() request was interrupted because the media was removed from the document."
      )
        return;
      console.log(e);
      onOpen();
    });
  }, [props.src, props.playtime]);

  useEffect(() => {
    if (audio.current) {
      // 音量值从 0 到 100,映射到 0 到 1 的范围
      audio.current.volume = volume / 100;
    }
  }, [volume]);

  return (
    <>
      <Flex flexDirection={"row"} alignItems={"center"}>
        <Progress flex={12} height={"32px"} max={length} value={time} />
        <Text flex={2} textAlign={"center"}>{`${Math.floor(
          time
        )} / ${Math.floor(length)}`}</Text>
        <Tooltip hasArrow label="当音乐没有自动播放时,点我试试">
          <IconButton
            flex={1}
            aria-label={"Play"}
            mr={2}
            icon={
              <Icon viewBox="0 0 1024 1024">
                <path
                  d="M128 138.666667c0-47.232 33.322667-66.666667 74.176-43.562667l663.146667 374.954667c40.96 23.168 40.853333 60.8 0 83.882666L202.176 928.896C161.216 952.064 128 932.565333 128 885.333333v-746.666666z"
                  fill="#3D3D3D"
                  p-id="2949"
                ></path>
              </Icon>
            }
            onClick={() => {
              audio.current?.play();
              audio.current?.pause();
              props.reset();
            }}
          />
        </Tooltip>
        <Tooltip hasArrow label={"切歌"}>
          <IconButton
            flex={1}
            icon={<ArrowRightIcon />}
            aria-label={"切歌"}
            onClick={props.nextClick}
          />
        </Tooltip>
      </Flex>

      {/* 音量控制 */}
      <Flex alignItems="center" justifyContent="center" mt={4}>
        <Text mr={2}>音量</Text>
        <Slider
          flex={1}
          aria-label="音量"
          value={volume}
          onChange={(val) => setVolume(val)}
          min={0}
          max={100}
        >
          <SliderTrack>
            <SliderFilledTrack />
          </SliderTrack>
          <SliderThumb />
        </Slider>
        <Text ml={2}>{volume}%</Text>
      </Flex>

      <Modal isOpen={isOpen} onClose={onClose}>
        <ModalOverlay>
          <ModalContent>
            <ModalHeader fontSize={"lg"} fontWeight={"bold"}>
              Error
            </ModalHeader>
            <ModalBody>
              <Text>
                看起来你的浏览器不允许音频自动播放, 请点击下方的按钮来启用自动播放~
              </Text>
            </ModalBody>
            <ModalFooter>
              <Button
                colorScheme={"blue"}
                onClick={() => {
                  audio.current?.play();
                  props.reset();
                  onClose();
                }}
              >
                启用自动播放
              </Button>
            </ModalFooter>
          </ModalContent>
        </ModalOverlay>
      </Modal>
    </>
  );
};

问题

现在是构建遇到了问题

解决

npm run build

不要用下面这句,会报错,真是莫名其妙,耽误了好久,气死了
//next build