File size: 16,876 Bytes
00a912e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | # 目录结构
## 根目录
```
music-together/
├── packages/
│ ├── client/ # React 前端
│ ├── server/ # Node.js 后端
│ └── shared/ # 共享类型与常量
├── docs/ # 项目文档(含本文件 PROJECT_ARCHITECTURE.md)
├── package.json # 根 package(工作区编排)
├── pnpm-workspace.yaml # pnpm 工作区定义
├── pnpm-lock.yaml
├── README.md
└── .gitignore
```
## packages/client/src/ — 前端源码
```
src/
├── main.tsx # 入口:ReactDOM.createRoot
├── App.tsx # 根组件:Router + Provider + ErrorBoundary + Suspense 懒加载
├── index.css # 全局样式:Tailwind + 配色变量 + 自定义动画
│
├── pages/ # 页面级组件
│ ├── HomePage.tsx # 大厅:创建/加入房间、房间列表
│ ├── RoomPage.tsx # 房间:播放器 + 聊天(桌面侧栏/移动端 Drawer) + 覆盖层弹窗
│ └── NotFoundPage.tsx # 404 页面
│
├── components/ # UI 组件
│ ├── Chat/
│ │ ├── ChatMessage.tsx # 单条消息(用户/系统)
│ │ └── ChatPanel.tsx # 聊天面板(消息列表 + 输入框)
│ ├── Lobby/
│ │ ├── CreateRoomDialog.tsx # 创建房间弹窗
│ │ ├── NicknameDialog.tsx # 设置昵称弹窗
│ │ ├── PasswordDialog.tsx # 输入房间密码弹窗
│ │ ├── RoomCard.tsx # 房间列表卡片
│ │ ├── UserPopover.tsx # 用户信息气泡
│ │ ├── HeroSection.tsx # 首页 Hero 标题区域
│ │ ├── ActionCards.tsx # 创建/加入房间卡片
│ │ └── RoomListSection.tsx # 活跃房间列表区域
│ ├── TrackListItem.tsx # 共享曲目行渲染(序号+封面+标题VIP+歌手可点击+时长+isAdded 添加按钮,memo 优化)
│ ├── VirtualTrackList.tsx # 共享虚拟滚动曲目列表(@tanstack/react-virtual + 无限加载 + skeleton + 空态,forwardRef 暴露 scrollToTop)
│ ├── Overlays/
│ │ ├── QueueDrawer.tsx # 播放队列抽屉(vaul Drawer,移动端底部/桌面端右侧)
│ │ ├── SearchDialog.tsx # 音乐搜索弹窗(VirtualTrackList 虚拟滚动 + 自动无限加载 + AbortController 竞态防护)
│ │ ├── SettingsDialog.tsx # 设置弹窗(壳,Tab 导航:房间/成员/账号/个人/外观,移动端 nav scrollbar-hide)
│ │ └── Settings/
│ │ ├── SettingRow.tsx # 设置行共享组件
│ │ ├── RoomSettingsSection.tsx # 房间设置(名称、密码)
│ │ ├── MembersSection.tsx # 成员列表(角色管理)
│ │ ├── PlatformAuthSection.tsx # 平台账号认证(VIP Cookie,旧版,已被 PlatformHub 替代)
│ │ ├── PlatformHub.tsx # 平台中心(登录 + 歌单浏览 + 导入,替代 PlatformAuthSection)
│ │ ├── LoginSection.tsx # 精简版平台登录区域(PlatformHub 子组件)
│ │ ├── PlaylistSection.tsx # 歌单列表 + 手动输入(PlatformHub 子组件)
│ │ ├── PlaylistDetail.tsx # 歌单详情(header + VirtualTrackList + queueKeys/addedIds 去重 + 动态全部添加过滤重复)
│ │ ├── ProfileSettingsSection.tsx # 个人设置(昵称)
│ │ ├── AppearanceSection.tsx # 外观设置(歌词 + 背景 + 布局)
│ │ ├── OtherSettingsSection.tsx # 其他设置
│ │ ├── ManualCookieDialog.tsx # 手动输入 Cookie 弹窗
│ │ └── QrLoginDialog.tsx # 通用 QR 扫码登录弹窗(网易云 + 酷狗 + QQ 音乐)
│ ├── Player/
│ │ ├── constants.ts # 共享动画常量(SPRING / LAYOUT_TRANSITION),NowPlaying 和 SongInfoBar 统一导入
│ │ ├── AudioPlayer.tsx # 主播放器布局(桌面:左右分栏;移动:双模式封面/歌词切换)
│ │ ├── LyricDisplay.tsx # AMLL 歌词渲染(LRC 正则支持 [mm:ss] / [mm:ss.x] / [mm:ss.xx] / [mm:ss.xxx])
│ │ ├── NowPlaying.tsx # 当前曲目展示(支持 compact 小封面横排模式 + layoutId 共享动画)
│ │ ├── SongInfoBar.tsx # 歌曲信息栏(标题/艺术家 + 音量/聊天按钮,竖屏模式自适应缩放)
│ │ └── PlayerControls.tsx # 进度条+播放控制+播放模式切换
│ ├── Room/
│ │ └── RoomHeader.tsx # 房间头部(房间名/人数/连接状态;移动端 DropdownMenu 收纳设置/离开等操作)
│ ├── Vote/
│ │ └── VoteBanner.tsx # 投票横幅(进行中的投票显示 + 投票按钮)
│ ├── InteractionGate.tsx # 浏览器交互解锁(点击后才能播放音频)
│ └── ui/ # shadcn/ui 基础组件
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ ├── drawer.tsx
│ ├── dropdown-menu.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── popover.tsx
│ ├── resize-handle.tsx
│ ├── scroll-area.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── marquee-text.tsx
│ ├── responsive-dialog.tsx
│ ├── sheet.tsx
│ ├── skeleton.tsx
│ ├── slider.tsx
│ ├── switch.tsx
│ ├── tabs.tsx
│ └── tooltip.tsx
│
├── hooks/ # 自定义 Hooks
│ ├── useSocketEvent.ts # 通用 Socket 事件订阅工具 Hook(自动 on/off,ref 稳定)
│ ├── usePlayer.ts # 播放器主 hook(组合 useHowl + useLyric + usePlayerSync)
│ ├── useHowl.ts # Howler.js 音频实例管理
│ ├── useLyric.ts # 歌词加载(TTML → 平台逐词 YRC/KRC → LRC)
│ ├── usePlayerSync.ts # 播放同步(Scheduled Execution + conductor 上报 + 周期性漂移校正)
│ ├── useClockSync.ts # NTP 时钟同步 hook(校准客户端时钟与服务器对齐)
│ ├── useRoom.ts # 房间组合 hook(编排 5 个子 hook,对外 API 不变)
│ ├── room/ # useRoom 子 hook(按职责拆分)
│ │ ├── useRoomState.ts # ROOM_STATE / JOIN / LEFT / SETTINGS / ROLE_CHANGED / ERROR + 挂载时补发 cookie(覆盖 HomePage 提前消费 ROOM_STATE 的场景)
│ │ ├── useChatSync.ts # CHAT_HISTORY / CHAT_MESSAGE
│ │ ├── useQueueSync.ts # QUEUE_UPDATED
│ │ ├── useAuthSync.ts # AUTH_SET_COOKIE_RESULT + localStorage 持久化(验证失败只做 toast 反馈,永不删除 cookie;删除权仅在 useAuth.logout)
│ │ └── useConnectionGuard.ts # disconnect → resetAllRoomState
│ ├── useAuth.ts # 平台认证 UI & Socket 事件
│ ├── useVote.ts # 投票(使用 useSocketEvent)
│ ├── useChat.ts # 聊天消息收发
│ ├── useLobby.ts # 大厅房间列表与操作(使用 useSocketEvent)
│ ├── useQueue.ts # 播放队列操作(含 addBatchTracks 批量添加)
│ ├── usePlaylist.ts # 歌单管理(用户歌单列表、分页曲目获取 + 无限加载、URL 解析、批量导入)
│ ├── useIsMobile.ts # 布局维度:orientation 检测(portrait=竖屏布局,landscape=横屏布局)
│ ├── useHasHover.ts # 交互维度:hover 能力检测((hover: hover) 媒体查询,触控设备=false)
│ ├── useContainerPortrait.ts # 容器宽高比检测(ResizeObserver,用于播放器横竖屏切换)
│ └── useCoverWidth.ts # 封面容器最小尺寸测量(约束信息栏/控件宽度与封面对齐)
│
├── stores/ # Zustand 状态仓库
│ ├── playerStore.ts # 播放状态(currentTrack, isPlaying, volume 等)
│ ├── roomStore.ts # 房间状态(room, currentUser, users)
│ ├── chatStore.ts # 聊天(messages, unreadCount, isChatOpen)
│ ├── lobbyStore.ts # 大厅(rooms 列表, isLoading)
│ └── settingsStore.ts # 设置(歌词参数、背景参数,持久化到 localStorage)
│
├── providers/ # React Context Provider
│ ├── SocketProvider.tsx # Socket.IO 连接管理,提供 socket + isConnected + 断线/重连 Toast
│ └── AbilityProvider.tsx # CASL 权限上下文(基于 currentUser.role)
│
└── lib/ # 工具库
├── config.ts # 配置常量(SERVER_URL)
├── constants.ts # 命名常量(定时器、阈值、布局尺寸)
├── clockSync.ts # NTP 时钟同步引擎(采样、offset 计算、getServerTime)
├── resetStores.ts # 全局 store 重置工具
├── socket.ts # Socket.IO 客户端实例
├── storage.ts # localStorage 封装(带类型校验)
├── platform.ts # 平台常量(PLATFORM_LABELS / PLATFORM_SHORT_LABELS / PLATFORM_COLORS / VIP_LABELS / 状态查找函数)
├── format.ts # 格式化工具(时间、文本等)
├── audioUnlock.ts # 浏览器音频自动播放解锁
└── utils.ts # cn() + trackKey() 等通用工具
```
## packages/server/src/ — 后端源码
```
src/
├── index.ts # 入口:Express + HTTP + Socket.IO 服务启动与优雅关闭
├── config.ts # 环境变量配置(PORT, CLIENT_URL, CORS)
│
├── controllers/ # 控制器:注册 Socket 事件处理器(薄编排层,不含业务逻辑)
│ ├── index.ts # 统一注册入口
│ ├── roomController.ts # 房间生命周期(创建/加入/离开/发现/设置/角色)
│ ├── playerController.ts # 播放控制(play/pause/seek/next/prev/sync/set_mode)+ NTP ping/pong
│ ├── queueController.ts # 队列管理(add/remove/reorder/clear)
│ ├── chatController.ts # 聊天消息(含限流反馈)
│ ├── voteController.ts # 投票系统(发起/投票/超时/执行,支持 set-mode / play-track / remove-track 投票)
│ ├── authController.ts # 平台认证(QR 登录/Cookie 管理/状态查询;支持网易云/酷狗/QQ 音乐三平台;策略模式——通过 AUTH_PROVIDERS 映射表统一处理;fast path: 内存池命中跳过 API;slow path: getUserInfo + 任意失败重试 1 次)
│ └── playlistController.ts # 歌单管理(获取用户歌单列表 via Socket,使用 getUserCookie 取请求者自己的 cookie,歌单私有)
│
├── services/ # 服务层:业务逻辑
│ ├── roomService.ts # 房间 CRUD + 角色管理 + 加入校验(validateJoinRequest)
│ ├── roomLifecycleService.ts # 房间生命周期定时器(空置删除)+ 防抖广播
│ ├── playerService.ts # 播放状态管理 + 流 URL 解析 + 切歌防抖 + 加入播放同步
│ ├── queueService.ts # 队列操作(reorder 保留未包含曲目防丢歌,getNextTrack 支持 4 种播放模式,clearQueue 清空,addBatchTracks 批量添加)
│ ├── chatService.ts # 聊天消息处理 + HTML 转义(含系统消息)
│ ├── syncService.ts # 播放位置估算工具(estimateCurrentTime)
│ ├── musicProvider.ts # 音乐数据聚合(3 层引用式 LRU 缓存 + 外部 API 超时保护 + 歌单分页获取;Netease 歌单使用 ncmApi.playlist_track_all 分块请求突破 1000 首限制,Kugou 用户歌单使用原生 API (get_other_list_file_nofilt) + Meting fallback,Tencent 使用 Meting 原始模式保留 VIP/时长字段)
│ ├── authService.ts # Cookie 池管理(房间级作用域;getAnyCookie 用于 VIP 播放共享,getUserCookie 用于歌单等用户私有操作)
│ ├── authProvider.ts # 统一认证接口(AuthProvider 接口定义 + GetUserInfoResult/UserInfoData 共享类型 + AUTH_PROVIDERS 策略映射表)
│ ├── neteaseAuthService.ts # 网易云 API 认证(QR / Cookie 验证 / 用户信息 / 用户歌单列表;getUserInfo 返回 { ok, data? } | { ok: false, reason: 'expired' | 'error' } 区分过期与临时故障)
│ ├── kugouAuthService.ts # 酷狗 API 认证(QR 扫码登录 + VIP 检查 + 用户昵称(RSA) + 用户歌单列表 + 歌单歌曲获取;kugouRequest 含 HTTP 状态检查与 JSON 安全解析;自包含签名实现,状态码归一化为 800-803 与网易云统一)
│ ├── tencentAuthService.ts # QQ 音乐认证(5 步 OAuth QR 扫码登录:ptqrshow/ptqrlogin/check_sig/authorize/QQLogin 换取 musickey;zzc 签名防风控;getUserInfo 获取昵称 + VIP 状态;getUserPlaylists 获取自建 + 收藏歌单;getPlaylistTracks 分页获取歌单歌曲)
│ └── voteService.ts # 投票状态管理
│
├── repositories/ # 数据仓库:内存状态与 SQLite 持久化
│ ├── types.ts # 接口定义(RoomRepository, ChatRepository)
│ ├── roomRepository.ts # 房间数据 + Socket 映射 + per-socket RTT + roomToSockets 反向索引(Map<string, RoomData>)
│ ├── chatRepository.ts # 聊天记录(内存最多 200 条;永久房间同步持久化到 SQLite)
│ └── audioProxyPolicyRepository.ts # 全局酷狗音频代理策略(SQLite server_settings)
│
├── middleware/ # Socket.IO 中间件
│ ├── types.ts # TypedServer, TypedSocket, HandlerContext
│ ├── withRoom.ts # 房间成员身份校验
│ ├── withControl.ts # 操作权限校验(包装 withRoom)
│ └── socketRateLimiter.ts # Socket 事件速率限制(per-socket,10次/5秒)+ 断连清理(cleanupSocketRateLimit)
│
├── routes/ # Express REST 路由
│ ├── music.ts # GET /api/music/search|url|lyric|cover|playlist|ttml(统一 validated() 路由包装器消除重复 try/catch + Zod 模式)
│ └── rooms.ts # GET /api/rooms/:roomId/check(房间预检)
│
├── types/
│ └── meting.d.ts # @meting/core 类型声明
│
└── utils/
├── logger.ts # 结构化日志(基于 pino,info/warn/error + JSON context)
└── roomUtils.ts # 房间数据转换纯函数(toPublicRoomState / toPublicRoomStateForOwner,密码仅 owner 可见)
```
## packages/shared/src/ — 共享代码
```
src/
├── index.ts # 统一导出(re-export 所有模块)
├── types.ts # 核心类型:ERROR_CODE, Track, RoomState, PlayState, ScheduledPlayState, PlayMode, AudioQuality, User, ChatMessage, VoteAction (incl. play-track, remove-track), VoteState, RoomListItem, Playlist
├── events.ts # 事件常量:EVENTS 对象(room:*, player:*, queue:*, chat:*, auth:*, ntp:*, playlist:*)
├── socket-types.ts # Socket.IO 类型:ServerToClientEvents, ClientToServerEvents
├── constants.ts # 业务常量:LIMITS(长度/数量限制), TIMING(同步间隔/宽限期), NTP(时钟同步参数), QR_STATUS(扫码状态码), QR_TIMING(轮询间隔)
├── schemas.ts # Zod 验证 schema
└── abilities.ts # CASL 权限定义(Actions incl. set-mode, Subjects, defineAbilityFor)
```
---
|