Skip to content

Commit

Permalink
feat: add album/artist filtering to run command (#17)
Browse files Browse the repository at this point in the history
* chore: remove stale TODO comment

* feat: add album/artist filtering to run command

* fix: remove unused DateTime import

* chore: set MSRV to 1.82.0

* chore: update nix flake (for newer rustc)
  • Loading branch information
samcday authored Dec 25, 2024
1 parent 2a2baf7 commit ca9a5de
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 47 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "bandsnatch"
version = "0.3.3"
edition = "2021"
rust-version = "1.82.0"
description = "A CLI batch downloader for your Bandcamp collection"
authors = ["Ashlynne Mitchell <ovy@ovyerus.com>"]
license = "MIT"
Expand Down
39 changes: 21 additions & 18 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 37 additions & 10 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,31 @@ impl Api {
Ok(response)
}

/// Filters the download map by optional artist or album filters.
fn filter_download_map<'a>(
unfiltered: Option<DownloadsMap>,
items: &'a Vec<&'a Item>,
album: Option<&String>,
artist: Option<&String>
) -> DownloadsMap {
unfiltered
.iter()
.flatten()
.filter_map(|(id, url)| {
items.iter().find(|v| &format!("{}{}", v.sale_item_type, v.sale_item_id) == id)
.filter(|item| {
artist.is_none_or(|v| item.band_name.eq_ignore_ascii_case(v))
})
.filter(|item| {
album.is_none_or(|v| item.item_title.eq_ignore_ascii_case(v))
})
.map(|_| (id.clone(), url.clone()))
})
.collect::<DownloadsMap>()
}

/// Scrape a user's Bandcamp page to find download urls
pub fn get_download_urls(&self, name: &str) -> Result<BandcampPage, Box<dyn Error>> {
pub fn get_download_urls(&self, name: &str, artist: Option<&String>, album: Option<&String>) -> Result<BandcampPage, Box<dyn Error>> {
debug!("`get_download_urls` for Bandcamp page '{name}'");

let body = self.request(Method::GET, &Self::bc_path(name))?.text()?;
Expand All @@ -115,6 +138,8 @@ impl Api {
.expect("Failed to deserialise collection page data blob.");
debug!("Successfully fetched Bandcamp page, and found + deserialised data blob");

let items = fanpage_data.item_cache.collection.values().collect::<Vec<&Item>>();

match fanpage_data.fan_data.is_own_page {
Some(true) => (),
_ => bail!(format!(
Expand All @@ -123,11 +148,7 @@ impl Api {
}

// TODO: make sure this exists
let mut collection = fanpage_data
.collection_data
.redownload_urls
.clone()
.unwrap();
let mut collection = Self::filter_download_map(fanpage_data.collection_data.redownload_urls.clone(), &items, album, artist);

let skip_hidden_items = true;
if skip_hidden_items {
Expand All @@ -142,7 +163,7 @@ impl Api {
// This should never be `None` thanks to the comparison above.
fanpage_data.collection_data.item_count.unwrap()
);
let rest = self.get_rest_downloads_in_collection(&fanpage_data, "collection_items")?;
let rest = self.get_rest_downloads_in_collection(&fanpage_data, "collection_items", album, artist)?;
collection.extend(rest);
}

Expand All @@ -153,7 +174,7 @@ impl Api {
"Too many in `hidden_data`, and we're told not to skip, so we need to paginate ({} total)",
fanpage_data.hidden_data.item_count.unwrap()
);
let rest = self.get_rest_downloads_in_collection(&fanpage_data, "hidden_items")?;
let rest = self.get_rest_downloads_in_collection(&fanpage_data, "hidden_items", album, artist)?;
collection.extend(rest);
}

Expand All @@ -171,6 +192,8 @@ impl Api {
&self,
data: &ParsedFanpageData,
collection_name: &str,
album: Option<&String>,
artist: Option<&String>,
) -> Result<DownloadsMap, Box<dyn Error>> {
debug!("Paginating results for {collection_name}");
let collection_data = match collection_name {
Expand Down Expand Up @@ -199,8 +222,12 @@ impl Api {
.send()?
.json::<ParsedCollectionItems>()?;

trace!("Collected {} items", body.redownload_urls.clone().len());
collection.extend(body.redownload_urls);
let items = body.items.iter().by_ref().collect::<Vec<_>>();
let redownload_urls = Self::filter_download_map(Some(body.redownload_urls), &items, album, artist);
trace!("Collected {} items", redownload_urls.len());


collection.extend(redownload_urls);
more_available = body.more_available;
last_token = body.last_token;
}
Expand Down
2 changes: 1 addition & 1 deletion src/api/structs/digital_item.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::util::make_string_fs_safe;

use chrono::{DateTime, Datelike, NaiveDateTime};
use chrono::{Datelike, NaiveDateTime};
use serde::{self, Deserialize};
use std::{collections::HashMap, path::Path};

Expand Down
30 changes: 15 additions & 15 deletions src/api/structs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,20 @@ pub struct ParsedFanpageData {
pub collection_data: CollectionData,
/// Data about items in the user's music collection that have been hidden.
pub hidden_data: CollectionData,
// pub item_cache: ItemCache,
pub item_cache: ItemCache,
}

#[derive(Deserialize, Debug)]
pub struct ItemCache {
pub collection: HashMap<String, Item>,
}

#[derive(Deserialize, Debug)]
pub struct Item {
pub sale_item_id: u64,
pub sale_item_type: String,
pub band_name: String,
pub item_title: String,
}

#[derive(Deserialize, Debug)]
Expand All @@ -35,26 +48,13 @@ pub struct CollectionData {
pub redownload_urls: Option<DownloadsMap>,
}

// #[derive(Deserialize, Debug)]
// pub struct ItemCache {
// pub collection: HashMap<String, CachedItem>,
// pub hidden: HashMap<String, CachedItem>,
// }

// #[derive(Deserialize, Debug)]
// pub struct CachedItem {
// #[serde(deserialize_with = "deserialize_string_from_number")]
// pub sale_item_id: String,
// pub band_name: String,
// pub item_title: String,
// }

/// Structure of the data returned from Bandcamp's collection API.
#[derive(Deserialize, Debug)]
pub struct ParsedCollectionItems {
pub more_available: bool,
pub last_token: String,
pub redownload_urls: DownloadsMap,
pub items: Vec<Item>,
}

#[derive(Deserialize, Debug)]
Expand Down
12 changes: 9 additions & 3 deletions src/cmds/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ macro_rules! skip_err {

#[derive(Debug, ClapArgs)]
pub struct Args {
#[arg(long, env = "BS_ALBUM")]
album: Option<String>,

#[arg(long, env = "BS_ARTIST")]
artist: Option<String>,

/// The audio format to download the files in.
#[arg(short = 'f', long = "format", value_parser = PossibleValuesParser::new(FORMATS), env = "BS_FORMAT")]
audio_format: String,
Expand Down Expand Up @@ -78,6 +84,8 @@ pub struct Args {

pub fn command(
Args {
album,
artist,
audio_format,
cookies,
debug,
Expand Down Expand Up @@ -117,7 +125,7 @@ pub fn command(
root.join("bandcamp-collection-downloader.cache"),
)));

let download_urls = api.get_download_urls(&user)?.download_urls;
let download_urls = api.get_download_urls(&user, artist.as_ref(), album.as_ref())?.download_urls;
let items = {
// Lock gets freed after this block.
let cache_content = cache.lock().unwrap().content()?;
Expand All @@ -139,8 +147,6 @@ pub fn command(
let m = Arc::new(MultiProgress::new());
let dry_run_results = Arc::new(Mutex::new(Vec::<String>::new()));

// TODO: dry_run

thread::scope(|scope| {
for i in 0..jobs {
let api = api.clone();
Expand Down

0 comments on commit ca9a5de

Please sign in to comment.