mirror of
https://github.com/atdunbg/Nekosonic-Music.git
synced 2026-06-22 00:58:51 +08:00
### 新功能 - 流式播放:边下载边播放,缓冲 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()
136 lines
3.9 KiB
TypeScript
136 lines
3.9 KiB
TypeScript
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,
|
|
};
|
|
}
|