use std::collections::HashMap; use std::sync::Mutex; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use melodl::SongInfo; use serde::Serialize; use uuid::Uuid; use crate::music::SongView; pub fn now_ts() -> f64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs_f64() } pub fn gen_id(len: usize) -> String { Uuid::new_v4().simple().to_string()[..len].to_string() } /// TTL 缓存:惰性过期,put 时顺手 GC pub struct TtlCache { ttl: Duration, data: Mutex>, } impl TtlCache { pub fn new(ttl_secs: u64) -> Self { Self { ttl: Duration::from_secs(ttl_secs), data: Mutex::new(HashMap::new()), } } pub fn put(&self, key: impl Into, value: T) { let mut data = self.data.lock().unwrap(); data.retain(|_, (ts, _)| ts.elapsed() <= self.ttl); data.insert(key.into(), (Instant::now(), value)); } pub fn get(&self, key: &str) -> Option { let mut data = self.data.lock().unwrap(); match data.get(key) { Some((ts, _)) if ts.elapsed() > self.ttl => { data.remove(key); None } Some((_, v)) => Some(v.clone()), None => None, } } } /// 异步搜索任务注册表 + 结果缓存(TTL 内同关键词去重) pub struct SearchStore { ttl: Duration, inner: Mutex, } #[derive(Default)] struct SearchStoreInner { jobs: HashMap, by_keyword: HashMap, } #[derive(Debug, Clone, Serialize)] pub struct SearchJob { pub id: String, pub keyword: String, pub status: String, // pending / running / done / failed pub error: Option, pub created_at: f64, pub started_at: Option, pub finished_at: Option, pub elapsed: Option, pub sources_ok: Vec, pub sources_failed: Vec, pub data: Vec, #[serde(skip)] pub raws: Vec, pub last_access: f64, } pub enum CreateOutcome { /// 命中缓存(job 为完整结果) Cached { job_id: String, job: SearchJob }, /// 同关键词已有任务在执行 Running { job_id: String }, /// 新建任务,调用方需入队执行 Created { job_id: String }, } impl SearchStore { pub fn new(ttl_secs: u64) -> Self { Self { ttl: Duration::from_secs(ttl_secs), inner: Mutex::new(SearchStoreInner::default()), } } pub fn create_or_get(&self, keyword: &str, cache: &TtlCache) -> CreateOutcome { let mut inner = self.inner.lock().unwrap(); self.gc(&mut inner); if let Some(job_id) = inner.by_keyword.get(keyword).cloned() { if let Some(job) = inner.jobs.get_mut(&job_id) { match job.status.as_str() { "pending" | "running" => return CreateOutcome::Running { job_id }, "done" => { // 缓存命中:刷新 token 有效期,避免拿到手已过期 for (view, raw) in job.data.iter().zip(job.raws.iter()) { cache.put(view.token.clone(), raw.clone()); } job.last_access = now_ts(); return CreateOutcome::Cached { job_id, job: job.clone(), }; } _ => {} // failed 等:落入下面重建 } } } let job_id = gen_id(12); let job = SearchJob { id: job_id.clone(), keyword: keyword.to_string(), status: "pending".to_string(), error: None, created_at: now_ts(), started_at: None, finished_at: None, elapsed: None, sources_ok: vec![], sources_failed: vec![], data: vec![], raws: vec![], last_access: now_ts(), }; inner.jobs.insert(job_id.clone(), job); inner.by_keyword.insert(keyword.to_string(), job_id.clone()); CreateOutcome::Created { job_id } } pub fn update(&self, job_id: &str, f: impl FnOnce(&mut SearchJob)) { let mut inner = self.inner.lock().unwrap(); if let Some(job) = inner.jobs.get_mut(job_id) { f(job); } } pub fn get(&self, job_id: &str) -> Option { let mut inner = self.inner.lock().unwrap(); let job = inner.jobs.get_mut(job_id)?; if job.status == "running" { if let Some(started) = job.started_at { job.elapsed = Some(((now_ts() - started) * 10.0).round() / 10.0); } } Some(job.clone()) } fn gc(&self, inner: &mut SearchStoreInner) { let now = now_ts(); let ttl = self.ttl.as_secs_f64(); let expired: Vec = inner .jobs .values() .filter(|j| now - j.last_access > ttl) .map(|j| j.id.clone()) .collect(); for jid in expired { if let Some(job) = inner.jobs.remove(&jid) { if inner.by_keyword.get(&job.keyword) == Some(&jid) { inner.by_keyword.remove(&job.keyword); } } } } } /// 下载任务状态表(内存实现,重启清空) #[derive(Default)] pub struct TaskStore { tasks: Mutex>, } #[derive(Debug, Clone, Serialize)] pub struct Task { pub id: String, pub title: String, pub status: String, // queued / matching / downloading / done / failed / unmatched pub file: Option, pub error: Option, pub created_at: f64, pub batch_id: Option, pub source: Option, pub ext: Option, } impl TaskStore { pub fn new() -> Self { Self::default() } pub fn create(&self, title: impl Into, batch_id: Option) -> String { let task_id = gen_id(12); let task = Task { id: task_id.clone(), title: title.into(), status: "queued".to_string(), file: None, error: None, created_at: now_ts(), batch_id, source: None, ext: None, }; self.tasks.lock().unwrap().insert(task_id.clone(), task); task_id } pub fn update(&self, task_id: &str, f: impl FnOnce(&mut Task)) { if let Some(task) = self.tasks.lock().unwrap().get_mut(task_id) { f(task); } } pub fn get(&self, task_id: &str) -> Option { self.tasks.lock().unwrap().get(task_id).cloned() } pub fn list(&self, limit: usize) -> Vec { let mut tasks: Vec = self.tasks.lock().unwrap().values().cloned().collect(); tasks.sort_by(|a, b| b.created_at.total_cmp(&a.created_at)); tasks.truncate(limit); tasks } } /// 串行任务队列:单 consumer 逐个处理,避免并发下载 pub fn spawn_worker(handler: F) -> tokio::sync::mpsc::UnboundedSender where T: Send + 'static, F: Fn(T) -> Fut + Send + Sync + 'static, Fut: std::future::Future + Send + 'static, { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); let handler = std::sync::Arc::new(handler); tokio::spawn(async move { while let Some(item) = rx.recv().await { (handler)(item).await; } }); tx }