From 6da544cffb70d46f7d74696607d2b362f0c3cc5e Mon Sep 17 00:00:00 2001 From: Atdunbg Date: Mon, 25 May 2026 19:38:48 +0800 Subject: [PATCH] =?UTF-8?q?v0.5.1:=20=E4=BF=AE=E5=A4=8D=E7=BC=93=E5=AD=98/?= =?UTF-8?q?FM/=E7=BF=BB=E8=AF=91/Linux=E7=AD=89=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 14 ++++++ package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/api.rs | 15 +++++++ src-tauri/src/lib.rs | 1 + src-tauri/src/media_controls.rs | 12 ++++-- src-tauri/tauri.conf.json | 2 +- src/App.vue | 6 +++ src/composables/usePageCache.ts | 6 +++ src/stores/player.ts | 75 ++++++++++++++++++++++++++------- src/views/DailySongs.vue | 8 +++- src/views/Discover.vue | 8 +++- src/views/FavoriteSongs.vue | 8 +++- src/views/Home.vue | 8 +++- src/views/LocalMusic.vue | 11 +++-- 16 files changed, 147 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca556ac..6cf9ac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## v0.5.1 + +### 🐛 修复 +- 修复页面缓存不刷新的问题:切换回已缓存的页面时数据永远不更新,现在超过 5 分钟会自动重新加载 +- 修复本地音乐页面空列表时刷新按钮不显示的问题 +- 修复修改下载路径后本地音乐列表不更新的问题,现在会自动刷新 +- 修复私人 FM 播放约二三十首后循环重复的问题:新增听歌打卡上报,服务端推荐不再重复 +- 修复歌词界面切换翻译开关时歌词未居中的问题 +- 修复 Linux 下从外部控制暂停时进度条跳回 0 的问题:MPRIS 现在正确报告播放进度位置 + +### ⚡ 优化 +- 私人 FM 预取队列优化,队列剩余不足时自动后台拉取下一批 + + ## v0.5.0 ### ✨ 新功能 diff --git a/package.json b/package.json index 58f1b11..c05d156 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nekosonic", "private": true, - "version": "0.3.0", + "version": "0.5.1", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index dfb8b28..2773ed8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "Nekosonic" -version = "0.5.0" +version = "0.5.1" dependencies = [ "base64 0.22.1", "cpal", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 600161c..36ee1f5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "Nekosonic" -version = "0.5.0" +version = "0.5.1" description = "A Simple music app" authors = ["atdunbg"] edition = "2021" diff --git a/src-tauri/src/api.rs b/src-tauri/src/api.rs index e0e155f..1da235d 100644 --- a/src-tauri/src/api.rs +++ b/src-tauri/src/api.rs @@ -318,6 +318,21 @@ pub async fn personal_fm(state: State<'_, ApiController>) -> Result, + pub time: u64, +} + +/// 听歌打卡 +#[tauri::command] +pub async fn scrobble(query: ScrobbleQuery, state: State<'_, ApiController>) -> Result { + api_call!(state, scrobble, params: [("id", &query.id.to_string()), ("sourceid", query.sourceid.as_deref().unwrap_or("")), ("time", &query.time.to_string())]) +} + /// 获取歌曲详情 #[tauri::command] pub async fn get_song_detail(id: String, state: State<'_, ApiController>) -> Result { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 735922c..0abe0d9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -145,6 +145,7 @@ pub fn run() { api::recommend_resource, api::recommend_songs, api::personal_fm, + api::scrobble, api::get_song_detail, api::get_qr_key, api::create_qr, diff --git a/src-tauri/src/media_controls.rs b/src-tauri/src/media_controls.rs index 22f6f6f..2d06f10 100644 --- a/src-tauri/src/media_controls.rs +++ b/src-tauri/src/media_controls.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, Mutex}; use tauri::{AppHandle, Emitter, Listener}; use souvlaki::{ MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, - PlatformConfig, SeekDirection, + MediaPosition, PlatformConfig, SeekDirection, }; struct MediaState { @@ -79,9 +79,15 @@ pub fn start_media_controls(app_handle: AppHandle, hwnd: Option<*mut std::ffi::c }; if let Some(status) = data.get("status").and_then(|v| v.as_str()) { + let position_us = data.get("positionUs").and_then(|v| v.as_i64()).unwrap_or(0); + let progress = if position_us > 0 { + Some(MediaPosition(std::time::Duration::from_micros(position_us as u64))) + } else { + None + }; let playback = match status { - "playing" => MediaPlayback::Playing { progress: None }, - "paused" => MediaPlayback::Paused { progress: None }, + "playing" => MediaPlayback::Playing { progress }, + "paused" => MediaPlayback::Paused { progress }, _ => MediaPlayback::Stopped, }; let _ = s.controls.set_playback(playback); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 24c021c..085732b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Nekosonic", - "version": "0.5.0", + "version": "0.5.1", "identifier": "com.atdunbg.Nekosonic", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.vue b/src/App.vue index 2ed3561..c8caffa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -375,6 +375,12 @@ watch(currentLyricIdx, () => { } }); +watch(showTranslation, () => { + if (player.showRoamDrawer && !roamLyricHovering.value) { + nextTick(() => scrollToRoamActiveLyric()); + } +}); + function scrollToRoamActiveLyric() { if (!lyricScrollContainer.value || roamLyricHovering.value) return; const active = lyricScrollContainer.value.querySelector('.roam-lyric-active') as HTMLElement | null; diff --git a/src/composables/usePageCache.ts b/src/composables/usePageCache.ts index cbf97d1..513e0f1 100644 --- a/src/composables/usePageCache.ts +++ b/src/composables/usePageCache.ts @@ -22,3 +22,9 @@ export function pageCacheDelete(key: string) { export function pageCacheInvalidate(key: string) { cache.delete(key); } + +export function pageCacheIsStale(key: string): boolean { + const entry = cache.get(key); + if (!entry) return true; + return Date.now() - entry.ts > TTL; +} diff --git a/src/stores/player.ts b/src/stores/player.ts index 5312b7a..f8aad85 100644 --- a/src/stores/player.ts +++ b/src/stores/player.ts @@ -111,8 +111,34 @@ export const usePlayerStore = defineStore('player', () => { }, { deep: true }); const isFmMode = ref(false); + const fmQueue: Song[] = []; let fmNextCallback: (() => void) | null = null; + let lastScrobbleId: number | null = null; + let lastScrobbleStartTime: number = 0; + + function reportScrobble() { + const song = currentSong.value; + if (!song || song.localPath || song.id == null) { + lastScrobbleId = null; + return; + } + if (lastScrobbleId === song.id && lastScrobbleStartTime > 0) { + const playedSec = Math.round((Date.now() - lastScrobbleStartTime) / 1000); + if (playedSec > 5) { + invoke('scrobble', { + query: { + id: song.id, + sourceid: isFmMode.value ? String(song.id) : '', + time: playedSec, + }, + }).catch(() => {}); + } + } + lastScrobbleId = song.id; + lastScrobbleStartTime = Date.now(); + } + function enableFmMode(onNext: () => void) { isFmMode.value = true; fmNextCallback = onNext; @@ -121,6 +147,15 @@ export const usePlayerStore = defineStore('player', () => { function disableFmMode() { isFmMode.value = false; fmNextCallback = null; + fmQueue.length = 0; + } + + async function fetchFmBatch(): Promise { + const jsonStr: string = await invoke('personal_fm'); + const data = JSON.parse(jsonStr); + const raw = data.data || data; + if (!Array.isArray(raw) || raw.length === 0) return []; + return raw.map((s: any) => normalizeSong(s)); } let fmVipSkipCount = 0; @@ -128,6 +163,7 @@ export const usePlayerStore = defineStore('player', () => { async function playFmSong(song: Song) { if (tickInterval) { clearInterval(tickInterval); setTickInterval(null); } + reportScrobble(); if (!song.dt || song.dt === 0) { try { const jsonStr: string = await invoke('get_song_detail', { id: String(song.id) }); @@ -248,6 +284,7 @@ export const usePlayerStore = defineStore('player', () => { async function playCurrent() { if (tickInterval) { clearInterval(tickInterval); setTickInterval(null); } + reportScrobble(); const song = queue.value[currentIndex.value]; if (!song?.id) { console.error('无效的歌曲数据', song); @@ -494,12 +531,11 @@ export const usePlayerStore = defineStore('player', () => { async function loadFirstFmSong() { try { - const jsonStr: string = await invoke('personal_fm'); - const data = JSON.parse(jsonStr); - const songs = data.data || data; - if (songs && songs.length > 0) { - const song = normalizeSong(songs[0]); - enableFmMode(() => loadFirstFmSong()); + const batch = await fetchFmBatch(); + if (batch.length > 0) { + fmQueue.push(...batch); + const song = fmQueue.shift()!; + enableFmMode(nextFm); await playFmSong(song); return true; } @@ -516,15 +552,18 @@ const fmPlaying = ref(false); async function loadFm() { try { - const jsonStr: string = await invoke('personal_fm'); - const data = JSON.parse(jsonStr); - const songs = data.data || data; - if (songs && songs.length > 0) { - const song = normalizeSong(songs[0]); - fmSong.value = song; - enableFmMode(nextFm); - await playFmSong(song); - fmPlaying.value = true; + if (fmQueue.length === 0) { + const batch = await fetchFmBatch(); + if (batch.length === 0) return; + fmQueue.push(...batch); + } + const song = fmQueue.shift()!; + fmSong.value = song; + enableFmMode(nextFm); + await playFmSong(song); + fmPlaying.value = true; + if (fmQueue.length <= 1) { + fetchFmBatch().then(batch => { fmQueue.push(...batch); }).catch(() => {}); } } catch (e) { console.error('FM加载失败', e); @@ -565,6 +604,7 @@ listen('audio-started', () => { listen('audio-ended', () => { if (_tickInterval) { clearInterval(_tickInterval); _tickInterval = null; } const player = usePlayerStore(); + player.reportScrobble(); player.next(); }); @@ -597,6 +637,9 @@ listen('mpris-command', (event) => { } else if (cmd.startsWith('SetPosition:')) { const posUs = parseInt(cmd.slice(13), 10); const posSec = posUs / 1_000_000; + if (posSec < 1 && player.currentTime > 5) { + return; + } player.seek(posSec); } else if (cmd === 'Raise') { getCurrentWindow().show().catch(() => {}); @@ -666,6 +709,8 @@ watch(playing, (val) => { toggleRoamDrawer, loadFirstFmSong, + reportScrobble, + fmSong, fmPlaying, loadFm, diff --git a/src/views/DailySongs.vue b/src/views/DailySongs.vue index 685cdd1..747f013 100644 --- a/src/views/DailySongs.vue +++ b/src/views/DailySongs.vue @@ -51,11 +51,11 @@