From 85903a0da5f1e2088e23916f58fefe0b52d8acde Mon Sep 17 00:00:00 2001 From: Paul Lietar Date: Wed, 16 Mar 2016 04:07:04 +0000 Subject: [PATCH] Split cache handling to separate module. Use it for audio keys and album covers as well. --- Cargo.lock | 58 +++++++++++++++++++++ Cargo.toml | 1 + src/audio_file.rs | 90 +++++++------------------------- src/audio_key.rs | 45 +++++----------- src/authentication.rs | 5 +- src/cache/default_cache.rs | 103 +++++++++++++++++++++++++++++++++++++ src/cache/mod.rs | 27 ++++++++++ src/lib.in.rs | 1 + src/lib.rs | 1 + src/main.rs | 23 ++++----- src/session.rs | 73 ++++++++++++++++++++------ src/util/mod.rs | 4 ++ 12 files changed, 293 insertions(+), 138 deletions(-) create mode 100644 src/cache/default_cache.rs create mode 100644 src/cache/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 22bf842e..6c15c921 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,7 @@ dependencies = [ "json_macros 0.3.0 (git+https://github.com/plietar/json_macros)", "lazy_static 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", "librespot-protocol 0.1.0", + "lmdb-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "num 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", "openssl 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", "portaudio 0.2.0 (git+https://github.com/mvdnes/portaudio-rs)", @@ -32,6 +33,14 @@ dependencies = [ "vorbis 0.0.14 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "aho-corasick" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ascii" version = "0.5.4" @@ -63,6 +72,11 @@ name = "bitflags" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "bitflags" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "blastfig" version = "0.3.3" @@ -283,6 +297,14 @@ name = "libc" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "liblmdb-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "librespot-protocol" version = "0.1.0" @@ -299,6 +321,18 @@ dependencies = [ "pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "lmdb-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "liblmdb-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.58 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "log" version = "0.3.5" @@ -312,6 +346,14 @@ name = "matches" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "memchr" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "mime" version = "0.1.3" @@ -472,6 +514,17 @@ dependencies = [ "libc 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "regex" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "regex-syntax" version = "0.3.0" @@ -745,6 +798,11 @@ dependencies = [ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "utf8-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "uuid" version = "0.1.18" diff --git a/Cargo.toml b/Cargo.toml index 5327d5a0..415f1b58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ getopts = "~0.2.14" hyper = { version = "0.7.2", default-features = false } #json_macros = "~0.3.0" lazy_static = "~0.1.15" +lmdb-rs = "0.7.0" num = "~0.1.30" protobuf = "~1.0.15" rand = "~0.3.13" diff --git a/src/audio_file.rs b/src/audio_file.rs index bd2ed395..0c47063c 100644 --- a/src/audio_file.rs +++ b/src/audio_file.rs @@ -1,26 +1,21 @@ use bit_set::BitSet; use byteorder::{ByteOrder, BigEndian}; +use eventual; use std::cmp::min; use std::sync::{Arc, Condvar, Mutex}; use std::sync::mpsc::{self, TryRecvError}; use std::thread; use std::fs; use std::io::{self, Read, Write, Seek, SeekFrom}; -use std::path::PathBuf; use tempfile::NamedTempFile; -use util::{FileId, IgnoreExt, mkdir_existing}; +use util::{FileId, IgnoreExt}; use session::Session; use stream::StreamEvent; const CHUNK_SIZE: usize = 0x20000; -pub enum AudioFile { - Direct(fs::File), - Loading(AudioFileLoading), -} - -pub struct AudioFileLoading { +pub struct AudioFile { read_file: fs::File, position: u64, @@ -31,14 +26,15 @@ pub struct AudioFileLoading { struct AudioFileShared { file_id: FileId, - size: usize, chunk_count: usize, cond: Condvar, bitmap: Mutex, } -impl AudioFileLoading { - fn new(session: &Session, file_id: FileId) -> AudioFileLoading { +impl AudioFile { + pub fn new(session: &Session, file_id: FileId) + -> (AudioFile, eventual::Future) { + let size = session.stream(file_id, 0, 1) .iter() .filter_map(|event| { @@ -54,10 +50,8 @@ impl AudioFileLoading { let chunk_count = (size + CHUNK_SIZE - 1) / CHUNK_SIZE; - let shared = Arc::new(AudioFileShared { file_id: file_id, - size: size, chunk_count: chunk_count, cond: Condvar::new(), bitmap: Mutex::new(BitSet::with_capacity(chunk_count)), @@ -68,27 +62,29 @@ impl AudioFileLoading { let read_file = write_file.reopen().unwrap(); let (seek_tx, seek_rx) = mpsc::channel(); + let (complete_tx, complete_rx) = eventual::Future::pair(); { let shared = shared.clone(); let session = session.clone(); - thread::spawn(move || AudioFileLoading::fetch(&session, shared, write_file, seek_rx)); + thread::spawn(move || AudioFile::fetch(&session, shared, write_file, seek_rx, complete_tx)); } - AudioFileLoading { + (AudioFile { read_file: read_file, position: 0, seek: seek_tx, shared: shared, - } + }, complete_rx) } fn fetch(session: &Session, shared: Arc, mut write_file: NamedTempFile, - seek_rx: mpsc::Receiver) { + seek_rx: mpsc::Receiver, + complete_tx: eventual::Complete) { let mut index = 0; loop { @@ -103,7 +99,8 @@ impl AudioFileLoading { let bitmap = shared.bitmap.lock().unwrap(); if bitmap.len() >= shared.chunk_count { drop(bitmap); - AudioFileLoading::persist_to_cache(session, &shared, &mut write_file); + write_file.seek(SeekFrom::Start(0)).unwrap(); + complete_tx.complete(write_file); break; } @@ -112,7 +109,7 @@ impl AudioFileLoading { } drop(bitmap); - AudioFileLoading::fetch_chunk(session, &shared, &mut write_file, index); + AudioFile::fetch_chunk(session, &shared, &mut write_file, index); } } @@ -149,19 +146,9 @@ impl AudioFileLoading { shared.cond.notify_all(); } - - fn persist_to_cache(session: &Session, shared: &AudioFileShared, write_file: &mut NamedTempFile) { - if let Some(path) = AudioFileManager::cache_path(session, shared.file_id) { - write_file.seek(SeekFrom::Start(0)).unwrap(); - mkdir_existing(path.parent().unwrap()).unwrap(); - - let mut cache_file = fs::File::create(path).unwrap(); - io::copy(write_file, &mut cache_file).unwrap(); - } - } } -impl Read for AudioFileLoading { +impl Read for AudioFile { fn read(&mut self, output: &mut [u8]) -> io::Result { let index = self.position as usize / CHUNK_SIZE; let offset = self.position as usize % CHUNK_SIZE; @@ -181,7 +168,7 @@ impl Read for AudioFileLoading { } } -impl Seek for AudioFileLoading { +impl Seek for AudioFile { fn seek(&mut self, pos: SeekFrom) -> io::Result { self.position = try!(self.read_file.seek(pos)); @@ -192,44 +179,3 @@ impl Seek for AudioFileLoading { Ok(self.position as u64) } } - -impl Read for AudioFile { - fn read(&mut self, output: &mut [u8]) -> io::Result { - match *self { - AudioFile::Direct(ref mut file) => file.read(output), - AudioFile::Loading(ref mut loading) => loading.read(output), - } - } -} - -impl Seek for AudioFile { - fn seek(&mut self, pos: io::SeekFrom) -> io::Result { - match *self { - AudioFile::Direct(ref mut file) => file.seek(pos), - AudioFile::Loading(ref mut loading) => loading.seek(pos), - } - } -} - -pub struct AudioFileManager; -impl AudioFileManager { - pub fn new() -> AudioFileManager { - AudioFileManager - } - - pub fn cache_path(session: &Session, file_id: FileId) -> Option { - session.config().cache_location.as_ref().map(|cache| { - let name = file_id.to_base16(); - cache.join(&name[0..2]).join(&name[2..]) - }) - } - - pub fn request(&mut self, session: &Session, file_id: FileId) -> AudioFile { - let cache_path = AudioFileManager::cache_path(session, file_id); - let cache_file = cache_path.and_then(|p| fs::File::open(p).ok()); - - cache_file.map(AudioFile::Direct).unwrap_or_else(|| { - AudioFile::Loading(AudioFileLoading::new(session, file_id)) - }) - } -} diff --git a/src/audio_key.rs b/src/audio_key.rs index ade12997..d48c7ec3 100644 --- a/src/audio_key.rs +++ b/src/audio_key.rs @@ -2,7 +2,6 @@ use byteorder::{BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt}; use eventual; use std::collections::HashMap; use std::io::{Cursor, Read, Write}; -use std::mem; use util::{SpotifyId, FileId}; use session::Session; @@ -12,19 +11,13 @@ pub type AudioKey = [u8; 16]; #[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)] pub struct AudioKeyError; -#[derive(Debug,Hash,PartialEq,Eq,Clone)] +#[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)] struct AudioKeyId(SpotifyId, FileId); -enum AudioKeyStatus { - Loading(Vec>), - Loaded(AudioKey), - Failed(AudioKeyError), -} - pub struct AudioKeyManager { next_seq: u32, pending: HashMap, - cache: HashMap, + cache: HashMap>>, } impl AudioKeyManager { @@ -60,23 +53,17 @@ impl AudioKeyManager { let id = AudioKeyId(track, file); self.cache .get_mut(&id) - .map(|status| { - match *status { - AudioKeyStatus::Failed(error) => eventual::Future::error(error), - AudioKeyStatus::Loaded(key) => eventual::Future::of(key), - AudioKeyStatus::Loading(ref mut req) => { - let (tx, rx) = eventual::Future::pair(); - req.push(tx); - rx - } - } + .map(|ref mut requests| { + let (tx, rx) = eventual::Future::pair(); + requests.push(tx); + rx }) .unwrap_or_else(|| { let seq = self.send_key_request(session, track, file); self.pending.insert(seq, id.clone()); let (tx, rx) = eventual::Future::pair(); - self.cache.insert(id, AudioKeyStatus::Loading(vec![tx])); + self.cache.insert(id, vec![tx]); rx }) } @@ -87,26 +74,18 @@ impl PacketHandler for AudioKeyManager { let mut data = Cursor::new(data); let seq = data.read_u32::().unwrap(); - if let Some(status) = self.pending.remove(&seq).and_then(|id| self.cache.get_mut(&id)) { + if let Some(callbacks) = self.pending.remove(&seq).and_then(|id| self.cache.remove(&id)) { if cmd == 0xd { let mut key = [0u8; 16]; data.read_exact(&mut key).unwrap(); - let status = mem::replace(status, AudioKeyStatus::Loaded(key)); - - if let AudioKeyStatus::Loading(cbs) = status { - for cb in cbs { - cb.complete(key); - } + for cb in callbacks { + cb.complete(key); } } else if cmd == 0xe { let error = AudioKeyError; - let status = mem::replace(status, AudioKeyStatus::Failed(error)); - - if let AudioKeyStatus::Loading(cbs) = status { - for cb in cbs { - cb.fail(error); - } + for cb in callbacks { + cb.fail(error); } } } diff --git a/src/authentication.rs b/src/authentication.rs index e6a80642..50fef6fc 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -128,9 +128,8 @@ impl Credentials { json::decode::(&contents).unwrap().into() } - pub fn from_file>(path: P) -> Credentials { - let file = File::open(path).unwrap(); - Credentials::from_reader(file) + pub fn from_file>(path: P) -> Option { + File::open(path).ok().map(Credentials::from_reader) } pub fn save_to_writer(&self, writer: &mut W) { diff --git a/src/cache/default_cache.rs b/src/cache/default_cache.rs new file mode 100644 index 00000000..34a92e6e --- /dev/null +++ b/src/cache/default_cache.rs @@ -0,0 +1,103 @@ +use lmdb_rs as lmdb; +use lmdb_rs::core::MdbResult; +use std::path::PathBuf; +use std::io::Read; +use std::fs::File; + +use util::{SpotifyId, FileId, ReadSeek, mkdir_existing}; +use authentication::Credentials; +use audio_key::AudioKey; + +use super::Cache; + +pub struct DefaultCache { + environment: lmdb::Environment, + root: PathBuf, +} + +impl DefaultCache { + pub fn new(location: PathBuf) -> Result { + let env = lmdb::EnvBuilder::new().max_dbs(5).open(&location.join("db"), 0o755).unwrap(); + + mkdir_existing(&location).unwrap(); + mkdir_existing(&location.join("files")).unwrap(); + + Ok(DefaultCache { + environment: env, + root: location + }) + } + + fn audio_keys(&self) -> MdbResult { + self.environment.create_db("audio-keys", lmdb::DbFlags::empty()) + } + + fn file_path(&self, file: FileId) -> PathBuf { + let name = file.to_base16(); + self.root.join("files").join(&name[0..2]).join(&name[2..]) + } + + fn credentials_path(&self) -> PathBuf { + self.root.join("credentials.json") + } +} + +impl Cache for DefaultCache { + fn get_audio_key(&self, track: SpotifyId, file: FileId) -> Option { + let reader = self.environment.get_reader().unwrap(); + let handle = self.audio_keys().unwrap(); + let db = reader.bind(&handle); + + let mut key = Vec::new(); + key.extend_from_slice(&track.to_raw()); + key.extend_from_slice(&file.0); + + let value : Option> = db.get(&key).ok(); + value.and_then(|value| if value.len() == 16 { + let mut result = [0u8; 16]; + result.clone_from_slice(&value); + Some(result) + } else { + None + }) + } + + fn put_audio_key(&self, track: SpotifyId, file: FileId, audio_key: AudioKey) { + let xact = self.environment.new_transaction().unwrap(); + let handle = self.audio_keys().unwrap(); + + { + let db = xact.bind(&handle); + + let mut key = Vec::new(); + key.extend_from_slice(&track.to_raw()); + key.extend_from_slice(&file.0); + + db.set(&key, &audio_key.as_ref()).unwrap(); + } + + xact.commit().unwrap(); + } + + fn get_credentials(&self) -> Option { + let path = self.credentials_path(); + Credentials::from_file(path) + } + fn put_credentials(&self, cred: &Credentials) { + let path = self.credentials_path(); + cred.save_to_file(&path); + } + + fn get_file(&self, file: FileId) -> Option> { + File::open(self.file_path(file)).ok().map(|f| Box::new(f) as Box) + } + + fn put_file(&self, file: FileId, contents: &mut Read) { + let path = self.file_path(file); + + mkdir_existing(path.parent().unwrap()).unwrap(); + + let mut cache_file = File::create(path).unwrap(); + ::std::io::copy(contents, &mut cache_file).unwrap(); + } +} diff --git a/src/cache/mod.rs b/src/cache/mod.rs new file mode 100644 index 00000000..37bd00a5 --- /dev/null +++ b/src/cache/mod.rs @@ -0,0 +1,27 @@ +use util::{SpotifyId, FileId, ReadSeek}; +use audio_key::AudioKey; +use authentication::Credentials; +use std::io::Read; + +pub trait Cache { + fn get_audio_key(&self, _track: SpotifyId, _file: FileId) -> Option { + None + } + fn put_audio_key(&self, _track: SpotifyId, _file: FileId, _audio_key: AudioKey) { } + + fn get_credentials(&self) -> Option { + None + } + fn put_credentials(&self, _cred: &Credentials) { } + + fn get_file(&self, _file: FileId) -> Option> { + None + } + fn put_file(&self, _file: FileId, _contents: &mut Read) { } +} + +pub struct NoCache; +impl Cache for NoCache { } + +mod default_cache; +pub use self::default_cache::DefaultCache; diff --git a/src/lib.in.rs b/src/lib.in.rs index e2eab057..0ce09a20 100644 --- a/src/lib.in.rs +++ b/src/lib.in.rs @@ -5,6 +5,7 @@ mod audio_file; mod audio_key; pub mod audio_sink; pub mod authentication; +pub mod cache; mod connection; mod diffie_hellman; pub mod discovery; diff --git a/src/lib.rs b/src/lib.rs index 8e8d8fcb..2a277cb4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ extern crate byteorder; extern crate crypto; extern crate eventual; extern crate hyper; +extern crate lmdb_rs; extern crate num; extern crate portaudio; extern crate protobuf; diff --git a/src/main.rs b/src/main.rs index 372630b7..b4338ee9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ use librespot::player::Player; use librespot::session::{Bitrate, Config, Session}; use librespot::spirc::SpircManager; use librespot::util::version::version_string; +use librespot::cache::{Cache, DefaultCache, NoCache}; #[cfg(feature = "facebook")] use librespot::facebook::facebook_login; @@ -76,9 +77,12 @@ fn main() { }).or_else(|| APPKEY.map(ToOwned::to_owned)).unwrap(); let username = matches.opt_str("u"); - let cache_location = matches.opt_str("c").map(PathBuf::from); let name = matches.opt_str("n").unwrap(); + let cache = matches.opt_str("c").map(|cache_location| { + Box::new(DefaultCache::new(PathBuf::from(cache_location)).unwrap()) as Box + }).unwrap_or_else(|| Box::new(NoCache) as Box); + let bitrate = match matches.opt_str("b").as_ref().map(String::as_ref) { None => Bitrate::Bitrate160, // default value @@ -92,13 +96,10 @@ fn main() { application_key: appkey, user_agent: version_string(), device_name: name, - cache_location: cache_location.clone(), bitrate: bitrate, }; - let session = Session::new(config); - - let credentials_path = cache_location.map(|c| c.join("credentials.json")); + let session = Session::new(config, cache); let credentials = username.map(|username| { let password = matches.opt_str("p") @@ -109,7 +110,6 @@ fn main() { read_password().unwrap() }); - Credentials::with_password(username, password) }).or_else(|| { if cfg!(feature = "facebook") && matches.opt_present("facebook") { @@ -117,11 +117,8 @@ fn main() { } else { None } - }).or_else(|| { - credentials_path.as_ref() - .and_then(|p| File::open(p).ok()) - .map(Credentials::from_reader) - }).unwrap_or_else(|| { + }).or_else(|| session.cache().get_credentials()) + .unwrap_or_else(|| { println!("No username provided and no stored credentials, starting discovery ..."); let mut discovery = DiscoveryManager::new(session.clone()); @@ -131,9 +128,7 @@ fn main() { std::env::remove_var(PASSWORD_ENV_NAME); let reusable_credentials = session.login(credentials).unwrap(); - if let Some(path) = credentials_path { - reusable_credentials.save_to_file(path); - } + session.cache().put_credentials(&reusable_credentials); let player = Player::new(session.clone(), || DefaultSink::open()); let spirc = SpircManager::new(session.clone(), player); diff --git a/src/session.rs b/src/session.rs index 0f07b48d..7d52fc4f 100644 --- a/src/session.rs +++ b/src/session.rs @@ -4,25 +4,27 @@ use crypto::hmac::Hmac; use crypto::mac::Mac; use eventual; use eventual::Future; +use eventual::Async; use protobuf::{self, Message}; use rand::thread_rng; use rand::Rng; -use std::io::{Read, Write}; -use std::path::PathBuf; +use std::io::{Read, Write, Cursor}; use std::result::Result; use std::sync::{Mutex, RwLock, Arc, mpsc}; +use album_cover::get_album_cover; use apresolve::apresolve; use audio_key::{AudioKeyManager, AudioKey, AudioKeyError}; -use audio_file::{AudioFileManager, AudioFile}; +use audio_file::AudioFile; use authentication::Credentials; +use cache::Cache; use connection::{self, PlainConnection, CipherConnection, PacketHandler}; use diffie_hellman::DHLocalKeys; use mercury::{MercuryManager, MercuryRequest, MercuryResponse}; use metadata::{MetadataManager, MetadataRef, MetadataTrait}; use protocol; use stream::{ChannelId, StreamManager, StreamEvent, StreamError}; -use util::{self, SpotifyId, FileId, mkdir_existing}; +use util::{self, SpotifyId, FileId, ReadSeek}; pub enum Bitrate { Bitrate96, @@ -34,7 +36,6 @@ pub struct Config { pub application_key: Vec, pub user_agent: String, pub device_name: String, - pub cache_location: Option, pub bitrate: Bitrate, } @@ -48,11 +49,11 @@ pub struct SessionInternal { config: Config, data: RwLock, + cache: Box, mercury: Mutex, metadata: Mutex, stream: Mutex, audio_key: Mutex, - audio_file: Mutex, rx_connection: Mutex>, tx_connection: Mutex>, } @@ -61,11 +62,7 @@ pub struct SessionInternal { pub struct Session(pub Arc); impl Session { - pub fn new(config: Config) -> Session { - if let Some(cache_location) = config.cache_location.as_ref() { - mkdir_existing(cache_location).unwrap(); - } - + pub fn new(config: Config, cache: Box) -> Session { let device_id = { let mut h = Sha1::new(); h.input_str(&config.device_name); @@ -83,11 +80,11 @@ impl Session { rx_connection: Mutex::new(None), tx_connection: Mutex::new(None), + cache: cache, mercury: Mutex::new(MercuryManager::new()), metadata: Mutex::new(MetadataManager::new()), stream: Mutex::new(StreamManager::new()), audio_key: Mutex::new(AudioKeyManager::new()), - audio_file: Mutex::new(AudioFileManager::new()), })) } @@ -267,12 +264,52 @@ impl Session { self.0.tx_connection.lock().unwrap().as_mut().unwrap().send_packet(cmd, data) } - pub fn audio_key(&self, track: SpotifyId, file: FileId) -> Future { - self.0.audio_key.lock().unwrap().request(self, track, file) + pub fn audio_key(&self, track: SpotifyId, file_id: FileId) -> Future { + self.0.cache + .get_audio_key(track, file_id) + .map(Future::of) + .unwrap_or_else(|| { + let self_ = self.clone(); + self.0.audio_key.lock().unwrap() + .request(self, track, file_id) + .map(move |key| { + self_.0.cache.put_audio_key(track, file_id, key); + key + }) + }) } - pub fn audio_file(&self, file: FileId) -> AudioFile { - self.0.audio_file.lock().unwrap().request(self, file) + pub fn audio_file(&self, file_id: FileId) -> Box { + self.0.cache + .get_file(file_id) + .unwrap_or_else(|| { + let (audio_file, complete_rx) = AudioFile::new(self, file_id); + + let self_ = self.clone(); + complete_rx.map(move |mut complete_file| { + self_.0.cache.put_file(file_id, &mut complete_file) + }).fire(); + + Box::new(audio_file) + }) + } + + pub fn album_cover(&self, file_id: FileId) -> eventual::Future, ()> { + self.0.cache + .get_file(file_id) + .map(|mut f| { + let mut data = Vec::new(); + f.read_to_end(&mut data).unwrap(); + Future::of(data) + }) + .unwrap_or_else(|| { + let self_ = self.clone(); + get_album_cover(self, file_id) + .map(move |data| { + self_.0.cache.put_file(file_id, &mut Cursor::new(&data)); + data + }) + }) } pub fn stream(&self, file: FileId, offset: u32, size: u32) -> eventual::Stream { @@ -295,6 +332,10 @@ impl Session { self.0.mercury.lock().unwrap().subscribe(self, uri) } + pub fn cache(&self) -> &Cache { + self.0.cache.as_ref() + } + pub fn config(&self) -> &Config { &self.0.config } diff --git a/src/util/mod.rs b/src/util/mod.rs index 27694587..d353ba67 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -129,3 +129,7 @@ impl<'s> Iterator for StrChunks<'s> { } } } + +pub trait ReadSeek : ::std::io::Read + ::std::io::Seek { } +impl ReadSeek for T { } +