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)",
  "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)",
  "lazy_static 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)",
  "librespot-protocol 0.1.0",
  "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)",
  "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)",
  "openssl 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "portaudio 0.2.0 (git+https://github.com/mvdnes/portaudio-rs)",
  "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)",
  "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]]
 [[package]]
 name = "ascii"
 name = "ascii"
 version = "0.5.4"
 version = "0.5.4"
@@ -63,6 +72,11 @@ name = "bitflags"
 version = "0.3.3"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 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]]
 [[package]]
 name = "blastfig"
 name = "blastfig"
 version = "0.3.3"
 version = "0.3.3"
@@ -283,6 +297,14 @@ name = "libc"
 version = "0.2.8"
 version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 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]]
 [[package]]
 name = "librespot-protocol"
 name = "librespot-protocol"
 version = "0.1.0"
 version = "0.1.0"
@@ -299,6 +321,18 @@ dependencies = [
  "pnacl-build-helper 1.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
  "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]]
 [[package]]
 name = "log"
 name = "log"
 version = "0.3.5"
 version = "0.3.5"
@@ -312,6 +346,14 @@ name = "matches"
 version = "0.1.2"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 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]]
 [[package]]
 name = "mime"
 name = "mime"
 version = "0.1.3"
 version = "0.1.3"
@@ -472,6 +514,17 @@ dependencies = [
  "libc 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "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]]
 [[package]]
 name = "regex-syntax"
 name = "regex-syntax"
 version = "0.3.0"
 version = "0.3.0"
@@ -745,6 +798,11 @@ dependencies = [
  "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "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]]
 [[package]]
 name = "uuid"
 name = "uuid"
 version = "0.1.18"
 version = "0.1.18"

+ 1 - 0
Cargo.toml

@@ -23,6 +23,7 @@ getopts         = "~0.2.14"
 hyper           = { version = "0.7.2", default-features = false }
 hyper           = { version = "0.7.2", default-features = false }
 #json_macros     = "~0.3.0"
 #json_macros     = "~0.3.0"
 lazy_static     = "~0.1.15"
 lazy_static     = "~0.1.15"
+lmdb-rs         = "0.7.0"
 num             = "~0.1.30"
 num             = "~0.1.30"
 protobuf        = "~1.0.15"
 protobuf        = "~1.0.15"
 rand            = "~0.3.13"
 rand            = "~0.3.13"

+ 18 - 72
src/audio_file.rs

@@ -1,26 +1,21 @@
 use bit_set::BitSet;
 use bit_set::BitSet;
 use byteorder::{ByteOrder, BigEndian};
 use byteorder::{ByteOrder, BigEndian};
+use eventual;
 use std::cmp::min;
 use std::cmp::min;
 use std::sync::{Arc, Condvar, Mutex};
 use std::sync::{Arc, Condvar, Mutex};
 use std::sync::mpsc::{self, TryRecvError};
 use std::sync::mpsc::{self, TryRecvError};
 use std::thread;
 use std::thread;
 use std::fs;
 use std::fs;
 use std::io::{self, Read, Write, Seek, SeekFrom};
 use std::io::{self, Read, Write, Seek, SeekFrom};
-use std::path::PathBuf;
 use tempfile::NamedTempFile;
 use tempfile::NamedTempFile;
 
 
-use util::{FileId, IgnoreExt, mkdir_existing};
+use util::{FileId, IgnoreExt};
 use session::Session;
 use session::Session;
 use stream::StreamEvent;
 use stream::StreamEvent;
 
 
 const CHUNK_SIZE: usize = 0x20000;
 const CHUNK_SIZE: usize = 0x20000;
 
 
-pub enum AudioFile {
-    Direct(fs::File),
-    Loading(AudioFileLoading),
-}
-
-pub struct AudioFileLoading {
+pub struct AudioFile {
     read_file: fs::File,
     read_file: fs::File,
 
 
     position: u64,
     position: u64,
@@ -31,14 +26,15 @@ pub struct AudioFileLoading {
 
 
 struct AudioFileShared {
 struct AudioFileShared {
     file_id: FileId,
     file_id: FileId,
-    size: usize,
     chunk_count: usize,
     chunk_count: usize,
     cond: Condvar,
     cond: Condvar,
     bitmap: Mutex<BitSet>,
     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)
         let size = session.stream(file_id, 0, 1)
                           .iter()
                           .iter()
                           .filter_map(|event| {
                           .filter_map(|event| {
@@ -54,10 +50,8 @@ impl AudioFileLoading {
 
 
         let chunk_count = (size + CHUNK_SIZE - 1) / CHUNK_SIZE;
         let chunk_count = (size + CHUNK_SIZE - 1) / CHUNK_SIZE;
 
 
-
         let shared = Arc::new(AudioFileShared {
         let shared = Arc::new(AudioFileShared {
             file_id: file_id,
             file_id: file_id,
-            size: size,
             chunk_count: chunk_count,
             chunk_count: chunk_count,
             cond: Condvar::new(),
             cond: Condvar::new(),
             bitmap: Mutex::new(BitSet::with_capacity(chunk_count)),
             bitmap: Mutex::new(BitSet::with_capacity(chunk_count)),
@@ -68,27 +62,29 @@ impl AudioFileLoading {
         let read_file = write_file.reopen().unwrap();
         let read_file = write_file.reopen().unwrap();
 
 
         let (seek_tx, seek_rx) = mpsc::channel();
         let (seek_tx, seek_rx) = mpsc::channel();
+        let (complete_tx, complete_rx) = eventual::Future::pair();
 
 
         {
         {
             let shared = shared.clone();
             let shared = shared.clone();
             let session = session.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,
             read_file: read_file,
 
 
             position: 0,
             position: 0,
             seek: seek_tx,
             seek: seek_tx,
 
 
             shared: shared,
             shared: shared,
-        }
+        }, complete_rx)
     }
     }
 
 
     fn fetch(session: &Session,
     fn fetch(session: &Session,
              shared: Arc<AudioFileShared>,
              shared: Arc<AudioFileShared>,
              mut write_file: NamedTempFile,
              mut write_file: NamedTempFile,
-             seek_rx: mpsc::Receiver<u64>) {
+             seek_rx: mpsc::Receiver<u64>,
+             complete_tx: eventual::Complete<NamedTempFile, ()>) {
         let mut index = 0;
         let mut index = 0;
 
 
         loop {
         loop {
@@ -103,7 +99,8 @@ impl AudioFileLoading {
             let bitmap = shared.bitmap.lock().unwrap();
             let bitmap = shared.bitmap.lock().unwrap();
             if bitmap.len() >= shared.chunk_count {
             if bitmap.len() >= shared.chunk_count {
                 drop(bitmap);
                 drop(bitmap);
-                AudioFileLoading::persist_to_cache(session, &shared, &mut write_file);
+                write_file.seek(SeekFrom::Start(0)).unwrap();
+                complete_tx.complete(write_file);
                 break;
                 break;
             }
             }
 
 
@@ -112,7 +109,7 @@ impl AudioFileLoading {
             }
             }
             drop(bitmap);
             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();
         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> {
     fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
         let index = self.position as usize / CHUNK_SIZE;
         let index = self.position as usize / CHUNK_SIZE;
         let offset = 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> {
     fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
         self.position = try!(self.read_file.seek(pos));
         self.position = try!(self.read_file.seek(pos));
 
 
@@ -192,44 +179,3 @@ impl Seek for AudioFileLoading {
         Ok(self.position as u64)
         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 eventual;
 use std::collections::HashMap;
 use std::collections::HashMap;
 use std::io::{Cursor, Read, Write};
 use std::io::{Cursor, Read, Write};
-use std::mem;
 
 
 use util::{SpotifyId, FileId};
 use util::{SpotifyId, FileId};
 use session::Session;
 use session::Session;
@@ -12,19 +11,13 @@ pub type AudioKey = [u8; 16];
 #[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)]
 #[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)]
 pub struct AudioKeyError;
 pub struct AudioKeyError;
 
 
-#[derive(Debug,Hash,PartialEq,Eq,Clone)]
+#[derive(Debug,Hash,PartialEq,Eq,Copy,Clone)]
 struct AudioKeyId(SpotifyId, FileId);
 struct AudioKeyId(SpotifyId, FileId);
 
 
-enum AudioKeyStatus {
-    Loading(Vec<eventual::Complete<AudioKey, AudioKeyError>>),
-    Loaded(AudioKey),
-    Failed(AudioKeyError),
-}
-
 pub struct AudioKeyManager {
 pub struct AudioKeyManager {
     next_seq: u32,
     next_seq: u32,
     pending: HashMap<u32, AudioKeyId>,
     pending: HashMap<u32, AudioKeyId>,
-    cache: HashMap<AudioKeyId, AudioKeyStatus>,
+    cache: HashMap<AudioKeyId, Vec<eventual::Complete<AudioKey, AudioKeyError>>>,
 }
 }
 
 
 impl AudioKeyManager {
 impl AudioKeyManager {
@@ -60,23 +53,17 @@ impl AudioKeyManager {
         let id = AudioKeyId(track, file);
         let id = AudioKeyId(track, file);
         self.cache
         self.cache
             .get_mut(&id)
             .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(|| {
             .unwrap_or_else(|| {
                 let seq = self.send_key_request(session, track, file);
                 let seq = self.send_key_request(session, track, file);
                 self.pending.insert(seq, id.clone());
                 self.pending.insert(seq, id.clone());
 
 
                 let (tx, rx) = eventual::Future::pair();
                 let (tx, rx) = eventual::Future::pair();
-                self.cache.insert(id, AudioKeyStatus::Loading(vec![tx]));
+                self.cache.insert(id, vec![tx]);
                 rx
                 rx
             })
             })
     }
     }
@@ -87,26 +74,18 @@ impl PacketHandler for AudioKeyManager {
         let mut data = Cursor::new(data);
         let mut data = Cursor::new(data);
         let seq = data.read_u32::<BigEndian>().unwrap();
         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 {
             if cmd == 0xd {
                 let mut key = [0u8; 16];
                 let mut key = [0u8; 16];
                 data.read_exact(&mut key).unwrap();
                 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 {
             } else if cmd == 0xe {
                 let error = AudioKeyError;
                 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()
         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) {
     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;
 mod audio_key;
 pub mod audio_sink;
 pub mod audio_sink;
 pub mod authentication;
 pub mod authentication;
+pub mod cache;
 mod connection;
 mod connection;
 mod diffie_hellman;
 mod diffie_hellman;
 pub mod discovery;
 pub mod discovery;

+ 1 - 0
src/lib.rs

@@ -15,6 +15,7 @@ extern crate byteorder;
 extern crate crypto;
 extern crate crypto;
 extern crate eventual;
 extern crate eventual;
 extern crate hyper;
 extern crate hyper;
+extern crate lmdb_rs;
 extern crate num;
 extern crate num;
 extern crate portaudio;
 extern crate portaudio;
 extern crate protobuf;
 extern crate protobuf;

+ 9 - 14
src/main.rs

@@ -16,6 +16,7 @@ use librespot::player::Player;
 use librespot::session::{Bitrate, Config, Session};
 use librespot::session::{Bitrate, Config, Session};
 use librespot::spirc::SpircManager;
 use librespot::spirc::SpircManager;
 use librespot::util::version::version_string;
 use librespot::util::version::version_string;
+use librespot::cache::{Cache, DefaultCache, NoCache};
 
 
 #[cfg(feature = "facebook")]
 #[cfg(feature = "facebook")]
 use librespot::facebook::facebook_login;
 use librespot::facebook::facebook_login;
@@ -76,9 +77,12 @@ fn main() {
     }).or_else(|| APPKEY.map(ToOwned::to_owned)).unwrap();
     }).or_else(|| APPKEY.map(ToOwned::to_owned)).unwrap();
 
 
     let username = matches.opt_str("u");
     let username = matches.opt_str("u");
-    let cache_location = matches.opt_str("c").map(PathBuf::from);
     let name = matches.opt_str("n").unwrap();
     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) {
     let bitrate = match matches.opt_str("b").as_ref().map(String::as_ref) {
         None => Bitrate::Bitrate160, // default value
         None => Bitrate::Bitrate160, // default value
 
 
@@ -92,13 +96,10 @@ fn main() {
         application_key: appkey,
         application_key: appkey,
         user_agent: version_string(),
         user_agent: version_string(),
         device_name: name,
         device_name: name,
-        cache_location: cache_location.clone(),
         bitrate: bitrate,
         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 credentials = username.map(|username| {
         let password = matches.opt_str("p")
         let password = matches.opt_str("p")
@@ -109,7 +110,6 @@ fn main() {
                                   read_password().unwrap()
                                   read_password().unwrap()
                               });
                               });
 
 
-
         Credentials::with_password(username, password)
         Credentials::with_password(username, password)
     }).or_else(|| {
     }).or_else(|| {
         if cfg!(feature = "facebook") && matches.opt_present("facebook") {
         if cfg!(feature = "facebook") && matches.opt_present("facebook") {
@@ -117,11 +117,8 @@ fn main() {
         } else {
         } else {
             None
             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 ...");
         println!("No username provided and no stored credentials, starting discovery ...");
 
 
         let mut discovery = DiscoveryManager::new(session.clone());
         let mut discovery = DiscoveryManager::new(session.clone());
@@ -131,9 +128,7 @@ fn main() {
     std::env::remove_var(PASSWORD_ENV_NAME);
     std::env::remove_var(PASSWORD_ENV_NAME);
 
 
     let reusable_credentials = session.login(credentials).unwrap();
     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 player = Player::new(session.clone(), || DefaultSink::open());
     let spirc = SpircManager::new(session.clone(), player);
     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 crypto::mac::Mac;
 use eventual;
 use eventual;
 use eventual::Future;
 use eventual::Future;
+use eventual::Async;
 use protobuf::{self, Message};
 use protobuf::{self, Message};
 use rand::thread_rng;
 use rand::thread_rng;
 use rand::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::result::Result;
 use std::sync::{Mutex, RwLock, Arc, mpsc};
 use std::sync::{Mutex, RwLock, Arc, mpsc};
 
 
+use album_cover::get_album_cover;
 use apresolve::apresolve;
 use apresolve::apresolve;
 use audio_key::{AudioKeyManager, AudioKey, AudioKeyError};
 use audio_key::{AudioKeyManager, AudioKey, AudioKeyError};
-use audio_file::{AudioFileManager, AudioFile};
+use audio_file::AudioFile;
 use authentication::Credentials;
 use authentication::Credentials;
+use cache::Cache;
 use connection::{self, PlainConnection, CipherConnection, PacketHandler};
 use connection::{self, PlainConnection, CipherConnection, PacketHandler};
 use diffie_hellman::DHLocalKeys;
 use diffie_hellman::DHLocalKeys;
 use mercury::{MercuryManager, MercuryRequest, MercuryResponse};
 use mercury::{MercuryManager, MercuryRequest, MercuryResponse};
 use metadata::{MetadataManager, MetadataRef, MetadataTrait};
 use metadata::{MetadataManager, MetadataRef, MetadataTrait};
 use protocol;
 use protocol;
 use stream::{ChannelId, StreamManager, StreamEvent, StreamError};
 use stream::{ChannelId, StreamManager, StreamEvent, StreamError};
-use util::{self, SpotifyId, FileId, mkdir_existing};
+use util::{self, SpotifyId, FileId, ReadSeek};
 
 
 pub enum Bitrate {
 pub enum Bitrate {
     Bitrate96,
     Bitrate96,
@@ -34,7 +36,6 @@ pub struct Config {
     pub application_key: Vec<u8>,
     pub application_key: Vec<u8>,
     pub user_agent: String,
     pub user_agent: String,
     pub device_name: String,
     pub device_name: String,
-    pub cache_location: Option<PathBuf>,
     pub bitrate: Bitrate,
     pub bitrate: Bitrate,
 }
 }
 
 
@@ -48,11 +49,11 @@ pub struct SessionInternal {
     config: Config,
     config: Config,
     data: RwLock<SessionData>,
     data: RwLock<SessionData>,
 
 
+    cache: Box<Cache + Send + Sync>,
     mercury: Mutex<MercuryManager>,
     mercury: Mutex<MercuryManager>,
     metadata: Mutex<MetadataManager>,
     metadata: Mutex<MetadataManager>,
     stream: Mutex<StreamManager>,
     stream: Mutex<StreamManager>,
     audio_key: Mutex<AudioKeyManager>,
     audio_key: Mutex<AudioKeyManager>,
-    audio_file: Mutex<AudioFileManager>,
     rx_connection: Mutex<Option<CipherConnection>>,
     rx_connection: Mutex<Option<CipherConnection>>,
     tx_connection: Mutex<Option<CipherConnection>>,
     tx_connection: Mutex<Option<CipherConnection>>,
 }
 }
@@ -61,11 +62,7 @@ pub struct SessionInternal {
 pub struct Session(pub Arc<SessionInternal>);
 pub struct Session(pub Arc<SessionInternal>);
 
 
 impl Session {
 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 device_id = {
             let mut h = Sha1::new();
             let mut h = Sha1::new();
             h.input_str(&config.device_name);
             h.input_str(&config.device_name);
@@ -83,11 +80,11 @@ impl Session {
             rx_connection: Mutex::new(None),
             rx_connection: Mutex::new(None),
             tx_connection: Mutex::new(None),
             tx_connection: Mutex::new(None),
 
 
+            cache: cache,
             mercury: Mutex::new(MercuryManager::new()),
             mercury: Mutex::new(MercuryManager::new()),
             metadata: Mutex::new(MetadataManager::new()),
             metadata: Mutex::new(MetadataManager::new()),
             stream: Mutex::new(StreamManager::new()),
             stream: Mutex::new(StreamManager::new()),
             audio_key: Mutex::new(AudioKeyManager::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)
         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> {
     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)
         self.0.mercury.lock().unwrap().subscribe(self, uri)
     }
     }
 
 
+    pub fn cache(&self) -> &Cache {
+        self.0.cache.as_ref()
+    }
+
     pub fn config(&self) -> &Config {
     pub fn config(&self) -> &Config {
         &self.0.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 { }
+