1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2025-12-06 14:52:50 +08:00

Fixes #399 (#415) 修复tailwind主题

* 引入tailwindcss, 更新默认主题ui

* 引入tailwindcss, 添加新的主题ui

* 引入tailwindcss, 添加新的主题ui

* 更新tailwind主题

* fix merge error

* fix merge error

* feat: Enhance song metadata display and API fetching

- Add more detailed song metadata in frontend (title, artist, album, year, genre, lyrics)
- Update API call to include music tags
- Replace default music icon with song cover image
- Improve type hinting in httpserver.py

* feat: Add keyboard shortcuts for music playback control

- Implement keyboard shortcuts for play/pause (Space), previous/next track (Left/Right arrows)
- Add volume control via Up/Down arrow keys
- Prevent default browser actions for shortcut keys
- Add event listener for keydown and remove on component unmount
- Enhance user interaction with music player

* feat: Improve song metadata handling and error resilience

- Enhance current song information retrieval with more robust error handling
- Add fallback values for song metadata when API calls fail
- Update current song state with additional properties like cover image
- Optimize song loading process with better error management
- Ensure consistent song information display even with incomplete data

* fix bugs

* tailwind fix bugs

* Merge branch 'main' of github.com:xiaomusic

* Merge branch 'main' of github.com:xiaomusic

* 修复刮削歌曲的歌曲名称和文件名称不一致,播放失败的问题
This commit is contained in:
jhcj.z
2025-03-04 18:35:05 +08:00
committed by GitHub
parent 57a1fb5ed4
commit 61bf233704
8 changed files with 1290 additions and 102 deletions

191
docs/issues/378.md Normal file
View File

@@ -0,0 +1,191 @@
---
title: 求助:如何自动或口令刷新本地音乐列表?
---
# 求助:如何自动或口令刷新本地音乐列表?
docker容器部署的xiaomusic目前是[0.3.74]最新版音响是小米AI音箱二代。目前可以正常通过口令 “小爱同学,播放本地音乐 XXX”来 播放容器外挂载的目录 /music 中的mp3、flac等音乐歌曲。
但是,每次往 /music 里面新拷入一些音乐之后xiaomusic 既没有自动刷新本地音乐列表,也不能通过“小爱同学,刷新列表”、“小爱同学,本地音乐 刷新列表”、“小爱同学,刷新本地列表”、“小爱同学,刷新本地音乐列表”等口令来刷新列表,必须在电脑上打开 xiaomusic 的后台管理页面并点一下那个刷新列表的按钮,然后本地音乐列表中才能刷新看见新增的音乐。
请问有没有办法做到每次往 /music 拷入音乐后自动刷新列表,或者通过口令刷新列表?谢谢~
## 评论
### 评论 1 - hanxi
刷新列表口令默认不在唤醒口令里,你可以先说播放歌曲,在播放中再说刷新列表,或者把刷新列表口令加到唤醒口令里。
`允许唤醒的命令:`
---
### 评论 2 - foxfire881
> 刷新列表口令默认不在唤醒口令里,你可以先说播放歌曲,在播放中再说刷新列表,或者把刷新列表口令加到唤醒口令里。
>
> `允许唤醒的命令:`
每次都要这样操作很麻烦欸~
注意到xiaomusic是用python写的python有个可以监控文件系统变化的库 watchdog 使用很方便,可否增加一个自动刷新列表的功能?当监控到音乐目录发生变化时(增、删、改)自动刷新xiaomusic的音乐列表 —— 这样连口令都不用说了xiaomusic自动实时刷新列表更方便了期待~~ @hanxi
---
### 评论 3 - foxfire881
豆包给了个demo
```python
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
class MyEventHandler(FileSystemEventHandler):
def on_modified(self, event):
print(f"File modified: {event.src_path}")
def on_created(self, event):
print(f"File created: {event.src_path}")
def on_deleted(self, event):
print(f"File deleted: {event.src_path}")
if __name__ == "__main__":
event_handler = MyEventHandler()
observer = Observer()
path = '.' # 要监控的目录
observer.schedule(event_handler, path, recursive=True)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
```
代码解释
- 定义一个继承自 FileSystemEventHandler 的类 MyEventHandler并重写 on_modified、on_created 和 on_deleted 方法,用于处理文件修改、创建和删除事件。
- 创建一个 Observer 对象,并将事件处理程序和要监控的目录传递给它。
- 启动观察者,并进入一个无限循环,直到用户按下 Ctrl+C 停止程序。
watchdog 库的优点是实时性好,能够及时响应文件系统的变化,并且支持多种操作系统。因此,推荐使用 watchdog 库来监控文件目录变化。
---
### 评论 4 - hanxi
@foxfire881 谢谢,有空我实现一下吧。需要做个队列延迟处理的,要不然拷贝一堆文件的时候会刷新多次。
---
### 评论 5 - foxfire881
刷新多次好像也影响不大xiaomusic刷新列表挺快的我的音乐目录有大概有100G左右近5000个mp3、flac音乐几乎是秒刷点一下按钮瞬间就更新了。
所以是不是也可以另外开个线程在后台定时每隔一段时间(用户可设置)自动刷新一下列表,这样实现快速、简单点。
队列方案更完美,实现也更复杂些,可以作远期目标持续优化。
---
### 评论 6 - foxfire881
@hanxi 另外还有一个问题想请教下如何解决?
一首歌曲往往有多个版本,例如王菲、梁静茹都唱过《红豆》:
/music/王菲/红豆.mp3
/music/梁静茹/红豆.mp3
/music/张惠妹/红豆生南国.mp3
/music/其他/张学友-红豆.mp3
如果我想听梁静茹的《红豆》,应该说什么指令?
“小爱同学,播放本地歌曲 红豆” —— 这个指令好像只能定位到第一个匹配到的文件,后面的文件都忽略了。
“小爱同学,播放本地歌曲 梁静茹 红豆” —— 这个指令无效,小爱同学没有反应。
---
### 评论 7 - hanxi
> [@hanxi](https://github.com/hanxi) 另外还有一个问题想请教下如何解决?
>
> 一首歌曲往往有多个版本,例如王菲、梁静茹都唱过《红豆》:
>
> /music/王菲/红豆.mp3 /music/梁静茹/红豆.mp3 /music/张惠妹/红豆生南国.mp3 /music/其他/张学友-红豆.mp3
>
> 如果我想听梁静茹的《红豆》,应该说什么指令? “小爱同学,播放本地歌曲 红豆” —— 这个指令好像只能定位到第一个匹配到的文件,后面的文件都忽略了。
>
> “小爱同学,播放本地歌曲 梁静茹 红豆” —— 这个指令无效,小爱同学没有反应。
文件名需要唯一,像张学友-红豆一样命名。然后说张学友红豆就行。相同的文件名是会覆盖的,只有一首生效。
---
### 评论 8 - foxfire881
> > [@hanxi](https://github.com/hanxi) 另外还有一个问题想请教下如何解决?
> > 一首歌曲往往有多个版本,例如王菲、梁静茹都唱过《红豆》:
> > /music/王菲/红豆.mp3 /music/梁静茹/红豆.mp3 /music/张惠妹/红豆生南国.mp3 /music/其他/张学友-红豆.mp3
> > 如果我想听梁静茹的《红豆》,应该说什么指令? “小爱同学,播放本地歌曲 红豆” —— 这个指令好像只能定位到第一个匹配到的文件,后面的文件都忽略了。
> > “小爱同学,播放本地歌曲 梁静茹 红豆” —— 这个指令无效,小爱同学没有反应。
>
> 文件名需要唯一,像张学友-红豆一样命名。然后说张学友红豆就行。相同的文件名是会覆盖的,只有一首生效。
原来如此。改文件名有点麻烦,能否做到把关键字搜出来的结果作为一个播放列表顺序播放?例如 “小爱同学,播放本地音乐 红豆”,然后 xiaomusic 把上述所有包含 “红豆” 关键字的歌曲全部搜索出来,作为一个临时播放列表顺序播放?
---
### 评论 9 - hanxi
可以用 music tag web 工具自动改名。不能同名是最早的基础设定,改动比较大,现在不好改了。
---
### 评论 10 - foxfire881
> 可以用 music tag web 工具自动改名。不能同名是最早的基础设定,改动比较大,现在不好改了。
关键是网上下载的很多mp3、flac文件没有 tag 或者 tag 不规范,要重命名得先把 tag 整理一遍,那个工程量就太太太大了
没关系,先这样用着吧,不着急。以后有空了还是希望可以重构一下,解决同名文件的问题。
或者也可以考虑下不一定要严格按照文件名去匹配,可以把文件路径、目录名也包含在模糊匹配规则里面,这样也可以大大提高命中准确率。
---
### 评论 11 - AisukaYuki
其实你可以用定时任务定时刷新列表。暂时先实现一下,隔几分钟刷新一下。
```
[
{
"expression": "*/10 * * * *",
"name": "refresh_music_list"
}
]
```
---
### 评论 12 - newmanfung
> > 可以用 music tag web 工具自动改名。不能同名是最早的基础设定,改动比较大,现在不好改了。
>
> 关键是网上下载的很多mp3、flac文件没有 tag 或者 tag 不规范,要重命名得先把 tag 整理一遍,那个工程量就太太太大了
>
> 没关系,先这样用着吧,不着急。以后有空了还是希望可以重构一下,解决同名文件的问题。
>
> 或者也可以考虑下不一定要严格按照文件名去匹配,可以把文件路径、目录名也包含在模糊匹配规则里面,这样也可以大大提高命中准确率。
用mp3tag通过网上的的歌曲meta数据自动更改tag的之前几十张专辑的时候大概3个小时就搞定了。现在我都弄了几百张专辑了。
---
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/378)

View File

@@ -27,7 +27,7 @@ title: 微信交流群二维码
### 评论 3 - hanxi
![mm_reward_qrcode_1726365700471](https://gproxy.hanxi.cc/proxy/user-attachments/assets/7863e361-7e61-48a7-bd71-8f8f609f11b4)
![mm_reward_qrcode_1726365700471](https://gproxy.hanxi.cc/proxy/user-attachments/assets/7863e361-7e61-48a7-bd71-8f8f609f11b4)
---

View File

@@ -3,4 +3,4 @@
if __name__ == "__main__":
from xiaomusic.cli import main
main()
main()

View File

@@ -8,10 +8,13 @@ import tempfile
import urllib.parse
from contextlib import asynccontextmanager
from dataclasses import asdict
from typing import TYPE_CHECKING, Annotated
from typing import TYPE_CHECKING, TYPE_CHECKING, Annotated
import socketio
if TYPE_CHECKING:
from xiaomusic.xiaomusic import XiaoMusic
if TYPE_CHECKING:
from xiaomusic.xiaomusic import XiaoMusic

View File

@@ -29,7 +29,10 @@ const API = {
// 获取当前播放状态
async getPlayingStatus(did = 'web_device') {
const response = await fetch(`/playingmusic?did=${did}`);
return response.json();
const data = await response.json();
localStorage.setItem('cur_music', data.cur_music);
localStorage.setItem('cur_playlist', data.cur_playlist);
return data;
},
// 播放歌单中的歌曲

View File

@@ -132,7 +132,7 @@
<!-- 系统歌单 -->
<ul class="menu gap-1 mb-4">
<li v-for="playlist in systemPlaylists" :key="playlist.id">
<a class="flex justify-between" :class="{ active: currentPlaylist === playlist.name }"
<a class="flex justify-between" :class="{ active: curSelectPlaylist === playlist.name }"
@click="selectPlaylist(playlist.name)">
{{ playlist.name }}
<div class="badge badge-sm">{{ playlist.count }}</div>
@@ -144,7 +144,7 @@
<div class="divider">自定义歌单</div>
<ul class="menu gap-1">
<li v-for="playlist in customPlaylists" :key="playlist.name">
<a class="flex justify-between" :class="{ active: currentPlaylist === playlist.name }"
<a class="flex justify-between" :class="{ active: curSelectPlaylist === playlist.name }"
@click="selectPlaylist(playlist.name)">
<span class="truncate flex-1">{{ playlist.name }}</span>
<div class="badge badge-sm">{{ playlist.count }}</div>
@@ -157,7 +157,7 @@
<!-- 右侧歌曲列表 -->
<div class="flex-1 bg-base-100 rounded-box p-4 shadow-lg overflow-hidden">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">{{ currentPlaylist }}</h2>
<h2 class="text-xl font-bold">{{ curSelectPlaylist }}</h2>
</div>
<div class="h-[calc(100vh-240px)] overflow-y-auto">
<table class="table w-full">
@@ -258,14 +258,16 @@
</div>
</a>
</div>
<div class="cursor-pointer hover:text-primary transition-colors">
<div class="font-bold">{{ currentSong?.title }}</div>
<div class="text-sm opacity-50">
{{ currentSong?.artist }} - {{ currentSong?.album }}
</div>
<div class="text-sm opacity-50">
{{ currentPlaylist }}
</div>
<div class="cursor-pointer hover:text-primary transition-colors" class="cursor-pointer hover:text-primary transition-colors">
<a href="./now_playing.html" target="_blank">
<div class="font-bold">{{ currentSong?.title }}</div>
<div class="text-sm opacity-50">
{{ currentSong?.artist }} - {{ currentSong?.album }}
</div>
<div class="text-sm opacity-50">
{{ currentPlaylist }}
</div>
</a>
</div>
<button class="btn btn-ghost btn-circle btn-sm"
@click="currentSong && toggleFavorite(currentSong)"
@@ -371,10 +373,11 @@
const currentTime = ref(0);
const duration = ref(0);
const volume = ref(80);
const currentPlaylist = ref(localStorage.getItem('cur_playlist') || '所有歌曲');
const curSelectPlaylist = ref('所有歌曲');
const systemPlaylists = ref([]);
const customPlaylists = ref([]);
const newPlaylistName = ref('');
const currentPlaylist = ref(localStorage.getItem('cur_playlist') || '所有歌曲');
// 添加音频播放相关状态
const audioPlayer = ref(null);
@@ -458,10 +461,10 @@
// 优化歌单切换
const selectPlaylist = async (name) => {
try {
if (name === currentPlaylist.value) return;
if (name === curSelectPlaylist.value) return;
currentPlaylist.value = name;
localStorage.setItem('cur_playlist', name);
curSelectPlaylist.value = name;
localStorage.setItem('curSelectPlaylist', name);
// 获取歌单中的歌曲
let songNames = [];
@@ -481,12 +484,14 @@
if (window.did === 'web_device') return;
const data = await API.getPlayingStatus(window.did);
if (data.ret === 'OK') {
isPlaying.value = data.is_playing;
currentTime.value = data.offset || 0;
duration.value = data.duration || 0;
currentPlaylist.value = data.cur_playlist;
if (data.cur_music && data.cur_music !== currentSong.value?.title) {
try {
// 获取音乐详细信息
@@ -516,6 +521,7 @@
localStorage.setItem('cur_music', data.cur_music);
localStorage.setItem('is_playing', data.is_playing);
localStorage.setItem('cur_playlist', data.cur_playlist);
}
}
} catch (error) {
@@ -530,7 +536,7 @@
localStorage.setItem('theme', theme);
// 更新 isDarkTheme 状态,用于图标显示
isDarkTheme.value = ['dark', 'black', 'luxury', 'dracula', 'night', 'coffee'].includes(theme);
showNotification(`已切换到 ${theme} 主题`, 'alert-success');
showMessage(`已切换到 ${theme} 主题`, 'alert-success');
};
const filteredSongs = computed(() => {
@@ -770,24 +776,74 @@
// 音频播放控制方法
const initAudioPlayer = async () => {
try {
// 创建音频播放器
// 检查是否已经存在音频播放器
let existingPlayer = document.getElementById('audio-player');
if (existingPlayer) {
document.body.removeChild(existingPlayer);
}
// 创建新的音频播放器
const audio = document.createElement('audio');
audio.id = 'audio-player';
// 设置音频属性
audio.preload = 'auto'; // 预加载
audio.crossOrigin = 'anonymous'; // 允许跨域
// 添加到文档
document.body.appendChild(audio);
audioPlayer.value = audio;
// 监听错误事件
audio.addEventListener('error', (e) => {
console.error('Audio playback error:', e);
const error = e.target.error;
let errorMessage = '播放出错';
if (error) {
switch (error.code) {
case error.MEDIA_ERR_ABORTED:
errorMessage = '播放被中断';
break;
case error.MEDIA_ERR_NETWORK:
errorMessage = '网络错误';
break;
case error.MEDIA_ERR_DECODE:
errorMessage = '解码错误';
break;
case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
errorMessage = '不支持的音频格式';
break;
}
}
showMessage(errorMessage, 'alert-error');
isPlaying.value = false;
});
// 监听播放状态变化
audio.addEventListener('play', () => {
isPlaying.value = true;
localStorage.setItem('is_playing', 'true');
});
audio.addEventListener('pause', () => {
isPlaying.value = false;
localStorage.setItem('is_playing', 'false');
});
// 监听播放进度
audio.addEventListener('timeupdate', () => {
currentTime.value = audio.currentTime;
duration.value = audio.duration;
// 保存播放进度到localStorage
localStorage.setItem('current_time', audio.currentTime.toString());
});
// 监听播放状态
audio.addEventListener('play', () => {
isPlaying.value = true;
});
audio.addEventListener('pause', () => {
isPlaying.value = false;
// 监听元数据加载
audio.addEventListener('loadedmetadata', () => {
duration.value = audio.duration;
localStorage.setItem('duration', audio.duration.toString());
});
// 监听播放结束
@@ -799,23 +855,31 @@
}
});
// 监听加载元数据
audio.addEventListener('loadedmetadata', () => {
duration.value = audio.duration;
});
// 监听错误
audio.addEventListener('error', (e) => {
console.error('Audio playback error:', e);
showNotification('播放出错,请重试', 'alert-error');
});
audioPlayer.value = audio;
// 设置初始音量
audio.volume = volume.value / 100;
// 恢复上次的播放状态
const lastPlayingState = localStorage.getItem('is_playing') === 'true';
const lastCurrentTime = parseFloat(localStorage.getItem('current_time') || '0');
const lastSong = localStorage.getItem('cur_music');
if (lastSong && lastPlayingState) {
try {
const musicInfo = await API.getMusicInfo(lastSong);
if (musicInfo && musicInfo.ret === 'OK' && musicInfo.url) {
audio.src = musicInfo.url;
audio.currentTime = lastCurrentTime;
if (lastPlayingState) {
await audio.play();
}
}
} catch (error) {
console.error('Error restoring last playing state:', error);
}
}
} catch (error) {
console.error('Error initializing audio player:', error);
showMessage('初始化音频播放器失败', 'alert-error');
}
};
@@ -823,57 +887,125 @@
try {
if (!song || !song.title) {
console.error('Invalid song object:', song);
showMessage('无效的歌曲信息', 'alert-error');
return;
}
const currentPlaylistName = localStorage.getItem("cur_playlist") || "所有歌曲";
console.log('Playing song:', song.title, 'from playlist:', currentPlaylistName);
const currentPlaylist = curSelectPlaylist.value;
console.log('did', window.did, 'Playing song:', song.id, 'from playlist:', currentPlaylist);
// song.id 修复刮削歌曲的歌曲名称和文件名称不一致的问题
if (window.did === 'web_device') {
// Web播放模式
const musicInfo = await API.getMusicInfo(song.title);
if (!musicInfo || musicInfo.ret !== 'OK') {
console.error('Failed to get music info:', musicInfo);
return;
}
try {
const musicInfo = await API.getMusicInfo(song.id);
if (!musicInfo || musicInfo.ret !== 'OK') {
console.error('Failed to get music info:', musicInfo);
showMessage('获取歌曲信息失败', 'alert-error');
return;
}
if (audioPlayer.value) {
if (audioPlayer.value.src === musicInfo.url) {
// 同一首歌,切换播放状态
if (audioPlayer.value.paused) {
await audioPlayer.value.play();
isPlaying.value = true;
} else {
audioPlayer.value.pause();
if (!musicInfo.url) {
console.error('No URL in music info:', musicInfo);
showMessage('歌曲URL无效', 'alert-error');
return;
}
// 验证URL是否有效
const validUrl = new URL(musicInfo.url);
if (!validUrl.pathname.endsWith('.mp3')) {
console.error('Invalid music URL format:', validUrl);
showMessage('音乐文件格式不支持', 'alert-error');
return;
}
if (audioPlayer.value) {
try {
if (audioPlayer.value.src === musicInfo.url) {
// 同一首歌,切换播放状态
if (audioPlayer.value.paused) {
await audioPlayer.value.play();
isPlaying.value = true;
showMessage('继续播放');
} else {
audioPlayer.value.pause();
isPlaying.value = false;
showMessage('暂停播放');
}
} else {
// 播放新歌曲
audioPlayer.value.src = musicInfo.url;
await audioPlayer.value.play();
isPlaying.value = true;
showMessage('开始播放新歌曲');
}
// 更新当前歌曲和播放状态
currentSong.value = song;
localStorage.setItem("cur_music", song.id);
localStorage.setItem("cur_playlist", currentPlaylist);
localStorage.setItem("is_playing", "true");
// 高亮当前播放的歌曲
songs.value = songs.value.map(s => ({
...s,
isPlaying: s.id === song.id
}));
} catch (playError) {
console.error('Error playing audio:', playError);
showMessage('播放失败: ' + playError.message, 'alert-error');
// 重置播放状态
isPlaying.value = false;
localStorage.setItem("is_playing", "false");
}
} else {
// 播放新歌曲
audioPlayer.value.src = musicInfo.url;
await audioPlayer.value.play();
isPlaying.value = true;
console.error('Audio player not initialized');
showMessage('音频播放器未初始化', 'alert-error');
}
} catch (error) {
console.error('Error in web playback:', error);
showMessage('播放出错: ' + error.message, 'alert-error');
}
} else {
// 设备播放模式
const response = await API.playMusicFromList(window.did, currentPlaylistName, song.title);
if (response.ret === 'OK') {
isPlaying.value = true;
try {
// 如果是当前正在播放的歌曲,则切换播放状态
if (currentSong.value && song.id === currentSong.value.id) {
if (isPlaying.value) {
const response = await API.sendCommand(window.did, API.commands.PLAY_PAUSE);
if (response.ret === 'OK') {
isPlaying.value = false;
showMessage('暂停播放');
}
} else {
const response = await API.playMusicFromList(window.did, currentPlaylist, song.id);
if (response.ret === 'OK') {
isPlaying.value = true;
showMessage('继续播放');
}
}
} else {
// 播放新的歌曲
const response = await API.playMusicFromList(window.did, currentPlaylist, song.id);
if (response.ret === 'OK') {
isPlaying.value = true;
currentSong.value = song;
localStorage.setItem("cur_music", song.id);
localStorage.setItem("cur_playlist", currentPlaylist);
localStorage.setItem("is_playing", "true");
showMessage('开始播放');
} else {
console.error('Device playback failed:', response);
showMessage('设备播放失败', 'alert-error');
}
}
} catch (error) {
console.error('Error in device playback:', error);
showMessage('设备播放出错: ' + error.message, 'alert-error');
}
}
// 更新当前歌曲和播放状态
currentSong.value = song;
localStorage.setItem("cur_music", song.title);
// 高亮当前播放的歌曲
songs.value = songs.value.map(s => ({
...s,
isPlaying: s.id === song.id
}));
} catch (error) {
console.error('Error playing song:', error);
showNotification('播放失败,请重试', 'alert-error');
console.error('Error in playSong:', error);
showMessage('播放失败,请重试', 'alert-error');
}
};
@@ -891,15 +1023,38 @@
}
}
} else {
// 设备播放模式
const response = await API.sendCommand(window.did, isPlaying.value ? "暂停播放" : "继续播放");
if (response.ret === 'OK') {
isPlaying.value = !isPlaying.value;
try {
if (isPlaying.value) {
// 如果正在播放,则暂停
const response = await API.sendCommand(window.did, API.commands.PLAY_PAUSE)
if (response.ret === 'OK') {
isPlaying.value = false
showMessage('小爱同学: 暂停播放')
}
} else {
// 如果当前是暂停状态,获取当前歌曲信息并重新播放
const status = await API.getPlayingStatus(window.did)
if (status.ret === 'OK' && status.cur_music && status.cur_playlist) {
// 使用 playmusiclist 接口重新播放当前歌曲
const response = await API.playMusicFromList(window.did, status.cur_playlist, status.cur_music)
if (response.ret === 'OK') {
isPlaying.value = true
showMessage('小爱同学: 开始播放')
} else {
showMessage('小爱同学: 播放失败', 'error')
}
} else {
showMessage('小爱同学: 获取播放信息失败', 'error')
}
}
} catch (error) {
console.error('Error toggling play state:', error)
showMessage('小爱同学: 播放控制失败', 'error')
}
}
} catch (error) {
console.error('Error toggling play state:', error);
showNotification('播放控制失败,请重试', 'alert-error');
showMessage('播放控制失败,请重试' + window.did, 'alert-error');
}
};
@@ -963,7 +1118,7 @@
}
} catch (error) {
console.error('Error playing previous song:', error);
showNotification('切换上一首失败,请重试', 'alert-error');
showMessage('切换上一首失败,请重试', 'alert-error');
}
};
@@ -987,7 +1142,7 @@
}
} catch (error) {
console.error('Error playing next song:', error);
showNotification('切换下一首失败,请重试', 'alert-error');
showMessage('切换下一首失败,请重试', 'alert-error');
}
};
@@ -1000,19 +1155,19 @@
// Web播放模式下直接更新
currentPlayMode.value = newMode;
localStorage.setItem('play_mode', newMode.toString());
showNotification(`已切换到${playModes.value[newMode].cmd}模式`, 'alert-info');
showMessage(`已切换到${playModes.value[newMode].cmd}模式`, 'alert-info');
} else {
// 设备播放模式
const response = await API.sendCommand(window.did, playModes.value[newMode].cmd);
if (response.ret === 'OK') {
currentPlayMode.value = newMode;
localStorage.setItem('play_mode', newMode.toString());
showNotification(`已切换到${playModes.value[newMode].cmd}模式`, 'alert-info');
showMessage(`已切换到${playModes.value[newMode].cmd}模式`, 'alert-info');
}
}
} catch (error) {
console.error('Error toggling play mode:', error);
showNotification('切换播放模式失败,请重试', 'alert-error');
showMessage('切换播放模式失败,请重试', 'alert-error');
}
};
@@ -1075,17 +1230,17 @@
// 修改 toggleFavorite 方法
const toggleFavorite = async (song) => {
try {
const isLiked = favoriteList.value.includes(song.title);
const isLiked = favoriteList.value.includes(song.id);
const cmd = isLiked ? "取消收藏" : "加入收藏";
localStorage.setItem("cur_music", song.title);
localStorage.setItem("cur_music", song.id);
const response = await API.sendCommand(window.did, cmd);
if (response.ret === 'OK') {
if (isLiked) {
favoriteList.value = favoriteList.value.filter(name => name !== song.title);
favoriteList.value = favoriteList.value.filter(name => name !== song.id);
} else {
favoriteList.value.push(song.title);
favoriteList.value.push(song.id);
}
if (currentPlaylist.value === '收藏') {
@@ -1103,7 +1258,7 @@
const toastType = ref('alert-info');
// 添加显示 toast 的方法
const showNotification = (message, type = 'alert-info') => {
const showMessage = (message, type = 'alert-info') => {
toastMessage.value = message;
toastType.value = type;
showToast.value = true;
@@ -1116,7 +1271,7 @@
// 修改 deleteMusic 方法
const deleteMusic = async (song) => {
try {
if (!confirm(`确定要删除歌曲"${song.title}"吗?`)) {
if (!confirm(`确定要删除歌曲"${song.id}"吗?`)) {
return;
}
@@ -1125,35 +1280,35 @@
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: song.title })
body: JSON.stringify({ name: song.id })
});
const data = await response.json();
if (data === 'success' || data.ret === 'OK') {
// 如果删除的是当前播放的歌曲,切换到下一首
if (currentSong.value?.title === song.title) {
if (currentSong.value?.id === song.id) {
await playNext();
}
// 从当前列表中移除歌曲
songs.value = songs.value.filter(s => s.title !== song.title);
songs.value = songs.value.filter(s => s.id !== song.id);
// 如果在收藏列表中,也从收藏列表中移除
if (favoriteList.value.includes(song.title)) {
favoriteList.value = favoriteList.value.filter(name => name !== song.title);
if (favoriteList.value.includes(song.id)) {
favoriteList.value = favoriteList.value.filter(name => name !== song.id);
}
// 刷新歌单信息
await loadPlaylists();
// 显示成功通知
showNotification(`删除歌曲"${song.title}"成功`, 'alert-success');
showMessage(`删除歌曲"${song.id}"成功`, 'alert-success');
} else {
throw new Error(typeof data === 'string' ? data : (data.msg || '删除失败'));
}
} catch (error) {
console.error('Error deleting song:', error);
showNotification(`删除失败: ${error.message}`, 'alert-error');
showMessage(`删除失败: ${error.message}`, 'alert-error');
}
};
@@ -1226,10 +1381,10 @@
currentTime.value = 0;
}
}
showNotification('已发送停止播放的请求', 'alert-info');
showMessage('已发送停止播放的请求', 'alert-info');
} catch (error) {
console.error('Error stopping playback:', error);
showNotification('停止播放失败,请重试', 'alert-error');
showMessage('停止播放失败,请重试', 'alert-error');
}
};
@@ -1352,6 +1507,7 @@
playNext,
togglePlayMode,
playSong,
curSelectPlaylist,
currentPlaylist,
systemPlaylists,
customPlaylists,

View File

@@ -13,6 +13,276 @@
<script src="./api.js"></script>
</head>
<body class="font-sans">
<div id="app" class="h-screen flex flex-col overflow-hidden">
<!-- Toast 提示 -->
<div class="toast toast-top toast-center z-[9999]">
<div v-if="showToast" :class="['alert', toastType]">
<span>{{ toastMessage }}</span>
</div>
</div>
<!-- 错误提示 -->
<div v-if="error" class="fixed top-4 right-4 z-50 alert alert-error shadow-lg max-w-sm">
<span class="material-icons">error</span>
<span>{{ error }}</span>
</div>
<!-- 背景图 -->
<div class="fixed inset-0 bg-cover bg-center transition-all duration-1000 ease-in-out"
:style="{ backgroundImage: `url(${currentSong.cover})` }"></div>
<div class="fixed inset-0 backdrop-blur-2xl bg-black/40"></div>
<!-- 主内容区 -->
<div class="relative z-10 flex flex-col h-full">
<!-- 添加一个返回按钮 在页面左侧顶部-->
<a class="btn btn-sm btn-ghost text-white/70 hover:text-white fixed top-4 left-4" href="./index.html">
<span class="material-icons text-lg">arrow_back</span>
</a>
<!-- 顶部信息 -->
<div class="text-center p-6">
<h2 class="text-3xl font-bold text-white mb-1 tracking-wide">{{ currentSong.title }}</h2>
<p class="text-sm text-white/40 mb-1">{{ currentSong.cur_playlist }}</p>
<p class="text-l text-white/40 mb-1">{{ currentSong.artist }} - {{ currentSong.album }}</p>
<!-- 标签显示 -->
<div v-if="currentSong.tags" class="flex justify-center gap-2 mt-2 flex-wrap">
<span v-for="(value, key) in currentSong.tags" :key="key"
class="px-2 py-1 rounded-full bg-primary/20 text-primary text-sm">
{{ key }}: {{ value }}
</span>
</div>
</div>
<!-- 歌词区域 -->
<div class="flex-1 overflow-y-auto lyrics-container px-4 py-2 scrollbar-hide">
<div class="max-w-2xl mx-auto pb-32">
<div v-if="currentSong.lyrics.length === 0" class="text-center text-white/50 text-xl mt-10">
暂无歌词
</div>
<p v-else v-for="(line, index) in currentSong.lyrics" :key="index"
:class="{'text-white text-3xl font-semibold': currentLyricIndex === index, 'text-white/50 text-xl': currentLyricIndex !== index}"
class="mb-6 transition-all duration-300 text-center"
:data-index="index">
{{ line.text }}
</p>
</div>
</div>
<!-- 右侧控制面板 -->
<div class="fixed right-8 top-1/2 -translate-y-1/2 flex items-start">
<!-- 控制面板内容 -->
<div class="transition-all duration-300"
:class="[showControlPanel ? 'translate-x-0 opacity-100' : 'translate-x-8 opacity-0']">
<div class="bg-black/30 backdrop-blur-lg rounded-xl p-6 shadow-2xl">
<!-- 快捷键提示 -->
<div class="flex flex-col gap-4 text-white/50 text-sm mb-8">
<h3 class="text-white/70 font-semibold mb-2">快捷键</h3>
<div class="flex items-center gap-2">
<kbd class="kbd kbd-sm">Space</kbd>
<span>播放/暂停</span>
</div>
<div class="flex items-center gap-2">
<div class="flex gap-1">
<kbd class="kbd kbd-sm"></kbd>
<kbd class="kbd kbd-sm"></kbd>
</div>
<span>切换歌曲</span>
</div>
<div class="flex items-center gap-2">
<div class="flex gap-1">
<kbd class="kbd kbd-sm"></kbd>
<kbd class="kbd kbd-sm"></kbd>
</div>
<span>调节音量</span>
</div>
</div>
<!-- 歌词偏移控制 -->
<div class="flex flex-col gap-4">
<h3 class="text-white/70 font-semibold">歌词偏移</h3>
<div class="flex items-center gap-2">
<button @click="adjustLyricsOffset(-0.5)"
class="btn btn-sm btn-ghost text-white/70 hover:text-white">
<span class="material-icons text-lg">remove</span>
</button>
<div class="text-white/70 text-sm">
{{ lyricsOffset.toFixed(1) }}s
</div>
<button @click="adjustLyricsOffset(0.5)"
class="btn btn-sm btn-ghost text-white/70 hover:text-white">
<span class="material-icons text-lg">add</span>
</button>
<button @click="resetLyricsOffset"
class="btn btn-sm btn-ghost text-white/70 hover:text-white"
title="重置歌词偏移">
<span class="material-icons text-lg">restart_alt</span>
</button>
</div>
</div>
</div>
</div>
<!-- 切换按钮 -->
<button @click="showControlPanel = !showControlPanel"
class="w-6 h-6 bg-black/30 backdrop-blur-lg rounded-full flex items-center justify-center hover:bg-black/50 transition-colors ml-2"
:class="{'rotate-180': !showControlPanel}">
<span class="material-icons text-white/70 text-sm">chevron_left</span>
</button>
</div>
<!-- 底部控制栏 -->
<div class="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/60 to-transparent pt-10 pb-4 px-4 z-20">
<!-- 进度条 -->
<div class="mb-4">
<input type="range" min="0" :max="duration" v-model="currentTime"
class="range range-xs range-primary w-full opacity-70 hover:opacity-100 transition-opacity cursor-pointer" @input="seek">
<div class="flex justify-between text-xs text-white/50 mt-1">
<span>{{ formatTime(currentTime) }}</span>
<span>{{ formatTime(duration) }}</span>
</div>
</div>
<div class="flex items-center justify-between">
<!-- 播放模式 -->
<div class="flex space-x-3">
<button class="text-white/70 hover:text-white transition-colors p-2 rounded-full"
:class="{'bg-primary/20 text-primary': playMode === 'repeat'}" @click="setPlayMode('repeat')">
<span class="material-icons text-xl">repeat</span>
</button>
<button class="text-white/70 hover:text-white transition-colors p-2 rounded-full"
:class="{'bg-primary/20 text-primary': playMode === 'repeat_one'}" @click="setPlayMode('repeat_one')">
<span class="material-icons text-xl">repeat_one</span>
</button>
<button class="text-white/70 hover:text-white transition-colors p-2 rounded-full"
:class="{'bg-primary/20 text-primary': playMode === 'shuffle'}" @click="setPlayMode('shuffle')">
<span class="material-icons text-xl">shuffle</span>
</button>
</div>
<!-- 播放控制 -->
<div class="flex items-center space-x-4">
<button class="text-white/70 hover:text-white transition-colors" @click="previousSong">
<span class="material-icons text-3xl">skip_previous</span>
</button>
<button class="bg-white text-black rounded-full p-2 hover:bg-primary hover:text-white transition-all"
@click="togglePlay">
<span class="material-icons text-3xl">{{ isPlaying ? 'pause' : 'play_arrow' }}</span>
</button>
<button class="text-white/70 hover:text-white transition-colors" @click="nextSong">
<span class="material-icons text-3xl">skip_next</span>
</button>
<button class="text-white/70 hover:text-white transition-colors" @click="stopPlay">
<span class="material-icons text-3xl">stop</span>
</button>
</div>
<!-- 音量控制 -->
<div class="flex items-center space-x-2">
<span class="material-icons text-white/70 text-xl hover:text-white transition-colors cursor-pointer">
{{ volume > 0 ? 'volume_up' : 'volume_off' }}
</span>
<input type="range" min="0" max="100" step="1" v-model.number="volume"
class="range range-primary w-32" @input="setVolume">
</div>
</div>
</div>
</div>
</div>
<style>
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* 添加平滑滚动效果 */
.lyrics-container {
scroll-behavior: smooth;
mask-image: linear-gradient(to bottom,
transparent 0%,
black 10%,
black 90%,
transparent 100%
);
-webkit-mask-image: linear-gradient(to bottom,
transparent 0%,
black 10%,
black 90%,
transparent 100%
);
}
/* 自定义范围输入样式 */
.range {
height: 8px;
background-color: rgba(255, 255, 255, 0.1);
-webkit-appearance: none;
appearance: none;
}
.range::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
cursor: pointer;
transition: all 0.3s ease-in-out;
}
.range::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}
.range::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
cursor: pointer;
border: none;
transition: all 0.3s ease-in-out;
}
.range::-moz-range-thumb:hover {
transform: scale(1.2);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
}
/* 歌词动画效果 */
.lyrics-container p {
transition: all 0.3s ease-out;
}
.lyrics-container p.active {
transform: scale(1.05);
}
</style>
<script src="./now_playing.js"></script>
</body>
</html><!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>正在播放 - XiaoMusic</title>
<link rel="icon" href="./favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.23/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="https://unpkg.com/vue@3.5.13/dist/vue.global.js"></script>
<script src="./api.js"></script>
</head>
<body class="font-sans">
<div id="app" class="h-screen flex flex-col">
<!-- Toast 提示 -->

View File

@@ -1,5 +1,570 @@
const { createApp, ref, computed, onMounted, watch, onUnmounted } = Vue
createApp({
setup() {
const currentSong = ref({
title: '',
artist: '',
album: '',
cover: '',
lyrics: [],
tags: null,
name: '', // 原始文件名
cur_playlist: '' // 当前歌单
})
const isPlaying = ref(false)
const currentTime = ref(0)
const duration = ref(0)
const volume = ref(1)
const playMode = ref('repeat') // 'repeat', 'repeat_one', 'shuffle'
const currentLyricIndex = ref(0)
const isLoading = ref(false)
const error = ref(null)
const lyricsOffset = ref(0) // 歌词偏移值(秒)
const showControlPanel = ref(true) // 控制面板显示状态
const deviceId = ref(localStorage.getItem('cur_did') || 'web_device')
const audioPlayer = ref(null) // 添加 audioPlayer ref
// Toast 提示相关
const showToast = ref(false)
const toastMessage = ref('')
const toastType = ref('alert-info')
let toastTimer = null
// 从localStorage获取保存的歌词偏移值
const savedOffset = localStorage.getItem('lyrics_offset')
if (savedOffset !== null) {
lyricsOffset.value = parseFloat(savedOffset)
}
// 调整歌词偏移
function adjustLyricsOffset(seconds) {
lyricsOffset.value += seconds
// 保存偏移值到localStorage
localStorage.setItem('lyrics_offset', lyricsOffset.value.toString())
// 重新解析歌词
if (currentSong.value.tags?.lyrics) {
currentSong.value.lyrics = parseLyrics(currentSong.value.tags.lyrics)
updateCurrentLyric()
}
}
// 重置歌词偏移
function resetLyricsOffset() {
lyricsOffset.value = 0
localStorage.setItem('lyrics_offset', '0')
if (currentSong.value.tags?.lyrics) {
currentSong.value.lyrics = parseLyrics(currentSong.value.tags.lyrics)
updateCurrentLyric()
}
}
// 初始化音频播放器
function initAudioPlayer() {
const audio = document.createElement('audio')
audio.id = 'audio-player'
document.body.appendChild(audio)
audioPlayer.value = audio
// 监听播放状态变化
audio.addEventListener('play', () => {
isPlaying.value = true
})
audio.addEventListener('pause', () => {
isPlaying.value = false
})
audio.addEventListener('timeupdate', () => {
currentTime.value = audio.currentTime
updateCurrentLyric()
})
audio.addEventListener('loadedmetadata', () => {
duration.value = audio.duration
})
audio.addEventListener('ended', () => {
// 根据播放模式决定下一步操作
if (playMode.value === 'repeat_one') {
audio.currentTime = 0
audio.play()
} else {
nextSong()
}
})
}
// 更新播放状态
async function updatePlayingStatus() {
try {
error.value = null
deviceId.value = localStorage.getItem('cur_did') || 'web_device'
if (deviceId.value === 'web_device') {
// Web播放模式 - 从localStorage获取当前播放信息
const curMusic = localStorage.getItem('cur_music')
const curPlaylist = localStorage.getItem('cur_playlist')
if (curMusic && (!currentSong.value?.name || curMusic !== currentSong.value.name)) {
isLoading.value = true
try {
// 获取音乐详细信息
const musicInfo = await API.getMusicInfo(curMusic)
if (musicInfo && musicInfo.ret === 'OK') {
const tags = musicInfo.tags || {}
currentSong.value = {
title: tags.title || musicInfo.name,
artist: tags.artist || '未知歌手',
album: tags.album || '未知专辑',
cover: tags.picture || `/cover?name=${encodeURIComponent(musicInfo.name)}`,
lyrics: parseLyrics(tags.lyrics || ''),
tags: {
year: tags.year,
genre: tags.genre
},
name: musicInfo.name,
cur_playlist: curPlaylist
}
// 如果音频播放器存在且URL不同更新URL
if (audioPlayer.value && audioPlayer.value.src !== musicInfo.url) {
audioPlayer.value.src = musicInfo.url
// 如果标记为正在播放,但实际已暂停,尝试恢复播放
if (isPlaying.value && audioPlayer.value.paused) {
try {
await audioPlayer.value.play()
} catch (e) {
console.error('Failed to resume playback:', e)
isPlaying.value = false
}
}
}
}
} finally {
isLoading.value = false
}
}
// 更新播放状态
if (audioPlayer.value) {
isPlaying.value = !audioPlayer.value.paused
currentTime.value = audioPlayer.value.currentTime || 0
duration.value = audioPlayer.value.duration || 0
updateCurrentLyric()
}
} else {
// 设备播放模式 - 从API获取状态
const status = await API.getPlayingStatus(deviceId.value)
if (status.ret === 'OK') {
// 更新播放状态
isPlaying.value = status.is_playing
currentTime.value = status.offset || 0
duration.value = status.duration || 0
currentSong.value.cur_playlist = status.cur_playlist || ''
// 如果有正在播放的音乐且音乐发生改变
if (status.cur_music && status.cur_music !== currentSong.value.name) {
isLoading.value = true
try {
// 获取音乐详细信息
const musicInfo = await API.getMusicInfo(status.cur_music)
if (musicInfo && musicInfo.ret === 'OK') {
const tags = musicInfo.tags || {}
currentSong.value = {
title: tags.title || musicInfo.name,
artist: tags.artist || '未知歌手',
album: tags.album || '未知专辑',
cover: tags.picture || `/cover?name=${encodeURIComponent(musicInfo.name)}`,
lyrics: parseLyrics(tags.lyrics || ''),
tags: {
year: tags.year,
genre: tags.genre
},
name: musicInfo.name,
cur_playlist: status.cur_playlist
}
}
} finally {
isLoading.value = false
}
}
// 更新当前歌词
updateCurrentLyric()
}
}
} catch (err) {
error.value = '获取播放状态失败'
console.error('Error updating playing status:', err)
}
}
// 解析歌词
function parseLyrics(lyricsText) {
if (!lyricsText) return []
const lines = lyricsText.split('\n')
const lyrics = []
const timeRegex = /\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)/
for (const line of lines) {
const match = line.match(timeRegex)
if (match) {
const minutes = parseInt(match[1])
const seconds = parseInt(match[2])
const milliseconds = parseInt(match[3])
const text = match[4].trim()
// 只保留实际歌词行,排除元数据
if (text && !text.startsWith('[') &&
!text.includes('Lyricist') && !text.includes('Composer') &&
!text.includes('Producer') && !text.includes('Engineer') &&
!text.includes('Studio') && !text.includes('Company') &&
!text.includes('') && !text.includes('Original') &&
!text.includes('Design') && !text.includes('Director') &&
!text.includes('Supervisor') && !text.includes('Promoter')) {
// 保存原始时间戳,不应用偏移
const time = minutes * 60 + seconds + (milliseconds / 1000)
lyrics.push({
time: Math.max(0, time),
text: text
})
}
}
}
return lyrics.sort((a, b) => a.time - b.time)
}
// 更新当前歌词
function updateCurrentLyric() {
const lyrics = currentSong.value.lyrics
if (!lyrics.length) return
// 找到当前时间对应的歌词
let foundIndex = -1
// 应用偏移后的当前时间
const currentTimeWithOffset = currentTime.value - lyricsOffset.value
// 二分查找优化性能
let left = 0
let right = lyrics.length - 1
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const lyricTime = lyrics[mid].time
if (mid === lyrics.length - 1) {
if (currentTimeWithOffset >= lyricTime) {
foundIndex = mid
break
}
} else {
const nextTime = lyrics[mid + 1].time
if (currentTimeWithOffset >= lyricTime && currentTimeWithOffset < nextTime) {
foundIndex = mid
break
}
}
if (currentTimeWithOffset < lyricTime) {
right = mid - 1
} else {
left = mid + 1
}
}
// 如果找到新的歌词索引,更新显示
if (foundIndex !== -1 && foundIndex !== currentLyricIndex.value) {
currentLyricIndex.value = foundIndex
// 滚动到当前歌词
const container = document.querySelector('.lyrics-container')
const currentLyric = container.querySelector(`[data-index="${foundIndex}"]`)
if (currentLyric) {
const containerHeight = container.clientHeight
const lyricTop = currentLyric.offsetTop
const lyricHeight = currentLyric.clientHeight
// 计算滚动位置,使当前歌词在容器中垂直居中
container.scrollTo({
top: lyricTop - (containerHeight / 2) + (lyricHeight / 2),
behavior: 'smooth'
})
}
}
}
// 显示提示
function showMessage(message, type = 'info') {
if (toastTimer) {
clearTimeout(toastTimer)
}
toastMessage.value = message
toastType.value = `alert-${type}`
showToast.value = true
toastTimer = setTimeout(() => {
showToast.value = false
}, 3000)
}
// 播放控制
async function togglePlay() {
try {
if (deviceId.value === 'web_device') {
// Web播放模式
if (!currentSong.value?.name) {
showMessage('没有可播放的歌曲', 'error')
return
}
if (isPlaying.value) {
if (audioPlayer.value) {
audioPlayer.value.pause()
isPlaying.value = false
showMessage('暂停播放')
}
} else {
try {
// 获取最新的音乐URL
const musicInfo = await API.getMusicInfo(currentSong.value.name)
if (musicInfo && musicInfo.ret === 'OK') {
if (audioPlayer.value) {
if (audioPlayer.value.src !== musicInfo.url) {
audioPlayer.value.src = musicInfo.url
}
await audioPlayer.value.play()
isPlaying.value = true
showMessage('开始播放')
}
} else {
showMessage('获取音乐信息失败', 'error')
}
} catch (error) {
console.error('Error getting music info:', error)
showMessage('播放失败', 'error')
}
}
} else {
// 设备播放模式
if (isPlaying.value) {
// 如果正在播放,则暂停
const response = await API.sendCommand(deviceId.value, API.commands.PLAY_PAUSE)
if (response.ret === 'OK') {
isPlaying.value = false
showMessage('暂停播放')
}
} else {
// 如果当前是暂停状态,获取当前歌曲信息并重新播放
const status = await API.getPlayingStatus(deviceId.value)
if (status.ret === 'OK' && status.cur_music && status.cur_playlist) {
// 使用 playmusiclist 接口重新播放当前歌曲
const response = await API.playMusicFromList(deviceId.value, status.cur_playlist, status.cur_music)
if (response.ret === 'OK') {
isPlaying.value = true
showMessage('开始播放')
} else {
showMessage('播放失败', 'error')
}
} else {
showMessage('获取播放信息失败', 'error')
}
}
}
} catch (error) {
console.error('Error toggling play state:', error)
showMessage('播放控制失败', 'error')
}
}
async function previousSong() {
const response = await API.sendCommand(deviceId.value, API.commands.PLAY_PREVIOUS)
if (response.ret === 'OK') {
showMessage('播放上一首')
}
}
async function nextSong() {
const response = await API.sendCommand(deviceId.value, API.commands.PLAY_NEXT)
if (response.ret === 'OK') {
showMessage('播放下一首')
}
}
async function stopPlay() {
const response = await API.sendCommand(deviceId.value, API.commands.PLAY_PAUSE)
if (response.ret === 'OK') {
isPlaying.value = false
showMessage('停止播放')
}
}
async function setPlayMode(mode) {
let cmd
let modeName
switch (mode) {
case 'repeat':
cmd = API.commands.PLAY_MODE_SEQUENCE
modeName = '顺序播放'
break
case 'repeat_one':
cmd = API.commands.PLAY_MODE_SINGLE
modeName = '单曲循环'
break
case 'shuffle':
cmd = API.commands.PLAY_MODE_RANDOM
modeName = '随机播放'
break
}
if (cmd) {
const response = await API.sendCommand(deviceId.value, cmd)
if (response.ret === 'OK') {
playMode.value = mode
showMessage(`切换到${modeName}模式`)
}
}
}
// 音量控制
async function setVolume() {
try {
const volumeValue = parseInt(volume.value)
const response = await API.setVolume(deviceId.value, volumeValue)
if (response.ret === 'OK') {
showMessage(`音量: ${volumeValue}%`)
if (audioPlayer.value) {
audioPlayer.value.volume = volumeValue / 100
}
} else {
console.error('Failed to set volume:', response)
}
} catch (error) {
console.error('Error setting volume:', error)
}
}
// 手动调整进度
async function seek() {
try {
if (deviceId.value === 'web_device') {
// Web播放模式
const audio = document.getElementById('audio-player')
if (audio) {
audio.currentTime = currentTime.value
}
} else {
// 设备播放模式
await API.sendCommand(deviceId.value, `seek ${Math.floor(currentTime.value)}`)
}
// 立即更新歌词显示
updateCurrentLyric()
} catch (error) {
console.error('Error seeking:', error)
showMessage('调整进度失败', 'error')
}
}
// 格式化时间
function formatTime(time) {
if (!time) return '00:00'
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
// 初始化
onMounted(async () => {
// 初始化音频播放器
initAudioPlayer()
// 获取并更新当前音量
try {
const volumeResponse = await API.getVolume(deviceId.value)
if (volumeResponse.ret === 'OK') {
volume.value = parseInt(volumeResponse.volume)
if (audioPlayer.value) {
audioPlayer.value.volume = volume.value / 100
}
}
} catch (err) {
console.error('Error getting volume:', err)
}
// 开始定时获取播放状态
updatePlayingStatus()
setInterval(updatePlayingStatus, 1000)
// 添加键盘事件监听
document.addEventListener('keydown', handleKeyPress)
})
// 处理键盘事件
async function handleKeyPress(event) {
// 如果用户正在输入,不处理快捷键
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
return
}
switch (event.code) {
case 'Space': // 空格键:播放/暂停
event.preventDefault() // 防止页面滚动
await togglePlay()
break
case 'ArrowLeft': // 左方向键:上一首
event.preventDefault()
await previousSong()
break
case 'ArrowRight': // 右方向键:下一首
event.preventDefault()
await nextSong()
break
case 'ArrowUp': // 上方向键:增加音量
event.preventDefault()
if (volume.value < 100) {
volume.value = Math.min(100, volume.value + 5)
await setVolume()
}
break
case 'ArrowDown': // 下方向键:减小音量
event.preventDefault()
if (volume.value > 0) {
volume.value = Math.max(0, volume.value - 5)
await setVolume()
}
break
}
}
// 在组件销毁时清理
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyPress)
if (audioPlayer.value) {
audioPlayer.value.remove()
}
})
return {
currentSong,
isPlaying,
currentTime,
duration,
volume,
playMode,
currentLyricIndex,
isLoading,
error,
lyricsOffset,
showToast,
toastMessage,
toastType,
showControlPanel,
togglePlay,
seek,
setVolume,
previousSong,
nextSong,
stopPlay,
setPlayMode,
formatTime,
adjustLyricsOffset,
resetLyricsOffset
}
}
}).mount('#app') const { createApp, ref, computed, onMounted, watch, onUnmounted } = Vue
createApp({
setup() {
const currentSong = ref({