Skip to content

Commit

Permalink
Use ttl cache instead of lru (#220)
Browse files Browse the repository at this point in the history
Replace `lru` cache with `ttl` cache.

## Context

Using `ttl` cache avoids the problem that the cached Spotify data becomes invalid after sometimes. For example, daily mix playlist changes the list of tracks everyday, hence playing a song from the cached playlist won't work.
  • Loading branch information
aome510 authored Jul 15, 2023
1 parent 5491fda commit 335ea4b
Show file tree
Hide file tree
Showing 10 changed files with 82 additions and 68 deletions.
48 changes: 17 additions & 31 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion spotify_player/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ librespot-connect = { version = "0.4.2", optional = true }
librespot-playback = { version = "0.4.2", optional = true }
librespot-core = "0.4.2"
log = "0.4.18"
lru = "0.10.0"
chrono = "0.4.26"
reqwest = { version = "0.11.18", features = ["json"] }
rpassword = "7.2.0"
Expand Down Expand Up @@ -47,6 +46,7 @@ serde_json = "1.0.96"
once_cell = "1.17.2"
regex = "1.8.3"
daemonize = { version = "0.5.0", optional = true }
ttl_cache = "0.5.1"

[features]
alsa-backend = ["streaming", "librespot-playback/alsa-backend"]
Expand Down
2 changes: 1 addition & 1 deletion spotify_player/src/client/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ pub async fn start_player_event_watchers(

// request new context's data if not found in memory
if let Some(id) = id {
if !state.data.read().caches.context.contains(&id.uri()) {
if !state.data.read().caches.context.contains_key(&id.uri()) {
client_pub
.send(ClientRequest::GetContext(id.clone()))
.unwrap_or_default();
Expand Down
56 changes: 40 additions & 16 deletions spotify_player/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,17 @@ impl Client {
let client = lyric_finder::Client::from_http_client(&self.http);
let query = format!("{track} {artists}");

if !state.data.read().caches.lyrics.contains(&query) {
if !state.data.read().caches.lyrics.contains_key(&query) {
let result = client.get_lyric(&query).await.context(format!(
"failed to get lyric for track {track} - artists {artists}"
))?;

state.data.write().caches.lyrics.put(query, result);
state
.data
.write()
.caches
.lyrics
.insert(query, result, *CACHE_DURATION);
}
}
ClientRequest::ConnectDevice(id) => {
Expand Down Expand Up @@ -255,44 +260,47 @@ impl Client {
}
ClientRequest::GetUserTopTracks => {
let uri = &USER_TOP_TRACKS_ID.uri;
if !state.data.read().caches.context.contains(uri) {
if !state.data.read().caches.context.contains_key(uri) {
let tracks = self.current_user_top_tracks().await?;
state.data.write().caches.context.put(
state.data.write().caches.context.insert(
uri.to_owned(),
Context::Tracks {
tracks,
desc: "User's top tracks".to_string(),
},
*CACHE_DURATION,
);
}
}
ClientRequest::GetUserSavedTracks => {
let tracks = self.current_user_saved_tracks().await?;
state.data.write().caches.context.put(
state.data.write().caches.context.insert(
USER_LIKED_TRACKS_ID.uri.to_owned(),
Context::Tracks {
tracks: tracks.clone(),
desc: "User's liked tracks".to_string(),
},
*CACHE_DURATION,
);
state.data.write().user_data.saved_tracks = tracks;
}
ClientRequest::GetUserRecentlyPlayedTracks => {
let uri = &USER_RECENTLY_PLAYED_TRACKS_ID.uri;
if !state.data.read().caches.context.contains(uri) {
if !state.data.read().caches.context.contains_key(uri) {
let tracks = self.current_user_recently_played_tracks().await?;
state.data.write().caches.context.put(
state.data.write().caches.context.insert(
uri.to_owned(),
Context::Tracks {
tracks,
desc: "User's recently played tracks".to_string(),
},
*CACHE_DURATION,
);
}
}
ClientRequest::GetContext(context) => {
let uri = context.uri();
if !state.data.read().caches.context.contains(&uri) {
if !state.data.read().caches.context.contains_key(&uri) {
let context = match context {
ContextId::Playlist(playlist_id) => {
self.playlist_context(playlist_id).await?
Expand All @@ -306,30 +314,41 @@ impl Client {
}
};

state.data.write().caches.context.put(uri, context);
state
.data
.write()
.caches
.context
.insert(uri, context, *CACHE_DURATION);
}
}
ClientRequest::Search(query) => {
if !state.data.read().caches.search.contains(&query) {
if !state.data.read().caches.search.contains_key(&query) {
let results = self.search(&query).await?;

state.data.write().caches.search.put(query, results);
state
.data
.write()
.caches
.search
.insert(query, results, *CACHE_DURATION);
}
}
ClientRequest::GetRadioTracks {
seed_uri: uri,
seed_name: name,
} => {
let radio_uri = format!("radio:{uri}");
if !state.data.read().caches.context.contains(&radio_uri) {
if !state.data.read().caches.context.contains_key(&radio_uri) {
let tracks = self.radio_tracks(uri).await?;

state.data.write().caches.context.put(
state.data.write().caches.context.insert(
radio_uri,
Context::Tracks {
tracks,
desc: format!("{name} Radio"),
},
*CACHE_DURATION,
);
}
}
Expand Down Expand Up @@ -812,7 +831,7 @@ impl Client {
.await?;

// After adding a new track to a playlist, remove the cache of that playlist to force refetching new data
state.data.write().caches.context.pop(&playlist_id.uri());
state.data.write().caches.context.remove(&playlist_id.uri());

Ok(())
}
Expand Down Expand Up @@ -1185,13 +1204,18 @@ impl Client {
}

#[cfg(feature = "image")]
if !state.data.read().caches.images.contains(url) {
if !state.data.read().caches.images.contains_key(url) {
let bytes = self.retrieve_image(url, &path, false).await?;
// Get the image from a url
let image =
image::load_from_memory(&bytes).context("Failed to load image from memory")?;

state.data.write().caches.images.put(url.to_owned(), image);
state
.data
.write()
.caches
.images
.insert(url.to_owned(), image, *CACHE_DURATION);
}

// notify user about the playback's change if any
Expand Down
2 changes: 1 addition & 1 deletion spotify_player/src/event/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ pub fn handle_key_sequence_for_search_page(
};

let data = state.data.read();
let search_results = data.caches.search.peek(current_query);
let search_results = data.caches.search.get(current_query);

match focus_state {
SearchFocusState::Input => anyhow::bail!("user's search input should be handled before"),
Expand Down
2 changes: 1 addition & 1 deletion spotify_player/src/event/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub fn handle_command_for_focused_context_window(

let data = state.data.read();

match data.caches.context.peek(&context_id.uri()) {
match data.caches.context.get(&context_id.uri()) {
Some(context) => match context {
Context::Artist {
top_tracks,
Expand Down
29 changes: 17 additions & 12 deletions spotify_player/src/state/data.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use std::{collections::HashMap, num::NonZeroUsize};
use std::collections::HashMap;

use once_cell::sync::Lazy;

use super::model::*;

pub type DataReadGuard<'a> = parking_lot::RwLockReadGuard<'a, AppData>;

#[derive(Default, Debug)]
// cache duration, which is default to be 1h
pub static CACHE_DURATION: Lazy<std::time::Duration> =
Lazy::new(|| std::time::Duration::from_secs(60 * 60));

#[derive(Default)]
/// the application's data
pub struct AppData {
pub user_data: UserData,
Expand All @@ -22,15 +28,14 @@ pub struct UserData {
pub saved_tracks: Vec<Track>,
}

#[derive(Debug)]
/// the application's caches
pub struct Caches {
pub context: lru::LruCache<String, Context>,
pub search: lru::LruCache<String, SearchResults>,
pub context: ttl_cache::TtlCache<String, Context>,
pub search: ttl_cache::TtlCache<String, SearchResults>,
#[cfg(feature = "lyric-finder")]
pub lyrics: lru::LruCache<String, lyric_finder::LyricResult>,
pub lyrics: ttl_cache::TtlCache<String, lyric_finder::LyricResult>,
#[cfg(feature = "image")]
pub images: lru::LruCache<String, image::DynamicImage>,
pub images: ttl_cache::TtlCache<String, image::DynamicImage>,
}

#[derive(Default, Debug)]
Expand All @@ -42,19 +47,19 @@ pub struct BrowseData {
impl Default for Caches {
fn default() -> Self {
Self {
context: lru::LruCache::new(NonZeroUsize::new(64).unwrap()),
search: lru::LruCache::new(NonZeroUsize::new(64).unwrap()),
context: ttl_cache::TtlCache::new(64),
search: ttl_cache::TtlCache::new(64),
#[cfg(feature = "lyric-finder")]
lyrics: lru::LruCache::new(NonZeroUsize::new(64).unwrap()),
lyrics: ttl_cache::TtlCache::new(64),
#[cfg(feature = "image")]
images: lru::LruCache::new(NonZeroUsize::new(64).unwrap()),
images: ttl_cache::TtlCache::new(64),
}
}
}

impl AppData {
pub fn get_tracks_by_id_mut(&mut self, id: &ContextId) -> Option<&mut Vec<Track>> {
self.caches.context.peek_mut(&id.uri()).map(|c| match c {
self.caches.context.get_mut(&id.uri()).map(|c| match c {
Context::Album { tracks, .. } => tracks,
Context::Playlist { tracks, .. } => tracks,
Context::Artist {
Expand Down
1 change: 0 additions & 1 deletion spotify_player/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ pub use parking_lot::{Mutex, RwLock};
pub type SharedState = std::sync::Arc<State>;

/// Application's state
#[derive(Debug)]
pub struct State {
pub app_config: config::AppConfig,
pub keymap_config: config::KeymapConfig,
Expand Down
6 changes: 3 additions & 3 deletions spotify_player/src/ui/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub fn render_search_page(
s => anyhow::bail!("expect a search page state, found {s:?}"),
};

let search_results = data.caches.search.peek(current_query);
let search_results = data.caches.search.get(current_query);

// 2. Construct the page's layout
let rect = construct_and_render_block("Search", &ui.theme, state, Borders::ALL, frame, rect);
Expand Down Expand Up @@ -229,7 +229,7 @@ pub fn render_context_page(
};

let data = state.data.read();
match data.caches.context.peek(&id.uri()) {
match data.caches.context.get(&id.uri()) {
Some(context) => {
// render context description
let chunks = Layout::default()
Expand Down Expand Up @@ -511,7 +511,7 @@ pub fn render_lyric_page(
s => anyhow::bail!("expect a lyric page state, found {s:?}"),
};

let (desc, lyric) = match data.caches.lyrics.peek(&format!("{track} {artists}")) {
let (desc, lyric) = match data.caches.lyrics.get(&format!("{track} {artists}")) {
None => {
frame.render_widget(Paragraph::new("Loading..."), rect);
return Ok(());
Expand Down
Loading

0 comments on commit 335ea4b

Please sign in to comment.