4 use std::io::BufReader;
5 use std::path::{Path, PathBuf};
7 use log::{warn, info, debug, trace};
8 use reqwest::{Client, RequestBuilder, Url, IntoUrl, header};
10 use std::fs::OpenOptions;
11 use rusqlite::Connection;
13 use simplelog::{TermLogger, LevelFilter};
14 use structopt::StructOpt;
15 use directories::ProjectDirs;
18 struct MagnatuneClient {
25 impl MagnatuneClient {
26 fn new(host: String, username: String, password: String) -> Result<MagnatuneClient, Box<Error>> {
27 let mut headers = header::HeaderMap::new();
28 headers.insert(header::USER_AGENT, header::HeaderValue::from_static(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))));
29 let client = Client::builder().default_headers(headers).build()?;
32 base_url: Url::parse(&format!("http://{}", host))?,
39 fn url(&self, path: &[&str]) -> Result<Url, Box<Error>> {
40 let base = Url::options().base_url(Some(&self.base_url));
41 let mut url = base.parse("")?;
42 url.path_segments_mut().map_err(|_| "cannot be base")?.extend(path);
47 fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
50 .basic_auth(self.username.clone(), Some(self.password.clone()))
63 fn path(&self) -> PathBuf {
64 [ &self.artist, &self.name ].iter().collect()
74 #[derive(StructOpt, Debug)]
76 #[structopt(long = "magnatune-host", default_value = "download.magnatune.com")]
77 magnatune_host: String,
79 /// Show verbose output
80 #[structopt(short = "v", long = "verbose")]
84 #[structopt(short = "d", long = "debug")]
88 #[structopt(parse(from_os_str))]
91 /// Limit to the first N entries in RSS feed. Useful for testing
92 #[structopt(long = "limit")]
97 let opt = Opt::from_args();
98 ::std::process::exit(match run(opt) {
101 eprintln!("{}", err);
107 fn run(opt: Opt) -> Result<(), Box<std::error::Error>> {
108 let level = if opt.debug {
111 else if opt.verbose {
116 TermLogger::init(level, simplelog::Config::default())?;
118 let username = env::var("magnatune_username").map_err(|_| "The environment variable magnatune_username must be set. Suggestion: run read magnatune_username; export magnatune_username")?;
119 let password = env::var("magnatune_password").map_err(|_| "The environment variable magnatune_password must be set. Suggestion: run read -s magnatune_password; export magnatune_password")?;
121 let magnatune_sqlite_url = "http://he3.magnatune.com/info/sqlite_normalized.db.gz";
123 info!("Syncing database...");
124 let sqlite_file = sync_magnatune_db(magnatune_sqlite_url).unwrap();
126 let magnatune = MagnatuneClient::new(opt.magnatune_host, username, password).unwrap();
128 info!("Fetching favourites...");
129 let favourites = fetch_favourites(&magnatune, &sqlite_file, opt.limit)?;
131 let favourites = favourites.into_iter().filter_map(|r| {
135 info!("Problematic favourites RSS entry: {}", e);
140 trace!("{:#?}",favourites);
142 info!("Looking for missing albums...");
143 let mut missing_locally = Vec::new();
144 for album in favourites {
145 if check_album(&album, &opt.folder) {
146 trace!("OK: {} - {}", album.artist, album.name);
149 info!("Missing: {} - {}", album.artist, album.name);
150 missing_locally.push(album);
154 if missing_locally.len() > 0 {
155 info!("Downloading...");
157 for album in missing_locally {
158 download(&magnatune, &album, &opt.folder)?;
162 info!("Nothing missing. My work is done.");
168 fn fetch_favourites(client: &MagnatuneClient, sqlite_file: &Path, limit: Option<usize>) -> Result<Vec<Result<Album, Box<Error>>>, Box<Error>> {
169 let db = Connection::open(sqlite_file)?;
171 let mut url = client.url(&["member","favorites_export"])?;
172 url.query_pairs_mut().append_pair("format", "rss");
173 let res = client.get(url).send()?.error_for_status()?;
175 let reader = BufReader::new(res);
176 let channel = Channel::read_from(reader)?;
178 let iter = channel.items().into_iter();
179 let iter: Box<Iterator<Item = _>> = if let Some(n) = limit {
180 Box::new(iter.take(n))
186 let albums = iter.map(|item| {
187 if let Some(l) = item.link() {
191 return Err(From::from(format!("RSS entry without link: {:#?}", item)));
198 fn sync_magnatune_db(url: &str ) -> Result<PathBuf, Box<Error>> {
199 let project_dirs = ProjectDirs::from("org.cakebox", "", env!("CARGO_PKG_NAME"))
200 .ok_or_else(|| format!("Could not find project directory"))?;
201 let dir = project_dirs.cache_dir();
203 fs::create_dir_all(dir)?;
205 let filename = dir.join("magnatune.sqlite");
206 let attr = fs::metadata(&filename);
207 if attr.is_err() {//TODO: check age
208 info!("Magnatune database missing or outdated, downloading...");
209 download_magnatune_db(url, &filename).map_err(|e| format!("Error downloading magnatune database: {}", e))?;
215 fn download_magnatune_db(url: &str, filename: &Path) -> Result<(), Box<Error>> {
216 let client = Client::new();
218 let res = client.get(url).send()?.error_for_status()?;
220 let mut uncompressed_res = flate2::bufread::GzDecoder::new(BufReader::new(res));
222 let mut file = OpenOptions::new().write(true)
225 io::copy(&mut uncompressed_res, &mut file)?;
230 fn find_album(db: &Connection, link: &str) -> Result<Album, Box<Error>> {
231 let url = Url::parse(link)?;
232 let path: Vec<&str> = url.path_segments().unwrap().collect();
234 let parse_error = Err(From::from(format!("Expected url in artists/albums/:sku format: {}", link)));
235 if path.len() != 3 { return parse_error }
236 if path[0] != "artists" { return parse_error }
237 if path[1] != "albums" { return parse_error }
241 let album = lookup_sku_in_db(&db, &sku)?;
246 fn lookup_sku_in_db(db: &Connection, sku: &str) -> Result<Album, Box<Error>> {
247 let mut album_stmt = db.prepare_cached(
248 "SELECT album_id, albums.name, artists.name AS artist, albums.sku FROM albums JOIN artists ON (artist_id=artists_id) WHERE sku = ?"
250 let mut rows = album_stmt.query_and_then(&[&sku.to_string()], |album_row| {
251 let album_id: i64 = album_row.get(0);
252 let mut song_stmt = db.prepare_cached(
253 "SELECT name, track_no FROM songs WHERE album_id = ?"
255 let songs_iter = song_stmt.query_map(&[&album_id], |song_row| {
257 name: song_row.get(0),
258 track_no: song_row.get(1),
261 let mut songs: Vec<Song> = Vec::new();
262 for s in songs_iter {
267 name: album_row.get(1),
268 artist: album_row.get(2),
269 sku: album_row.get(3),
274 if let Some(album_result) = rows.next() {
278 Err(From::from(format!("Could not find album with SKU {} in database", sku)))
282 //TODO: also check that all songs are present
283 fn check_album(album: &Album, folder: &Path) -> bool {
284 let stat = folder.join(album.path()).metadata();
288 fn download(client: &MagnatuneClient, album: &Album, folder: &Path) -> Result<(), Box<Error>> {
289 info!("Downloading {}", album.name);
291 let album_folder = folder.join(album.path());
292 fs::create_dir_all(&album_folder)?;
294 download_and_unpack_flac(client, album, folder)?;
296 if let Err(e) = download_file(client, &["music", &album.artist, &album.name, "cover.jpg"], &album_folder.join("cover.jpg")) {
297 warn!("Error downloading cover: {}", e);
299 if let Err(e) = download_file(client, &["music", &album.artist, &album.name, "artwork.pdf"], &album_folder.join("artwork.pdf")) {
300 warn!("Error downloading artwork: {}", e);
306 fn download_and_unpack_flac(client: &MagnatuneClient, album: &Album, folder: &Path) -> Result<(), Box<Error>> {
307 // "http:/download.magnatune.com/music/".urlenc(artist)."/".urlenc(album)."/sku-wav.zip -- you won't need an API key.
308 let filename = format!("{}-flac.zip", album.sku);
309 let zip_path = folder.join(&filename);
310 download_file(client, &["music", &album.artist, &album.name, &filename], &zip_path)?;
312 unpack(&zip_path, &album, &folder)?;
314 fs::remove_file(zip_path)?;
319 fn download_file(client: &MagnatuneClient, url_path: &[&str], target: &Path) -> Result<(), Box<Error>> {
320 let mut resp = client.get(client.url(url_path)?).send()?.error_for_status()?;
322 let mut file = fs::OpenOptions::new().write(true).create_new(true).open(&target)?;
323 resp.copy_to(&mut file)?;
328 fn unpack(zip_path: &Path, album: &Album, folder: &Path) -> Result<(), Box<Error>> {
329 let album_folder = folder.join(album.path());
330 debug!("Unpacking {:?} to {:?}", zip_path, album_folder);//FIXME
332 let rdr = fs::File::open(zip_path)?;
333 let mut zip = ZipArchive::new(rdr)?;
335 for i in 0..zip.len() {
336 let mut file = zip.by_index(i)?;
338 info!("Extracting {:?}", file.sanitized_name());//FIXME
340 let mut relative_path = file.sanitized_name();
341 if relative_path.starts_with(&album.artist) {
342 relative_path = relative_path.strip_prefix(&album.artist).unwrap().into();
344 if relative_path.starts_with(&album.name) {
345 relative_path = relative_path.strip_prefix(&album.name).unwrap().into();
348 let path = album_folder.join(relative_path);
350 if ! path.parent().unwrap().canonicalize()?.starts_with(album_folder.canonicalize()?) {
351 return Err(format!("zip file wanted to write outside containing folder: {:?} is not beneath {:?}", path, folder).into()) //FIXME
354 debug!("Extracting to {:?}", path);//FIXME
355 let mut wtr = fs::OpenOptions::new().write(true).create_new(true).open(path)?;
356 io::copy(&mut file, &mut wtr)?;