Преглед изворни кода

Split cache handling to separate module.

Use it for audio keys and album covers as well.
Paul Lietar пре 9 година
родитељ
комит
85903a0da5
12 измењених фајлова са 293 додато и 138 уклоњено
  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 { }
+