Skip to content

Commit

Permalink
✨ feat: cache ncm song
Browse files Browse the repository at this point in the history
Signed-off-by: SimonShiki <[email protected]>
  • Loading branch information
SimonShiki committed Aug 13, 2024
1 parent de3444c commit 474045f
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 12 deletions.
28 changes: 28 additions & 0 deletions src-tauri/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
BSD 3-Clause License

Copyright (c) 2024, Simon Shiki

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
137 changes: 137 additions & 0 deletions src-tauri/src/cache_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tauri::State;

pub struct CacheManagerState(pub Arc<CacheManager>);

pub struct CacheManager {
cache_dir: PathBuf,
max_cache_size: u64,
current_cache_size: Mutex<u64>,
cache_items: Mutex<HashMap<u64, CacheItem>>,
}

struct CacheItem {
id: u64,
path: PathBuf,
size: u64,
last_accessed: std::time::SystemTime,
}

impl CacheManager {
pub fn new(cache_dir: PathBuf, max_cache_size: u64) -> Self {
let cm = CacheManager {
cache_dir,
max_cache_size,
current_cache_size: Mutex::new(0),
cache_items: Mutex::new(HashMap::new()),
};
cm.init();
cm
}

fn init(&self) {
if !self.cache_dir.exists() {
fs::create_dir_all(&self.cache_dir).expect("Failed to create cache directory");
}
self.load_cache_info();
}

fn load_cache_info(&self) {
let mut current_size = 0;
let mut items = HashMap::new();

if let Ok(entries) = fs::read_dir(&self.cache_dir) {
for entry in entries.flatten() {
if let Ok(metadata) = entry.metadata() {
if metadata.is_file() {
let id = entry
.file_name()
.to_str()
.and_then(|s| s.split('.').next())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);

let item = CacheItem {
id,
path: entry.path(),
size: metadata.len(),
last_accessed: metadata.modified().unwrap_or_else(|_| std::time::SystemTime::now()),
};

current_size += item.size;
items.insert(id, item);
}
}
}
}

*self.current_cache_size.lock().unwrap() = current_size;
*self.cache_items.lock().unwrap() = items;
}

pub fn get_cached_song(&self, id: u64) -> Option<Vec<u8>> {
let mut items = self.cache_items.lock().unwrap();
if let Some(item) = items.get_mut(&id) {
item.last_accessed = std::time::SystemTime::now();
fs::read(&item.path).ok()
} else {
None
}
}

pub fn cache_song(&self, id: u64, data: &[u8]) -> Result<(), String> {
let file_path = self.cache_dir.join(format!("{}.mp3", id));
self.ensure_space_available(data.len() as u64)?;

fs::write(&file_path, data).map_err(|e| e.to_string())?;

let mut items = self.cache_items.lock().unwrap();
let mut current_size = self.current_cache_size.lock().unwrap();

items.insert(
id,
CacheItem {
id,
path: file_path,
size: data.len() as u64,
last_accessed: std::time::SystemTime::now(),
},
);
*current_size += data.len() as u64;

Ok(())
}

fn ensure_space_available(&self, required_space: u64) -> Result<(), String> {
let mut current_size = self.current_cache_size.lock().unwrap();
let mut items = self.cache_items.lock().unwrap();

while *current_size + required_space > self.max_cache_size && !items.is_empty() {
let oldest_id = items
.iter()
.min_by_key(|(_, item)| item.last_accessed)
.map(|(id, _)| *id)
.ok_or_else(|| "No items to remove".to_string())?;

if let Some(item) = items.remove(&oldest_id) {
fs::remove_file(&item.path).map_err(|e| e.to_string())?;
*current_size -= item.size;
}
}

Ok(())
}
}

#[tauri::command]
pub fn get_cached_song(id: u64, state: State<CacheManagerState>) -> Option<Vec<u8>> {
state.0.get_cached_song(id)
}

#[tauri::command]
pub fn cache_song(id: u64, data: Vec<u8>, state: State<CacheManagerState>) -> Result<(), String> {
state.0.cache_song(id, &data)
}
8 changes: 8 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ mod audio;
mod media_control;
mod error;
mod local_scanner;
mod cache_manager;

use audio::AudioState;
use cache_manager::{CacheManager, CacheManagerState};
use media_control::MediaControlState;
use rodio::OutputStream;
use std::sync::{Arc, Condvar, Mutex, Once};
Expand Down Expand Up @@ -38,6 +40,10 @@ pub async fn run() {
.manage(audio_state)
.manage(media_control_state)
.setup(|app| {
let cache_dir = app.path().app_cache_dir().unwrap();
let cache_manager = Arc::new(CacheManager::new(cache_dir, 1024 * 1024 * 1024)); // 1GB cache limit
app.manage(CacheManagerState(cache_manager));

let show = MenuItemBuilder::with_id("show", "Show").build(app)?;
let pause_resume = MenuItemBuilder::with_id("pause_resume", "Pause/Resume").build(app)?;
let quit = MenuItemBuilder::with_id("quit", "Quit").build(app)?;
Expand Down Expand Up @@ -107,6 +113,8 @@ pub async fn run() {
media_control::update_playback_status,
local_scanner::get_song_buffer,
local_scanner::scan_folder,
cache_manager::get_cached_song,
cache_manager::cache_song,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
2 changes: 1 addition & 1 deletion src/jotais/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface Song<From extends string> {
export interface AbstractStorage {
scan(): Promise<void>;
getMusicStream?(id: string | number): AsyncGenerator<ArrayLike<unknown>, void, unknown>;
getMusicBuffer?(id: string | number): Promise<ArrayBuffer>;
getMusicBuffer?(id: string | number): Promise<number[]>;
}

export interface StorageMeta {
Expand Down
14 changes: 9 additions & 5 deletions src/storages/ncm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { AbstractStorage, Song, storagesJotai } from '../jotais/storage';
import { mergeDeep } from '../utils/merge-deep';
import type { SetStateAction, WritableAtom } from 'jotai';
import { backendStorage } from '../utils/local-utitity';
import { fetchArraybuffer } from '../utils/chunk-transformer';
import { fetchArraybuffer, fetchBuffer } from '../utils/chunk-transformer';

Check failure on line 8 in src/storages/ncm.ts

View workflow job for this annotation

GitHub Actions / publish-tauri (windows-latest)

'"../utils/chunk-transformer"' has no exported member named 'fetchArraybuffer'. Did you mean 'fetchBuffer'?
import { currentSongJotai } from '../jotais/play';
import { mergeLyrics } from '../utils/lyric-parser';
import { cacheSong, getCachedSong } from '../utils/cache.';

interface NCMSearchResult {
id: number;
Expand Down Expand Up @@ -393,9 +394,9 @@ export class NCM implements AbstractStorage {
private async getMusicURL (id: number, quality = this.config.defaultQuality) {
const res = await fetch(`${this.config.api}song/url/v1?id=${id}&level=${quality}${this.config.cookie ? `&cookie=${this.config.cookie}` : ''}`);
const { data } = await res.json();
const { url } = data[0];
if (!url) throw new Error(`Cannot get url for ${id}`);
return url as string;
const song = data[0];
if (!song.url) throw new Error(`Cannot get url for ${id}:\n ${JSON.stringify(song)}`);
return song.url as string;
}

async * getMusicStream (id: number, quality = this.config.defaultQuality) {
Expand All @@ -413,8 +414,11 @@ export class NCM implements AbstractStorage {
}

async getMusicBuffer (id: number, quality = this.config.defaultQuality) {
const cached = await getCachedSong(id);
if (cached) return cached;
const url = await this.getMusicURL(id, quality);
const buffer = await fetchArraybuffer(url);
const buffer = await fetchBuffer(url);
cacheSong(id, buffer);
return buffer;
}

Expand Down
9 changes: 9 additions & 0 deletions src/utils/cache..ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { invoke } from '@tauri-apps/api/core';

export async function getCachedSong (id: number) {
return await invoke<number[] | null>('get_cached_song', {id});
}

export async function cacheSong (id: number, data: number[]) {
await invoke('cache_song', {id, data});
}
4 changes: 2 additions & 2 deletions src/utils/chunk-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ export const transformChunk = greenlet(
}
);

export const fetchArraybuffer = greenlet(
export const fetchBuffer = greenlet(
async (url: string) => {
const res = await fetch(url);
const buffer = await res.arrayBuffer();
return buffer;
return Array.from(new Uint8Array(buffer));
}
);
14 changes: 10 additions & 4 deletions src/utils/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ async function playCurrentSong () {
if (currentSong.storage === 'local') {
await invoke('play_local_file', { filePath: currentSong.path });
await invoke('set_volume', { volume: volumeToFactor(volume) });
sharedStore.set(bufferingJotai, false);
sharedStore.set(backendPlayingJotai, true);
} else {
const storages = sharedStore.get(storagesJotai);
Expand Down Expand Up @@ -117,7 +118,7 @@ async function playCurrentSong () {
console.warn('buffer interrupted');
return;
}
await invoke('play_arraybuffer', { buffer: await transformChunk(buffer) });
await invoke('play_arraybuffer', { buffer });
await invoke('set_volume', { volume: volumeToFactor(volume) });
sharedStore.set(backendPlayingJotai, true);
sharedStore.set(bufferingJotai, false);
Expand Down Expand Up @@ -170,13 +171,18 @@ function setupEventListeners () {
}
});

let prevProgress: number;
sharedStore.sub(progressJotai, () => {
const progress = sharedStore.get(progressJotai);
const currentSong = sharedStore.get(currentSongJotai);
if (!currentSong || !currentSong.duration) return;
webviewWindow.WebviewWindow.getCurrent().setProgressBar({
progress: Math.ceil(progress / currentSong.duration * 100000)
});
const percentage = Math.ceil(progress / currentSong.duration * 100000);
if (prevProgress !== percentage) {
prevProgress = percentage;
webviewWindow.WebviewWindow.getCurrent().setProgressBar({
progress: percentage
});
}
});

sharedStore.sub(playingJotai, async () => {
Expand Down

0 comments on commit 474045f

Please sign in to comment.