mirror of
https://github.com/atdunbg/Nekosonic-Music.git
synced 2026-06-22 10:48:05 +08:00
feat: v0.3.0 - 流式播放、本地音乐、下载系统、漫游修复
### 新功能 - 流式播放:边下载边播放,缓冲 64KB 后即刻开始,无需等待完整下载 - 本地音乐页面:支持浏览、播放本地歌曲,横向菜单含「从磁盘删除」 - 下载系统:支持下载歌曲到自定义路径,保存完整元数据(封面/专辑/时长) - 封面补全:本地音乐缺少封面时自动从网易云 API 获取 - 更新信息:接入 Gitea Releases API,查看最新版更新日志 ### 修复 - 修复私人漫游播完一首歌后跳三首的问题(双重触发:audio-ended + startTick) - 修复全屏漫游抽屉和漫游页面无封面歌曲显示破损图片 - 修复 PlayerBar 无封面歌曲显示破损图片 - 修复下载路径修改后不生效(Rust serde camelCase 映射) - 修复本地音乐始终只显示默认路径歌曲 - 修复下载完成提示弹出 4 次 - 修复播放网络歌曲时进度条先走但无声音(audio-started 事件同步) ### 优化 - PlayerBar 下载状态:未下载显示下载按钮,下载中显示进度,已下载不显示 - audio.rs 新增 manual_stop 标志防止 stop_audio 触发虚假 audio-ended - player.ts 新增 waitForAudioStart() 确保 playing 状态与实际播放同步 - 切歌/停止时立即清除 tickInterval 防止重复触发 next()
This commit is contained in:
135
src/composables/useDownload.ts
Normal file
135
src/composables/useDownload.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { reactive, watch } from 'vue';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { useSettingsStore } from '../stores/settings';
|
||||
import { showToast } from '../composables/useToast';
|
||||
|
||||
interface DownloadTask {
|
||||
id: number;
|
||||
name: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
const downloadingIds = reactive<Set<number>>(new Set());
|
||||
const tasks = reactive<DownloadTask[]>([]);
|
||||
const localSongIds = reactive<Set<number>>(new Set());
|
||||
|
||||
let listenerSetup = false;
|
||||
let storeSetup = false;
|
||||
|
||||
async function setupDownloadListener() {
|
||||
if (listenerSetup) return;
|
||||
listenerSetup = true;
|
||||
await listen<{ id: number; progress: number; name: string }>('download-progress', (event) => {
|
||||
const { id, progress, name } = event.payload;
|
||||
if (progress >= 100) {
|
||||
const idx = tasks.findIndex(t => t.id === id);
|
||||
if (idx >= 0) {
|
||||
tasks.splice(idx, 1);
|
||||
downloadingIds.delete(id);
|
||||
showToast(`${name} 下载完成`, 'success');
|
||||
}
|
||||
} else {
|
||||
const task = tasks.find(t => t.id === id);
|
||||
if (task) {
|
||||
task.progress = progress;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshLocalIds() {
|
||||
try {
|
||||
const settings = useSettingsStore();
|
||||
const list: { id: number }[] = await invoke('list_local_songs', { downloadPath: settings.downloadPath || null });
|
||||
localSongIds.clear();
|
||||
for (const s of list) {
|
||||
localSongIds.add(s.id);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function ensureStoreSetup() {
|
||||
if (storeSetup) return;
|
||||
storeSetup = true;
|
||||
const settings = useSettingsStore();
|
||||
refreshLocalIds();
|
||||
watch(() => settings.downloadPath, () => {
|
||||
refreshLocalIds();
|
||||
});
|
||||
}
|
||||
|
||||
function isDownloaded(songId: number): boolean {
|
||||
return localSongIds.has(songId);
|
||||
}
|
||||
|
||||
function isDownloading(songId: number): boolean {
|
||||
return downloadingIds.has(songId);
|
||||
}
|
||||
|
||||
function getDownloadProgress(songId: number): number {
|
||||
const task = tasks.find(t => t.id === songId);
|
||||
return task?.progress ?? 0;
|
||||
}
|
||||
|
||||
async function downloadSong(song: { id: number; name: string; ar?: { name: string }[]; artists?: { name: string }[]; al?: { picUrl?: string; name?: string }; album?: { picUrl?: string; name?: string }; dt?: number; duration?: number }) {
|
||||
if (downloadingIds.has(song.id)) return;
|
||||
if (localSongIds.has(song.id)) {
|
||||
showToast(`${song.name} 已下载`, 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = useSettingsStore();
|
||||
const artist = song.ar?.map(a => a.name).join(' / ') || song.artists?.map(a => a.name).join(' / ') || '未知';
|
||||
const albumName = song.al?.name || song.album?.name || null;
|
||||
const durationVal = song.dt || song.duration || null;
|
||||
const coverUrl = song.al?.picUrl || song.album?.picUrl || null;
|
||||
|
||||
downloadingIds.add(song.id);
|
||||
tasks.push({ id: song.id, name: song.name, progress: 0 });
|
||||
|
||||
try {
|
||||
await invoke('download_song', {
|
||||
query: {
|
||||
id: song.id,
|
||||
name: song.name,
|
||||
artist,
|
||||
album: albumName,
|
||||
duration: durationVal,
|
||||
coverUrl,
|
||||
level: settings.audioQuality,
|
||||
downloadPath: settings.downloadPath || null,
|
||||
},
|
||||
});
|
||||
localSongIds.add(song.id);
|
||||
} catch (e: any) {
|
||||
downloadingIds.delete(song.id);
|
||||
const idx = tasks.findIndex(t => t.id === song.id);
|
||||
if (idx >= 0) tasks.splice(idx, 1);
|
||||
if (e === '文件已存在') {
|
||||
localSongIds.add(song.id);
|
||||
showToast(`${song.name} 已下载`, 'info');
|
||||
} else if (e === 'VIP歌曲无法下载') {
|
||||
showToast(`${song.name} 为 VIP 歌曲,无法下载`, 'error');
|
||||
} else if (typeof e === 'string' && e.includes('VIP')) {
|
||||
showToast(`${song.name} 需要 VIP 权限才能下载`, 'error');
|
||||
} else {
|
||||
showToast(`下载失败: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useDownload() {
|
||||
setupDownloadListener();
|
||||
ensureStoreSetup();
|
||||
return {
|
||||
downloadingIds,
|
||||
tasks,
|
||||
localSongIds,
|
||||
isDownloaded,
|
||||
isDownloading,
|
||||
getDownloadProgress,
|
||||
downloadSong,
|
||||
refreshLocalIds,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user