Browse Source

Split cache handling to separate module.

Use it for audio keys and album covers as well.
Paul Lietar 10 years ago
parent
commit
85903a0da5
12 changed files with 293 additions and 138 deletions
  1. 58 0
      Cargo.lock
  2. 1 0
      Cargo.toml
  3. 18 72
      src/audio_file.rs
  4. 12 33
      src/audio_key.rs
  5. 2 3
      src/authentication.rs
  6. 103 0
      src/cache/default_cache.rs
  7. 27 0
      src/cache/mod.rs
  8. 1 0
      src/lib.in.rs
  9. 1 0
      src/lib.rs
  10. 9 14
      src/main.rs
  11. 57 16
      src/session.rs
  12. 4 0
      src/util/mod.rs

+ 58 - 0
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"

+ 1 - 0
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"

+ 18 - 72
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<BitSet>,
 }
 
-impl AudioFileLoading {
-    fn new(session: &Session, file_id: FileId) -> AudioFileLoading {
+impl AudioFile {
+    pub fn new(session: &Session, file_id: FileId)
+        -> (AudioFile, eventual::Future<NamedTempFile, ()>) {
+
         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<AudioFileShared>,
              mut write_file: NamedTempFile,
-             seek_rx: mpsc::Receiver<u64>) {
+             seek_rx: mpsc::Receiver<u64>,
+             complete_tx: eventual::Complete<NamedTempFile, ()>) {
         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<usize> {
         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<u64> {
         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<usize> {
-        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<u64> {
-        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<PathBuf> {
-        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))
-        })
-    }
-}

+ 12 - 33
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<eventual::Complete<AudioKey, AudioKeyError>>),
-    Loaded(AudioKey),
-    Failed(AudioKeyError),
-}
-
 pub struct AudioKeyManager {
     next_seq: u32,
     pending: HashMap<u32, AudioKeyId>,
-    cache: HashMap<AudioKeyId, AudioKeyStatus>,
+    cache: HashMap<AudioKeyId, Vec<eventual::Complete<AudioKey, AudioKeyError>>>,
 }
 
 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::<BigEndian>().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);
                 }
             }
         }

+ 2 - 3
src/authentication.rs

@@ -128,9 +128,8 @@ impl Credentials {
         json::decode::<StoredCredentials>(&contents).unwrap().into()
     }
 
-    pub fn from_file<P: AsRef<Path>>(path: P) -> Credentials {
-        let file = File::open(path).unwrap();
-        Credentials::from_reader(file)
+    pub fn from_file<P: AsRef<Path>>(path: P) -> Option<Credentials> {
+        File::open(path).ok().map(Credentials::from_reader)
     }
 
     pub fn save_to_writer<W: Write>(&self, writer: &mut W) {

+ 103 - 0
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<DefaultCache, ()> {
+        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<lmdb::DbHandle> {
+        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<AudioKey> {
+        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<Vec<_>> = 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<Credentials> {
+        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<Box<ReadSeek>> {
+        File::open(self.file_path(file)).ok().map(|f| Box::new(f) as Box<ReadSeek>)
+    }
+
+    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();
+    }
+}

+ 27 - 0
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<AudioKey> {
+        None
+    }
+    fn put_audio_key(&self, _track: SpotifyId, _file: FileId, _audio_key: AudioKey) { }
+
+    fn get_credentials(&self) -> Option<Credentials> {
+        None
+    }
+    fn put_credentials(&self, _cred: &Credentials) { }
+
+    fn get_file(&self, _file: FileId) -> Option<Box<ReadSeek>> {
+        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;

+ 1 - 0
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;

+ 1 - 0
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;

+ 9 - 14
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<Cache + Send + Sync>
+    }).unwrap_or_else(|| Box::new(NoCache) as Box<Cache + Send + Sync>);
+
     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);

+ 57 - 16
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<u8>,
     pub user_agent: String,
     pub device_name: String,
-    pub cache_location: Option<PathBuf>,
     pub bitrate: Bitrate,
 }
 
@@ -48,11 +49,11 @@ pub struct SessionInternal {
     config: Config,
     data: RwLock<SessionData>,
 
+    cache: Box<Cache + Send + Sync>,
     mercury: Mutex<MercuryManager>,
     metadata: Mutex<MetadataManager>,
     stream: Mutex<StreamManager>,
     audio_key: Mutex<AudioKeyManager>,
-    audio_file: Mutex<AudioFileManager>,
     rx_connection: Mutex<Option<CipherConnection>>,
     tx_connection: Mutex<Option<CipherConnection>>,
 }
@@ -61,11 +62,7 @@ pub struct SessionInternal {
 pub struct Session(pub Arc<SessionInternal>);
 
 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<Cache + Send + Sync>) -> 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<AudioKey, AudioKeyError> {
-        self.0.audio_key.lock().unwrap().request(self, track, file)
+    pub fn audio_key(&self, track: SpotifyId, file_id: FileId) -> Future<AudioKey, AudioKeyError> {
+        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_id: FileId) -> Box<ReadSeek> {
+        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 audio_file(&self, file: FileId) -> AudioFile {
-        self.0.audio_file.lock().unwrap().request(self, file)
+    pub fn album_cover(&self, file_id: FileId) -> eventual::Future<Vec<u8>, ()> {
+        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<StreamEvent, StreamError> {
@@ -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
     }

+ 4 - 0
src/util/mod.rs

@@ -129,3 +129,7 @@ impl<'s> Iterator for StrChunks<'s> {
         }
     }
 }
+
+pub trait ReadSeek : ::std::io::Read + ::std::io::Seek { }
+impl <T: ::std::io::Read + ::std::io::Seek> ReadSeek for T { }
+