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)",
"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"

View file

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

View file

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

View file

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

View file

@ -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
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;
pub mod audio_sink;
pub mod authentication;
pub mod cache;
mod connection;
mod diffie_hellman;
pub mod discovery;

View file

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

View file

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

View file

@ -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: FileId) -> AudioFile {
self.0.audio_file.lock().unwrap().request(self, file)
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 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
}

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