mirror of
https://github.com/atdunbg/Nekosonic-Music.git
synced 2026-06-22 10:48:05 +08:00
feat: 跨平台持久化与版本管理优化
- Cookie 存储从 temp_dir 迁移至 Tauri app_data_dir,兼容 Linux - 简单统一风格,UI优化 - recentLocal 播放历史持久化到 localStorage - 添加设置界面可以修改简单的设置
This commit is contained in:
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +1,25 @@
|
||||
use tauri::{
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
menu::{MenuBuilder, MenuItemBuilder},
|
||||
Manager, LogicalSize, Emitter,
|
||||
Manager, Emitter,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
mod api;
|
||||
mod audio;
|
||||
use api::ApiController;
|
||||
use audio::AppAudio;
|
||||
|
||||
static ALLOW_EXIT: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
// 窗口最小尺寸
|
||||
window.set_min_size(Some(LogicalSize::new(1280.0, 700.0)))?;
|
||||
|
||||
// 注入控制器
|
||||
let api_controller = ApiController::new();
|
||||
let app_data_dir = app.path().app_data_dir().expect("无法获取应用数据目录");
|
||||
let api_controller = ApiController::new(app_data_dir);
|
||||
app.manage(api_controller);
|
||||
|
||||
let audio_controller = audio::AudioController::new(app.handle().clone());
|
||||
@ -66,7 +67,10 @@ pub fn run() {
|
||||
let _ = app.emit("tray-prev", ());
|
||||
}
|
||||
"quit" => {
|
||||
app.exit(0);
|
||||
ALLOW_EXIT.store(true, Ordering::SeqCst);
|
||||
if let Some(w) = app.get_webview_window("main") {
|
||||
let _ = w.close();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@ -87,11 +91,15 @@ pub fn run() {
|
||||
.build(app)?;
|
||||
|
||||
// 点击关闭按钮时隐藏到托盘
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
let window_clone = window.clone();
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api: close_api, .. } = event {
|
||||
close_api.prevent_close(); // 阻止窗口关闭
|
||||
let _ = window_clone.hide(); // 隐藏到托盘
|
||||
if ALLOW_EXIT.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
close_api.prevent_close();
|
||||
let _ = window_clone.hide();
|
||||
}
|
||||
});
|
||||
|
||||
@ -115,6 +123,13 @@ pub fn run() {
|
||||
api::create_qr,
|
||||
api::check_qr_status,
|
||||
api::get_login_status,
|
||||
api::likelist,
|
||||
api::user_record,
|
||||
api::like_song,
|
||||
api::record_recent_song,
|
||||
api::playlist_subscribe,
|
||||
api::playlist_track_all,
|
||||
api::exit_app,
|
||||
|
||||
audio::play_audio,
|
||||
audio::pause_audio,
|
||||
@ -125,6 +140,7 @@ pub fn run() {
|
||||
audio::seek_audio,
|
||||
audio::set_volume
|
||||
])
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running Nekosonic");
|
||||
}
|
||||
Reference in New Issue
Block a user