bandcamp/src/requests.rs
2024-12-07 19:55:03 +01:00

202 lines
5.6 KiB
Rust

use once_cell::sync::Lazy;
use regex::Regex;
use reqwest::{IntoUrl, Url};
use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::models::{Album, Band, Feed, SearchFilter, SearchResult, TrAlbumType};
use crate::{util, Bandcamp, Error, API_URL};
static BAND_ID_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r#"<script\stype="text/javascript"\s[^>]*data-band="\{[^"]*&quot;id&quot;:(\d+)[,}]"#,
)
.unwrap()
});
#[derive(Serialize)]
struct BandRequest {
band_id: u64,
}
#[derive(Serialize)]
struct AlbumRequest {
band_id: u64,
tralbum_id: u64,
tralbum_type: TrAlbumType,
}
#[derive(Serialize)]
struct SearchRequest<'a> {
search_text: &'a str,
search_filter: SearchFilter,
full_page: bool,
}
#[derive(Deserialize)]
struct SearchResultWrapper {
auto: SearchResult,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum ResponseWrapper<T> {
Ok(T),
Error { error_message: String },
}
impl<T> ResponseWrapper<T> {
fn convert(self) -> Result<T> {
match self {
ResponseWrapper::Ok(data) => Ok(data),
ResponseWrapper::Error { error_message } => {
if error_message.starts_with("No such ") || error_message.ends_with(" not found") {
Err(Error::NotFound(error_message.into()))
} else {
Err(Error::Bandcamp(error_message.into()))
}
}
}
}
}
impl Bandcamp {
/// Get a band from the Bandcamp API
pub async fn band(&self, band_id: u64) -> Result<Band> {
let req = BandRequest { band_id };
let resp = self
.client
.post(format!("{}mobile/24/band_details", API_URL))
.json(&req)
.send()
.await?
.error_for_status()?;
resp.json::<ResponseWrapper<Band>>().await?.convert()
}
/// Get a album (or a track) from the Bandcamp API
///
/// # Parameters
/// - `band_id` The Band ID of the album/track
/// - `tralbum_id` Album/Track ID
/// - `tralbum_type` Track/Album
pub async fn album(
&self,
band_id: u64,
tralbum_id: u64,
tralbum_type: TrAlbumType,
) -> Result<Album> {
let req = AlbumRequest {
band_id,
tralbum_id,
tralbum_type,
};
let resp = self
.client
.post(format!("{}mobile/25/tralbum_details", API_URL))
.json(&req)
.send()
.await?
.error_for_status()?;
resp.json::<ResponseWrapper<Album>>().await?.convert()
}
/// Get an album (or a track) from the Bandcamp API using a unified ID string
///
/// # Examples
/// - Album: `3760769193a1493086082`
/// - Track: `2464198920t716010980`
pub async fn album_uid<S: AsRef<str>>(&self, uid: S) -> Result<Album> {
let (band_id, tralbum_type, tralbum_id) = util::parse_album_uid(uid)?;
self.album(band_id, tralbum_id, tralbum_type).await
}
/// Search Bandcamp for bands, albums or tracks
pub async fn search<S: AsRef<str>>(
&self,
query: S,
filter: SearchFilter,
) -> Result<SearchResult> {
let req = SearchRequest {
search_text: query.as_ref(),
search_filter: filter,
full_page: false,
};
let resp = self
.client
.post(format!(
"{}bcsearch_public_api/1/autocomplete_elastic",
API_URL
))
.json(&req)
.send()
.await?
.error_for_status()?;
Ok(resp
.json::<ResponseWrapper<SearchResultWrapper>>()
.await?
.convert()?
.auto)
}
/// Get the current Bandcamp feed containing featured works.
pub async fn feed(&self) -> Result<Feed> {
let resp = self
.client
.get(format!(
"{}mobile/25/feed_older_logged_out?story_groups=featured",
API_URL
))
.send()
.await?
.error_for_status()?;
resp.json::<ResponseWrapper<Feed>>().await?.convert()
}
/// Get a continuation page for the Bandcamp feed
pub async fn feed_cont<S: AsRef<str>>(&self, token: S) -> Result<Feed> {
let resp = self
.client
.get(
Url::parse_with_params(
&format!("{}mobile/25/feed_older_logged_out", API_URL),
&[("story_groups", &format!("featured:{}", token.as_ref()))],
)
.unwrap(),
)
.send()
.await?
.error_for_status()?;
resp.json::<ResponseWrapper<Feed>>().await?.convert()
}
/// Get the numeric band id from their bandcamp URL
pub async fn band_id_from_url<U: IntoUrl>(&self, url: U) -> Result<u64> {
let mut url = url.into_url().map_err(|_| Error::InvalidUrl)?;
let host = url.host_str().ok_or(Error::InvalidUrl)?;
if !host.ends_with(".bandcamp.com") {
return Err(Error::InvalidUrl);
}
url.set_path("");
url.set_query(None);
let resp = self.client.get(url).send().await?.error_for_status()?;
let html = resp.text().await?;
BAND_ID_REGEX
.captures(&html)
.ok_or_else(|| Error::WebsiteParsing("could not find band id".into()))?
.get(1)
.unwrap()
.as_str()
.parse()
.map_err(|_| Error::WebsiteParsing("could not parse band id".into()))
}
}