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)",
|
"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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -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
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;
|
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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
23
src/main.rs
23
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 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);
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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