From ca9a5de5c4809fa3954f590835305292cc71b04c Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 25 Dec 2024 02:55:43 +0100 Subject: [PATCH] feat: add album/artist filtering to run command (#17) * 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) --- Cargo.toml | 1 + flake.lock | 39 ++++++++++++++------------- src/api/mod.rs | 47 ++++++++++++++++++++++++++------- src/api/structs/digital_item.rs | 2 +- src/api/structs/mod.rs | 30 ++++++++++----------- src/cmds/run.rs | 12 ++++++--- 6 files changed, 84 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9603b9e..11ba5e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 "] license = "MIT" diff --git a/flake.lock b/flake.lock index 6a3b5e7..20cf3a1 100644 --- a/flake.lock +++ b/flake.lock @@ -6,11 +6,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1720506553, - "narHash": "sha256-cd8Mt+kqN876nk2zX/BZePiwDvrxgHgoBXJeYIj09es=", + "lastModified": 1735021898, + "narHash": "sha256-Y04+5ZiYWRAciEDoTV/rXArxfI2NwvcvfhXlLJrq3/o=", "owner": "nix-community", "repo": "fenix", - "rev": "5c6f98d68f4825e4fa7b62b65d6777f8d152b3a0", + "rev": "2f200599e9753f3766a5a21e344a770a5d62226f", "type": "github" }, "original": { @@ -24,11 +24,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1718727675, - "narHash": "sha256-uFsCwWYI2pUpt0awahSBorDUrUfBhaAiyz+BPTS2MHk=", + "lastModified": 1733346208, + "narHash": "sha256-a4WZp1xQkrnA4BbnKrzJNr+dYoQr5Xneh2syJoddFyE=", "owner": "nix-community", "repo": "naersk", - "rev": "941ce6dc38762a7cfb90b5add223d584feed299b", + "rev": "378614f37a6bee5a3f2ef4f825a73d948d3ae921", "type": "github" }, "original": { @@ -40,11 +40,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1720418205, - "narHash": "sha256-cPJoFPXU44GlhWg4pUk9oUPqurPlCFZ11ZQPk21GTPU=", + "lastModified": 1734649271, + "narHash": "sha256-4EVBRhOjMDuGtMaofAIqzJbg4Ql7Ai0PSeuVZTHjyKQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "655a58a72a6601292512670343087c2d75d859c1", + "rev": "d70bd19e0a38ad4790d3913bf08fcbfc9eeca507", "type": "github" }, "original": { @@ -56,9 +56,12 @@ }, "nixpkgs_2": { "locked": { - "narHash": "sha256-rwz8NJZV+387rnWpTYcXaRNvzUSnnF9aHONoJIYmiUQ=", - "path": "/nix/store/dk2rpyb6ndvfbf19bkb2plcz5y3k8i5v-source", - "type": "path" + "lastModified": 1734988233, + "narHash": "sha256-Ucfnxq1rF/GjNP3kTL+uTfgdoE9a3fxDftSfeLIS8mA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "de1864217bfa9b5845f465e771e0ecb48b30e02d", + "type": "github" }, "original": { "id": "nixpkgs", @@ -67,11 +70,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1720031269, - "narHash": "sha256-rwz8NJZV+387rnWpTYcXaRNvzUSnnF9aHONoJIYmiUQ=", + "lastModified": 1734649271, + "narHash": "sha256-4EVBRhOjMDuGtMaofAIqzJbg4Ql7Ai0PSeuVZTHjyKQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9f4128e00b0ae8ec65918efeba59db998750ead6", + "rev": "d70bd19e0a38ad4790d3913bf08fcbfc9eeca507", "type": "github" }, "original": { @@ -91,11 +94,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1720455058, - "narHash": "sha256-jqfodED9cglRkRfNI8TBWdjHcEoregUEzZd7+4edDsM=", + "lastModified": 1734948743, + "narHash": "sha256-UQ5humzfowEqBwLq0Er6hrx6ewZfKHRGEb+M0fO1pv4=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "da27b89ca55d066680b2ddbc53477b3816cd5407", + "rev": "80c97cab43b0a41c87d073361e3073787b3e9173", "type": "github" }, "original": { diff --git a/src/api/mod.rs b/src/api/mod.rs index 006efb7..156ddd8 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -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, + 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::() + } + /// Scrape a user's Bandcamp page to find download urls - pub fn get_download_urls(&self, name: &str) -> Result> { + pub fn get_download_urls(&self, name: &str, artist: Option<&String>, album: Option<&String>) -> Result> { debug!("`get_download_urls` for Bandcamp page '{name}'"); let body = self.request(Method::GET, &Self::bc_path(name))?.text()?; @@ -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::>(); + match fanpage_data.fan_data.is_own_page { Some(true) => (), _ => bail!(format!( @@ -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 { @@ -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); } @@ -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); } @@ -171,6 +192,8 @@ impl Api { &self, data: &ParsedFanpageData, collection_name: &str, + album: Option<&String>, + artist: Option<&String>, ) -> Result> { debug!("Paginating results for {collection_name}"); let collection_data = match collection_name { @@ -199,8 +222,12 @@ impl Api { .send()? .json::()?; - trace!("Collected {} items", body.redownload_urls.clone().len()); - collection.extend(body.redownload_urls); + let items = body.items.iter().by_ref().collect::>(); + 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; } diff --git a/src/api/structs/digital_item.rs b/src/api/structs/digital_item.rs index efb2ea2..fab2bce 100644 --- a/src/api/structs/digital_item.rs +++ b/src/api/structs/digital_item.rs @@ -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}; diff --git a/src/api/structs/mod.rs b/src/api/structs/mod.rs index a0a040a..4787c6d 100644 --- a/src/api/structs/mod.rs +++ b/src/api/structs/mod.rs @@ -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, +} + +#[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)] @@ -35,26 +48,13 @@ pub struct CollectionData { pub redownload_urls: Option, } -// #[derive(Deserialize, Debug)] -// pub struct ItemCache { -// pub collection: HashMap, -// pub hidden: HashMap, -// } - -// #[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, } #[derive(Deserialize, Debug)] diff --git a/src/cmds/run.rs b/src/cmds/run.rs index 5493872..85f5839 100644 --- a/src/cmds/run.rs +++ b/src/cmds/run.rs @@ -34,6 +34,12 @@ macro_rules! skip_err { #[derive(Debug, ClapArgs)] pub struct Args { + #[arg(long, env = "BS_ALBUM")] + album: Option, + + #[arg(long, env = "BS_ARTIST")] + artist: Option, + /// The audio format to download the files in. #[arg(short = 'f', long = "format", value_parser = PossibleValuesParser::new(FORMATS), env = "BS_FORMAT")] audio_format: String, @@ -78,6 +84,8 @@ pub struct Args { pub fn command( Args { + album, + artist, audio_format, cookies, debug, @@ -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()?; @@ -139,8 +147,6 @@ pub fn command( let m = Arc::new(MultiProgress::new()); let dry_run_results = Arc::new(Mutex::new(Vec::::new())); - // TODO: dry_run - thread::scope(|scope| { for i in 0..jobs { let api = api.clone();