mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Split cache handling to separate module.
Use it for audio keys and album covers as well.
This commit is contained in:
parent
a7559787df
commit
85903a0da5
12 changed files with 293 additions and 138 deletions
58
Cargo.lock
generated
58
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
103
src/cache/default_cache.rs
vendored
Normal 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
27
src/cache/mod.rs
vendored
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
23
src/main.rs
23
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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 { }
|
||||
|
||||
|
|
Loading…
Reference in a new issue