feat: 跨平台持久化与版本管理优化

- Cookie 存储从 temp_dir 迁移至 Tauri app_data_dir,兼容 Linux
- 简单统一风格,UI优化
- recentLocal 播放历史持久化到 localStorage
- 添加设置界面可以修改简单的设置
This commit is contained in:
2026-05-12 09:58:07 +08:00
parent 463e8e95b6
commit 7847a9f6b2
28 changed files with 1592 additions and 535 deletions

View File

@ -1,8 +1,9 @@
use ncm_api_rs::{create_client, ApiClient, Query};
use serde::Deserialize;
use tauri::State;
use tokio::sync::Mutex; // 异步 Mutex
use std::sync::Mutex as StdMutex; // 同步 Mutex 用于 cookie
use tauri::{Manager, State};
use tokio::sync::Mutex;
use std::sync::Mutex as StdMutex;
use std::sync::atomic::Ordering;
use std::fs;
use std::path::PathBuf;
@ -10,13 +11,13 @@ use std::path::PathBuf;
pub struct ApiController {
client: Mutex<ApiClient>,
cookie: StdMutex<Option<String>>,
cookie_path: PathBuf,
cookie_path: PathBuf,
}
fn cookies_to_key_values(cookies: &[String]) -> String {
cookies
.iter()
.filter_map(|c| c.split(';').next()) // 取第一个键值对
.filter_map(|c| c.split(';').next())
.map(|s| s.trim().to_string())
.collect::<Vec<_>>()
.join("; ")
@ -24,14 +25,14 @@ fn cookies_to_key_values(cookies: &[String]) -> String {
impl ApiController {
pub fn new() -> Self {
let cookie_path = std::env::temp_dir().join("netease_cookies.json");
pub fn new(app_data_dir: PathBuf) -> Self {
let _ = fs::create_dir_all(&app_data_dir);
let cookie_path = app_data_dir.join("netease_cookies.json");
let saved_cookie = fs::read_to_string(&cookie_path)
.map(|s| s.trim().to_string())
.ok(); // 注意这里返回 Option<String>
// eprintln!("[api] 启动时加载 cookie: {:?}", saved_cookie);
.ok();
let client = create_client(None); // 不依赖客户端存储,我们自己管理
let client = create_client(None);
ApiController {
client: Mutex::new(client),
cookie: StdMutex::new(saved_cookie),
@ -43,13 +44,11 @@ fn build_query(&self) -> Query {
let mut query = Query::new();
if let Ok(cookie_guard) = self.cookie.lock() {
if let Some(c) = cookie_guard.as_ref() {
// eprintln!("[api] 请求携带 cookie: {}", c);
query = query.cookie(c);
}
}
query
}
/// 保存 cookie 到文件
fn save_cookie(&self, cookie_str: &str) {
let _ = fs::write(&self.cookie_path, cookie_str);
}
@ -58,14 +57,13 @@ fn build_query(&self) -> Query {
#[derive(Deserialize)]
pub struct SearchQuery { pub keyword: String }
#[derive(Deserialize)]
pub struct LoginQuery { pub phone: String, pub password: String }
#[derive(Deserialize)]
pub struct QrKeyQuery { pub key: String }
// 搜索歌曲
/// 搜索歌曲
#[tauri::command]
pub async fn search_songs(query: SearchQuery, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -78,7 +76,7 @@ pub async fn search_songs(query: SearchQuery, state: State<'_, ApiController>) -
.map_err(|e| e.to_string())
}
// 获取热搜词
/// 获取热搜词列表
#[tauri::command]
pub async fn get_hot_search(state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -88,14 +86,33 @@ pub async fn get_hot_search(state: State<'_, ApiController>) -> Result<String, S
.map_err(|e| e.to_string())
}
#[derive(Deserialize)]
pub struct PlaylistTrackAllQuery { pub id: u64, pub limit: Option<i64>, pub offset: Option<i64> }
// 获取歌曲链接
/// 获取歌单全部歌
#[tauri::command]
pub async fn get_song_url(id: u64, state: State<'_, ApiController>) -> Result<String, String> {
pub async fn playlist_track_all(query: PlaylistTrackAllQuery, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
let q = state.build_query()
.param("id", &id.to_string())
.param("level", "standard");
.param("id", &query.id.to_string())
.param("limit", &query.limit.unwrap_or(1000).to_string())
.param("offset", &query.offset.unwrap_or(0).to_string());
client.playlist_track_all(&q).await
.map(|r| r.body.to_string())
.map_err(|e| e.to_string())
}
#[derive(Deserialize)]
pub struct SongUrlQuery { pub id: u64, pub level: Option<String> }
/// 获取歌曲播放地址
#[tauri::command]
pub async fn get_song_url(query: SongUrlQuery, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
let level = query.level.as_deref().unwrap_or("standard");
let q = state.build_query()
.param("id", &query.id.to_string())
.param("level", level);
let resp = client.song_url_v1(&q).await.map_err(|e| e.to_string())?;
resp.body["data"][0]["url"].as_str()
.filter(|s| !s.is_empty())
@ -103,8 +120,7 @@ pub async fn get_song_url(id: u64, state: State<'_, ApiController>) -> Result<St
.ok_or_else(|| "暂无播放源".into())
}
// 获取歌词
/// 获取歌词
#[tauri::command]
pub async fn get_lyric(id: u64, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -114,8 +130,7 @@ pub async fn get_lyric(id: u64, state: State<'_, ApiController>) -> Result<Strin
.map_err(|e| e.to_string())
}
// 获取歌单详情
/// 获取歌单详情
#[tauri::command]
pub async fn get_playlist_detail(id: u64, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -125,7 +140,7 @@ pub async fn get_playlist_detail(id: u64, state: State<'_, ApiController>) -> Re
.map_err(|e| e.to_string())
}
// 登录
/// 手机号密码登录
#[tauri::command]
pub async fn login(query: LoginQuery, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -143,17 +158,15 @@ pub async fn login(query: LoginQuery, state: State<'_, ApiController>) -> Result
Ok(resp.body.to_string())
}
// 登出
/// 退出登录
#[tauri::command]
pub async fn logout(state: State<'_, ApiController>) -> Result<(), String> {
// 清除内存中的 cookie
*state.cookie.lock().map_err(|e| e.to_string())? = None;
// 删除持久化文件
let _ = fs::remove_file(&state.cookie_path);
Ok(())
}
// 获取二维码key
/// 获取二维码登录密钥
#[tauri::command]
pub async fn get_qr_key(state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -165,7 +178,7 @@ pub async fn get_qr_key(state: State<'_, ApiController>) -> Result<String, Strin
.ok_or_else(|| "缺少 unikey".into())
}
// 创建二维码, 功能暂时有问题
/// 生成二维码图片
#[tauri::command]
pub async fn create_qr(
query: QrKeyQuery,
@ -177,7 +190,6 @@ pub async fn create_qr(
.param("key", &query.key)
.param("qrimg", "true");
let resp = client.login_qr_create(&q).await.map_err(|e| e.to_string())?;
// 提取 qrurl 字段(网易云新的返回格式)
let qrurl = resp.body["data"]["qrurl"]
.as_str()
.ok_or("未获取到二维码链接")?
@ -185,7 +197,7 @@ pub async fn create_qr(
Ok(qrurl)
}
// 检查二维码状态
/// 检查二维码扫码状态
#[tauri::command]
pub async fn check_qr_status(query: QrKeyQuery, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -199,7 +211,7 @@ pub async fn check_qr_status(query: QrKeyQuery, state: State<'_, ApiController>)
Ok(resp.body.to_string())
}
// 获取登录状态
/// 获取当前登录状态
#[tauri::command]
pub async fn get_login_status(state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -209,7 +221,7 @@ pub async fn get_login_status(state: State<'_, ApiController>) -> Result<String,
.map_err(|e| e.to_string())
}
// 用户歌单
/// 获取用户歌单列表
#[tauri::command]
pub async fn user_playlist(uid: u64, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -218,7 +230,7 @@ pub async fn user_playlist(uid: u64, state: State<'_, ApiController>) -> Result<
Ok(resp.body.to_string())
}
// 每日推荐歌曲
/// 获取每日推荐歌曲
#[tauri::command]
pub async fn recommend_songs(state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -227,7 +239,7 @@ pub async fn recommend_songs(state: State<'_, ApiController>) -> Result<String,
Ok(resp.body.to_string())
}
// 推荐歌单(需要登录)
/// 获取推荐歌单
#[tauri::command]
pub async fn recommend_resource(state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -236,6 +248,7 @@ pub async fn recommend_resource(state: State<'_, ApiController>) -> Result<Strin
Ok(resp.body.to_string())
}
/// 获取私人漫游歌曲
#[tauri::command]
pub async fn personal_fm(state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
@ -244,10 +257,86 @@ pub async fn personal_fm(state: State<'_, ApiController>) -> Result<String, Stri
Ok(resp.body.to_string())
}
/// 获取歌曲详情
#[tauri::command]
pub async fn get_song_detail(id: u64, state: State<'_, ApiController>) -> Result<String, String> {
pub async fn get_song_detail(id: String, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
let q = state.build_query().param("ids", &id.to_string());
let q = state.build_query().param("ids", &id);
let resp = client.song_detail(&q).await.map_err(|e| e.to_string())?;
Ok(resp.body.to_string())
}
}
#[derive(Deserialize)]
pub struct UserRecordQuery { pub uid: u64, pub r#type: String }
#[derive(Deserialize)]
pub struct LikeSongQuery { pub id: u64, pub like: String }
/// 获取喜欢的歌曲ID列表
#[tauri::command]
pub async fn likelist(uid: u64, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
let q = state.build_query().param("uid", &uid.to_string());
client.likelist(&q).await
.map(|r| r.body.to_string())
.map_err(|e| e.to_string())
}
/// 获取用户播放记录
#[tauri::command]
pub async fn user_record(query: UserRecordQuery, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
let q = state.build_query()
.param("uid", &query.uid.to_string())
.param("type", &query.r#type);
client.user_record(&q).await
.map(|r| r.body.to_string())
.map_err(|e| e.to_string())
}
/// 喜欢/取消喜欢歌曲
#[tauri::command]
pub async fn like_song(query: LikeSongQuery, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
let q = state.build_query()
.param("id", &query.id.to_string())
.param("like", &query.like);
client.like(&q).await
.map(|r| r.body.to_string())
.map_err(|e| e.to_string())
}
/// 上报最近播放歌曲
#[tauri::command]
pub async fn record_recent_song(limit: u64, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
let q = state.build_query().param("limit", &limit.to_string());
client.record_recent_song(&q).await
.map(|r| r.body.to_string())
.map_err(|e| e.to_string())
}
#[derive(Deserialize)]
pub struct PlaylistSubscribeQuery { pub id: u64, pub subscribe: Option<bool> }
/// 收藏/取消收藏歌单
#[tauri::command]
pub async fn playlist_subscribe(query: PlaylistSubscribeQuery, state: State<'_, ApiController>) -> Result<String, String> {
let client = state.client.lock().await;
let t = if query.subscribe.unwrap_or(true) { "1" } else { "0" };
let q = state.build_query()
.param("id", &query.id.to_string())
.param("t", t);
client.playlist_subscribe(&q).await
.map(|r| r.body.to_string())
.map_err(|e| e.to_string())
}
/// 退出应用
#[tauri::command]
pub async fn exit_app(app_handle: tauri::AppHandle) {
crate::ALLOW_EXIT.store(true, Ordering::SeqCst);
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.close();
}
}