Split cache handling to separate module.

Use it for audio keys and album covers as well.
This commit is contained in:
Paul Lietar 2016-03-16 04:07:04 +00:00
parent a7559787df
commit 85903a0da5
12 changed files with 293 additions and 138 deletions

58
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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 { pub struct AudioFile {
Direct(fs::File),
Loading(AudioFileLoading),
}
pub struct AudioFileLoading {
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 { impl AudioFile {
fn new(session: &Session, file_id: FileId) -> AudioFileLoading { 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))
})
}
}

View file

@ -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| { .map(|ref mut requests| {
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(); let (tx, rx) = eventual::Future::pair();
req.push(tx); requests.push(tx);
rx 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,28 +74,20 @@ 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)); for cb in callbacks {
if let AudioKeyStatus::Loading(cbs) = status {
for cb in cbs {
cb.complete(key); 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)); for cb in callbacks {
if let AudioKeyStatus::Loading(cbs) = status {
for cb in cbs {
cb.fail(error); cb.fail(error);
} }
} }
} }
} }
}
} }

View file

@ -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 { pub fn from_file<P: AsRef<Path>>(path: P) -> Option<Credentials> {
let file = File::open(path).unwrap(); File::open(path).ok().map(Credentials::from_reader)
Credentials::from_reader(file)
} }
pub fn save_to_writer<W: Write>(&self, writer: &mut W) { pub fn save_to_writer<W: Write>(&self, writer: &mut W) {

103
src/cache/default_cache.rs vendored Normal file
View file

@ -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
src/cache/mod.rs vendored Normal file
View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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 session = Session::new(config, cache);
let credentials_path = cache_location.map(|c| c.join("credentials.json"));
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(|| { }).or_else(|| session.cache().get_credentials())
credentials_path.as_ref() .unwrap_or_else(|| {
.and_then(|p| File::open(p).ok())
.map(Credentials::from_reader)
}).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 { session.cache().put_credentials(&reusable_credentials);
reusable_credentials.save_to_file(path);
}
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);

View file

@ -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::io::{Read, Write, Cursor};
use std::path::PathBuf;
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 { pub fn new(config: Config, cache: Box<Cache + Send + Sync>) -> Session {
if let Some(cache_location) = config.cache_location.as_ref() {
mkdir_existing(cache_location).unwrap();
}
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> { pub fn audio_key(&self, track: SpotifyId, file_id: FileId) -> Future<AudioKey, AudioKeyError> {
self.0.audio_key.lock().unwrap().request(self, track, file) 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 { pub fn audio_file(&self, file_id: FileId) -> Box<ReadSeek> {
self.0.audio_file.lock().unwrap().request(self, file) 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<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
} }

View file

@ -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 { }