diff --git a/Cargo.lock b/Cargo.lock index 0c0d5e2..cf88361 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -775,6 +775,7 @@ dependencies = [ "librespot-audio", "librespot-core", "librespot-metadata", + "linear-map", "log 0.4.11", "regex", "sanitize-filename", diff --git a/Cargo.toml b/Cargo.toml index 14d32e0..7cba7a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,13 +10,14 @@ repository = "https://github.com/pisto/oggify" readme = "README.md" [dependencies] -tokio-core = "0.1.17" +env_logger = "0.6" +indexmap = "1.6" librespot-core = { git = "https://github.com/librespot-org/librespot.git", rev="0adb8516a65f551211a9ce78c2d822b85454546c" } librespot-metadata = { git = "https://github.com/librespot-org/librespot.git", rev="0adb8516a65f551211a9ce78c2d822b85454546c" } librespot-audio = { git = "https://github.com/librespot-org/librespot.git", rev="0adb8516a65f551211a9ce78c2d822b85454546c" } -regex = "1.1.0" +linear-map = "1.2" log = "0.4.6" -env_logger = "0.6.0" +regex = "1.1" +sanitize-filename = "0.3" scoped_threadpool = "0.1.9" -sanitize-filename = "0.3.0" -indexmap = "1.6.1" +tokio-core = "0.1.17" diff --git a/src/main.rs b/src/main.rs index c876944..a960753 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,38 +1,32 @@ -extern crate env_logger; -extern crate librespot_audio; -extern crate librespot_core; -extern crate librespot_metadata; #[macro_use] extern crate log; -extern crate regex; -extern crate sanitize_filename; -extern crate scoped_threadpool; -extern crate tokio_core; -use std::{env, panic}; -use std::io::Write; -use std::io::{self, BufRead, Read, Result}; +use std::io::{self, BufRead}; +use std::io::{Read, Write}; use std::path::Path; use std::process::{Command, Stdio}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::Duration; +use std::rc::Rc; -use env_logger::{Builder, Env}; use indexmap::map::IndexMap; use librespot_audio::{AudioDecrypt, AudioFile}; -use librespot_core::authentication::Credentials; -use librespot_core::config::SessionConfig; -use librespot_core::session::Session; -use librespot_core::spotify_id::SpotifyId; +use librespot_core::spotify_id::{FileId, SpotifyId}; +use librespot_core::{authentication::Credentials, config::SessionConfig, session::Session}; use librespot_metadata::{Album, Artist, Episode, FileFormat, Metadata, Playlist, Show, Track}; use regex::Regex; use scoped_threadpool::Pool; use tokio_core::reactor::Core; +enum IndexedTy { + Track { album_name: Option> }, + Episode { show: Option> }, +} + +type Files = linear_map::LinearMap; + fn main() { - Builder::from_env(Env::default().default_filter_or("info")).init(); + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); - let args: Vec<_> = env::args().collect(); + let args: Vec<_> = std::env::args().collect(); assert!( args.len() == 3 || args.len() == 4, "Usage: {} user password [helper_script] < tracks_file", @@ -40,12 +34,16 @@ fn main() { ); let mut core = Core::new().unwrap(); - let handle = core.handle(); let session_config = SessionConfig::default(); let credentials = Credentials::with_password(args[1].to_owned(), args[2].to_owned()); info!("Connecting ..."); let session = core - .run(Session::connect(session_config, credentials, None, handle)) + .run(Session::connect( + session_config, + credentials, + None, + core.handle(), + )) .unwrap(); info!("Connected!"); @@ -59,56 +57,63 @@ fn main() { for line in io::stdin().lock().lines() { match line { Ok(line) => { - if line.trim() == "done" { + let line = line.trim(); + if line == "done" { break; } - let spotify_captures = re.captures(&line.trim()); - if spotify_captures.is_none() { - continue; - } - let spotify_match = spotify_captures.unwrap(); + let spotify_match = match re.captures(line) { + None => continue, + Some(x) => x, + }; let spotify_type = spotify_match.get(1).unwrap().as_str(); let spotify_id = SpotifyId::from_base62(spotify_match.get(2).unwrap().as_str()).unwrap(); match spotify_type { - "playlist" => { - let playlist = core.run(Playlist::get(&session, spotify_id)).unwrap(); - for track_id in playlist.tracks { - ids.insert(track_id, "track"); - } - } + "playlist" => ids.extend( + core.run(Playlist::get(&session, spotify_id)) + .unwrap() + .tracks + .into_iter() + .map(|id| (id, IndexedTy::Track { album_name: None })), + ), "album" => { let album = core.run(Album::get(&session, spotify_id)).unwrap(); - for track_id in album.tracks { - ids.insert(track_id, "track"); - } + let album_name = Rc::new(album.name); + ids.extend(album.tracks.into_iter().map(|id| { + ( + id, + IndexedTy::Track { + album_name: Some(album_name.clone()), + }, + ) + })); } "show" => { - let show = core.run(Show::get(&session, spotify_id)).unwrap(); - let mut episodes = IndexMap::new(); - for episode_id in show.episodes { - episodes.insert(episode_id, "episode"); - } + let show = Rc::new(core.run(Show::get(&session, spotify_id)).unwrap()); // Since Spotify returns the IDs of episodes in a show in reverse order, // we have to reverse it ourselves again. - episodes.reverse(); - for (key, value) in episodes.iter() { - ids.insert(*key, value); - } + ids.extend(show.episodes.iter().rev().map(|&id| { + ( + id, + IndexedTy::Episode { + show: Some(show.clone()), + }, + ) + })); } "track" => { - ids.insert(spotify_id, "track"); + ids.insert(spotify_id, IndexedTy::Track { album_name: None }); } "episode" => { - ids.insert(spotify_id, "episode"); + ids.insert(spotify_id, IndexedTy::Episode { show: None }); } - _ => warn!("Unknown link type."), + _ => warn!("Unknown link type: {}", spotify_type), }; } @@ -117,35 +122,30 @@ fn main() { } for (id, value) in ids { + let fmtid = id.to_base62(); match value { - "track" => { - info!("Getting track {}...", id.to_base62()); - let track_result = core.run(Track::get(&session, id)); - if track_result.is_ok() { - let mut track = track_result.unwrap(); + IndexedTy::Track { mut album_name } => { + info!("Getting track {}...", fmtid); + if let Ok(mut track) = core.run(Track::get(&session, id)) { if !track.available { - warn!( - "Track {} is not available, finding alternative...", - id.to_base62() - ); - let alt_track = track.alternatives.iter().find_map(|id| { - let alt_track = core - .run(Track::get(&session, *id)) - .expect("Cannot get track metadata"); - match alt_track.available { - true => Some(alt_track), - false => None, + warn!("Track {} is not available, finding alternative...", fmtid); + let alt_track = track + .alternatives + .iter() + .map(|id| { + core.run(Track::get(&session, *id)) + .expect("Cannot get track metadata") + }) + .find(|alt_track| alt_track.available); + track = match alt_track { + Some(x) => { + warn!("Found track alternative {} -> {}", fmtid, x.id.to_base62()); + x } - }); - track = alt_track.expect(&format!( - "Could not find alternative for track {}", - id.to_base62() - )); - warn!( - "Found track alternative {} -> {}", - id.to_base62(), - track.id.to_base62() - ); + None => { + panic!("Could not find alternative for track {}", fmtid); + } + }; } let artists_strs: Vec<_> = track .artists @@ -156,179 +156,144 @@ fn main() { .name }) .collect(); - debug!( - "File formats: {}", - track - .files - .keys() - .map(|filetype| format!("{:?}", filetype)) - .collect::>() - .join(" ") + handle_entry( + &mut core, + &mut threadpool, + &session, + &args[..], + track.id, + &track.files, + &fmtid, + &track.name, + |core| { + album_name + .get_or_insert_with(|| { + Rc::new( + core.run(Album::get(&session, track.album)) + .expect("Cannot get album metadata") + .name, + ) + }) + .as_str() + }, + &artists_strs, ); - let file_id = track - .files - .get(&FileFormat::OGG_VORBIS_320) - .or(track.files.get(&FileFormat::OGG_VORBIS_160)) - .or(track.files.get(&FileFormat::OGG_VORBIS_96)) - .expect("Could not find a OGG_VORBIS format for the track."); - let key = core - .run(session.audio_key().request(track.id, *file_id)) - .expect("Cannot get audio key"); - let mut encrypted_file = core - .run(AudioFile::open(&session, *file_id, 320, true)) - .unwrap(); - let mut buffer = Vec::new(); - let mut read_all: Result = Ok(0); - let fname = sanitize_filename::sanitize(format!( - "{} - {}.ogg", - artists_strs.join(", "), - track.name - )); - - if Path::new(&fname).exists() { - info!("File {} already exists.", fname); - } else { - let fetched = AtomicBool::new(false); - threadpool.scoped(|scope| { - scope.execute(|| { - read_all = encrypted_file.read_to_end(&mut buffer); - fetched.store(true, Ordering::Release); - }); - while !fetched.load(Ordering::Acquire) { - core.turn(Some(Duration::from_millis(100))); - } - }); - read_all.expect("Cannot read file stream"); - let mut decrypted_buffer = Vec::new(); - AudioDecrypt::new(key, &buffer[..]) - .read_to_end(&mut decrypted_buffer) - .expect("Cannot decrypt stream"); - if args.len() == 3 { - let fname = sanitize_filename::sanitize(format!( - "{} - {}.ogg", - artists_strs.join(", "), - track.name - )); - if Path::new(&fname).exists() { - info!("File {} already exists.", fname); - } else { - std::fs::write(&fname, &decrypted_buffer[0xa7..]) - .expect("Cannot write decrypted track"); - info!("Filename: {}", fname); - } - } else { - let album = core - .run(Album::get(&session, track.album)) - .expect("Cannot get album metadata"); - let mut cmd = Command::new(args[3].to_owned()); - cmd.stdin(Stdio::piped()); - cmd.arg(id.to_base62()) - .arg(track.name) - .arg(album.name) - .args(artists_strs.iter()); - let mut child = cmd.spawn().expect("Could not run helper program"); - let pipe = child.stdin.as_mut().expect("Could not open helper stdin"); - pipe.write_all(&decrypted_buffer[0xa7..]) - .expect("Failed to write to stdin"); - assert!( - child - .wait() - .expect("Out of ideas for error messages") - .success(), - "Helper script returned an error" - ); - } - } } } - "episode" => { - info!("Getting episode {}...", id.to_base62()); - let episode_result = core.run(Episode::get(&session, id)); - if episode_result.is_ok() { - let episode = episode_result.unwrap(); + IndexedTy::Episode { show } => { + info!("Getting episode {}...", fmtid); + if let Ok(episode) = core.run(Episode::get(&session, id)) { if !episode.available { - warn!("Episode {} is not available.", id.to_base62()); + warn!("Episode {} is not available.", fmtid); } - let show = core - .run(Show::get(&session, episode.show)) - .expect("Cannot get show"); - debug!( - "File formats: {}", - episode - .files - .keys() - .map(|filetype| format!("{:?}", filetype)) - .collect::>() - .join(" ") + let show = show.unwrap_or_else(|| { + Rc::new( + core.run(Show::get(&session, episode.show)) + .expect("Cannot get show"), + ) + }); + let sname = &show.name; + handle_entry( + &mut core, + &mut threadpool, + &session, + &args[..], + episode.id, + &episode.files, + &fmtid, + &episode.name, + |_| sname, + &[show.publisher.clone()], ); - let file_id = episode - .files - .get(&FileFormat::OGG_VORBIS_320) - .or(episode.files.get(&FileFormat::OGG_VORBIS_160)) - .or(episode.files.get(&FileFormat::OGG_VORBIS_96)) - .expect("Could not find a OGG_VORBIS format for the episode."); - let key = core - .run(session.audio_key().request(episode.id, *file_id)) - .expect("Cannot get audio key"); - let mut encrypted_file = core - .run(AudioFile::open(&session, *file_id, 320, true)) - .unwrap(); - let mut buffer = Vec::new(); - let mut read_all: Result = Ok(0); - let fname = format!("{} - {}.ogg", show.publisher, episode.name); - if Path::new(&fname).exists() { - info!("File {} already exists.", fname); - } else { - let fetched = AtomicBool::new(false); - threadpool.scoped(|scope| { - scope.execute(|| { - read_all = encrypted_file.read_to_end(&mut buffer); - fetched.store(true, Ordering::Release); - }); - while !fetched.load(Ordering::Acquire) { - core.turn(Some(Duration::from_millis(100))); - } - }); - read_all.expect("Cannot read file stream"); - let mut decrypted_buffer = Vec::new(); - AudioDecrypt::new(key, &buffer[..]) - .read_to_end(&mut decrypted_buffer) - .expect("Cannot decrypt stream"); - if args.len() == 3 { - if Path::new(&fname).exists() { - info!("File {} already exists.", fname); - } else { - std::fs::write(&fname, &decrypted_buffer[0xa7..]) - .expect("Cannot write decrypted episode"); - info!("Filename: {}", fname); - } - } else { - let mut cmd = Command::new(args[3].to_owned()); - cmd.stdin(Stdio::piped()); - cmd.arg(id.to_base62()) - .arg(episode.name) - .arg(show.name) - .arg(show.publisher); - let mut child = cmd.spawn().expect("Could not run helper program"); - let pipe = child.stdin.as_mut().expect("Could not open helper stdin"); - pipe.write_all(&decrypted_buffer[0xa7..]) - .expect("Failed to write to stdin"); - assert!( - child - .wait() - .expect("Out of ideas for error messages") - .success(), - "Helper script returned an error" - ); - } - } } } + } + } +} - _ => { - warn!("Error {}", value); +fn handle_entry( + core: &mut Core, + threadpool: &mut Pool, + session: &Session, + args: &[String], + track_id: SpotifyId, + files: &Files, + fmtid: &str, + element: &str, + group_getter: GG, + origins: &[String], +) where + GG: FnOnce(&mut Core) -> GR, + GR: AsRef, +{ + let fname = sanitize_filename::sanitize(format!("{} - {}.ogg", origins.join(", "), element)); + if Path::new(&fname).exists() { + info!("File {} already exists.", fname); + return; + } + debug!( + "File formats:{}", + files.keys().fold(String::new(), |mut acc, filetype| { + acc.push(' '); + acc += &format!("{:?}", filetype); + acc + }) + ); + let file_id = *files + .get(&FileFormat::OGG_VORBIS_320) + .or_else(|| files.get(&FileFormat::OGG_VORBIS_160)) + .or_else(|| files.get(&FileFormat::OGG_VORBIS_96)) + .expect("Could not find a OGG_VORBIS format for the track."); + let key = core + .run(session.audio_key().request(track_id, file_id)) + .expect("Cannot get audio key"); + let mut encrypted_file = core + .run(AudioFile::open(&session, file_id, 320, true)) + .unwrap(); + let mut buffer = Vec::new(); + { + use std::sync::atomic::{AtomicBool, Ordering}; + let dur = std::time::Duration::from_millis(100); + let fetched = AtomicBool::new(false); + let mut read_all = Ok(0); + threadpool.scoped(|scope| { + scope.execute(|| { + read_all = encrypted_file + .read_to_end(&mut buffer); + fetched.store(true, Ordering::Release); + }); + while !fetched.load(Ordering::Acquire) { + core.turn(Some(dur)); } - } + }); + read_all.expect("Cannot read file stream"); + } + let mut decrypted_buffer = Vec::new(); + AudioDecrypt::new(key, &buffer[..]) + .read_to_end(&mut decrypted_buffer) + .expect("Cannot decrypt stream"); + let decrypted_buffer = &decrypted_buffer[0xa7..]; + if args.len() == 3 { + std::fs::write(&fname, decrypted_buffer).expect("Cannot write decrypted audio stream"); + info!("Filename: {}", fname); + } else { + let mut cmd = Command::new(&args[3]); + cmd.stdin(Stdio::piped()); + cmd.arg(fmtid) + .arg(element) + .arg(group_getter(core).as_ref()) + .args(origins.iter().map(|i| i.as_str())); + let mut child = cmd.spawn().expect("Could not run helper program"); + let pipe = child.stdin.as_mut().expect("Could not open helper stdin"); + pipe.write_all(decrypted_buffer) + .expect("Failed to write to stdin"); + assert!( + child + .wait() + .expect("Out of ideas for error messages") + .success(), + "Helper script returned an error" + ); } }