2021-12-26 20:18:42 +00:00
|
|
|
use std::{
|
|
|
|
convert::{TryFrom, TryInto},
|
|
|
|
ops::{Deref, DerefMut},
|
|
|
|
};
|
|
|
|
|
2021-12-16 21:42:37 +00:00
|
|
|
use chrono::Local;
|
2021-12-26 20:18:42 +00:00
|
|
|
use protobuf::Message;
|
2021-12-16 21:42:37 +00:00
|
|
|
use thiserror::Error;
|
|
|
|
use url::Url;
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
use super::{date::Date, Error, FileId, Session};
|
2021-12-16 21:42:37 +00:00
|
|
|
|
|
|
|
use librespot_protocol as protocol;
|
|
|
|
use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage;
|
|
|
|
use protocol::storage_resolve::StorageResolveResponse_Result;
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct MaybeExpiringUrl(pub String, pub Option<Date>);
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct MaybeExpiringUrls(pub Vec<MaybeExpiringUrl>);
|
|
|
|
|
|
|
|
impl Deref for MaybeExpiringUrls {
|
|
|
|
type Target = Vec<MaybeExpiringUrl>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
&self.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DerefMut for MaybeExpiringUrls {
|
|
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
|
|
&mut self.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum CdnUrlError {
|
|
|
|
#[error("all URLs expired")]
|
|
|
|
Expired,
|
|
|
|
#[error("resolved storage is not for CDN")]
|
|
|
|
Storage,
|
2021-12-27 20:37:22 +00:00
|
|
|
#[error("no URLs resolved")]
|
|
|
|
Unresolved,
|
2021-12-26 20:18:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl From<CdnUrlError> for Error {
|
|
|
|
fn from(err: CdnUrlError) -> Self {
|
|
|
|
match err {
|
|
|
|
CdnUrlError::Expired => Error::deadline_exceeded(err),
|
2021-12-27 20:37:22 +00:00
|
|
|
CdnUrlError::Storage | CdnUrlError::Unresolved => Error::unavailable(err),
|
2021-12-26 20:18:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-16 21:42:37 +00:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct CdnUrl {
|
|
|
|
pub file_id: FileId,
|
2021-12-26 20:18:42 +00:00
|
|
|
urls: MaybeExpiringUrls,
|
2021-12-16 21:42:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl CdnUrl {
|
|
|
|
pub fn new(file_id: FileId) -> Self {
|
|
|
|
Self {
|
|
|
|
file_id,
|
|
|
|
urls: MaybeExpiringUrls(Vec::new()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
pub async fn resolve_audio(&self, session: &Session) -> Result<Self, Error> {
|
2021-12-16 21:42:37 +00:00
|
|
|
let file_id = self.file_id;
|
2021-12-27 20:37:22 +00:00
|
|
|
let response = session.spclient().get_audio_storage(file_id).await?;
|
2021-12-16 21:42:37 +00:00
|
|
|
let msg = CdnUrlMessage::parse_from_bytes(&response)?;
|
|
|
|
let urls = MaybeExpiringUrls::try_from(msg)?;
|
|
|
|
|
|
|
|
let cdn_url = Self { file_id, urls };
|
|
|
|
|
|
|
|
trace!("Resolved CDN storage: {:#?}", cdn_url);
|
|
|
|
|
|
|
|
Ok(cdn_url)
|
|
|
|
}
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
pub fn try_get_url(&self) -> Result<&str, Error> {
|
2021-12-27 20:37:22 +00:00
|
|
|
if self.urls.is_empty() {
|
|
|
|
return Err(CdnUrlError::Unresolved.into());
|
|
|
|
}
|
|
|
|
|
2021-12-16 21:42:37 +00:00
|
|
|
let now = Local::now();
|
2021-12-26 20:18:42 +00:00
|
|
|
let url = self.urls.iter().find(|url| match url.1 {
|
|
|
|
Some(expiry) => now < expiry.as_utc(),
|
|
|
|
None => true,
|
|
|
|
});
|
2021-12-16 21:42:37 +00:00
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
if let Some(url) = url {
|
|
|
|
Ok(&url.0)
|
2021-12-16 21:42:37 +00:00
|
|
|
} else {
|
2021-12-26 20:18:42 +00:00
|
|
|
Err(CdnUrlError::Expired.into())
|
2021-12-16 21:42:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TryFrom<CdnUrlMessage> for MaybeExpiringUrls {
|
2021-12-26 20:18:42 +00:00
|
|
|
type Error = crate::Error;
|
2021-12-16 21:42:37 +00:00
|
|
|
fn try_from(msg: CdnUrlMessage) -> Result<Self, Self::Error> {
|
|
|
|
if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) {
|
2021-12-26 20:18:42 +00:00
|
|
|
return Err(CdnUrlError::Storage.into());
|
2021-12-16 21:42:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let is_expiring = !msg.get_fileid().is_empty();
|
|
|
|
|
|
|
|
let result = msg
|
|
|
|
.get_cdnurl()
|
|
|
|
.iter()
|
|
|
|
.map(|cdn_url| {
|
2021-12-26 20:18:42 +00:00
|
|
|
let url = Url::parse(cdn_url)?;
|
2021-12-16 21:42:37 +00:00
|
|
|
|
|
|
|
if is_expiring {
|
|
|
|
let expiry_str = if let Some(token) = url
|
|
|
|
.query_pairs()
|
|
|
|
.into_iter()
|
|
|
|
.find(|(key, _value)| key == "__token__")
|
|
|
|
{
|
2021-12-26 20:18:42 +00:00
|
|
|
if let Some(mut start) = token.1.find("exp=") {
|
|
|
|
start += 4;
|
|
|
|
if token.1.len() >= start {
|
|
|
|
let slice = &token.1[start..];
|
|
|
|
if let Some(end) = slice.find('~') {
|
|
|
|
// this is the only valid invariant for akamaized.net
|
|
|
|
String::from(&slice[..end])
|
|
|
|
} else {
|
|
|
|
String::from(slice)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
String::new()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
String::new()
|
|
|
|
}
|
2021-12-16 21:42:37 +00:00
|
|
|
} else if let Some(query) = url.query() {
|
|
|
|
let mut items = query.split('_');
|
2021-12-26 20:18:42 +00:00
|
|
|
if let Some(first) = items.next() {
|
|
|
|
// this is the only valid invariant for scdn.co
|
|
|
|
String::from(first)
|
|
|
|
} else {
|
|
|
|
String::new()
|
|
|
|
}
|
2021-12-16 21:42:37 +00:00
|
|
|
} else {
|
2021-12-26 20:18:42 +00:00
|
|
|
String::new()
|
2021-12-16 21:42:37 +00:00
|
|
|
};
|
|
|
|
|
2021-12-26 20:18:42 +00:00
|
|
|
let mut expiry: i64 = expiry_str.parse()?;
|
|
|
|
|
2021-12-16 21:42:37 +00:00
|
|
|
expiry -= 5 * 60; // seconds
|
|
|
|
|
|
|
|
Ok(MaybeExpiringUrl(
|
|
|
|
cdn_url.to_owned(),
|
2021-12-26 20:18:42 +00:00
|
|
|
Some(expiry.try_into()?),
|
2021-12-16 21:42:37 +00:00
|
|
|
))
|
|
|
|
} else {
|
|
|
|
Ok(MaybeExpiringUrl(cdn_url.to_owned(), None))
|
|
|
|
}
|
|
|
|
})
|
2021-12-26 20:18:42 +00:00
|
|
|
.collect::<Result<Vec<MaybeExpiringUrl>, Error>>()?;
|
2021-12-16 21:42:37 +00:00
|
|
|
|
|
|
|
Ok(Self(result))
|
|
|
|
}
|
|
|
|
}
|