A tool to sync music with your favorite devices
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

*: wire ipod sync, ipod initialization

maybe i'll pull off ipod restore as well, who knows

Gee Sawra 2c504877 80c6edd5

+123 -29
+12
.sqlx/query-982e25be3542b9e39f9b6c9141330a803358fcf473516599c605dbabc9dc3ff1.json
··· 1 + { 2 + "db_name": "SQLite", 3 + "query": "\n INSERT OR REPLACE INTO tracks (\n track_id,\n title,\n artist,\n album,\n genre,\n track_len,\n year,\n number,\n file_path,\n disc_number,\n disc_total,\n file_state,\n extension\n ) VALUES (\n ?1,\n ?2,\n ?3,\n ?4,\n ?5,\n ?6,\n ?7,\n ?8,\n ?9,\n ?10,\n ?11,\n ?12,\n ?13\n );\n ", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Right": 13 8 + }, 9 + "nullable": [] 10 + }, 11 + "hash": "982e25be3542b9e39f9b6c9141330a803358fcf473516599c605dbabc9dc3ff1" 12 + }
+3
src/cli.rs
··· 41 41 42 42 /// Convert files from one format to another during sync. 43 43 Transcode(cmd::transcode::Args), 44 + 45 + /// Initializes an iPod mounted at the provided path. 46 + InitializeIpod(cmd::ipod_init::Args), 44 47 }
+19
src/cmd/ipod_init.rs
··· 1 + use anyhow::Result; 2 + use clap::Args as ClapArgs; 3 + 4 + use crate::gpod::gpod::IPodHandle; 5 + 6 + #[derive(ClapArgs, Debug)] 7 + pub struct Args { 8 + /// Path where iPod is mounted. 9 + #[arg(long)] 10 + pub path: String, 11 + 12 + /// Model number, can be found in the iPod settings. 13 + #[arg(long)] 14 + pub model: String, 15 + } 16 + 17 + pub async fn run(args: Args) -> Result<()> { 18 + IPodHandle::initialize(args.path, args.model) 19 + }
+1
src/cmd/mod.rs
··· 3 3 pub mod dupes; 4 4 pub mod error; 5 5 pub mod filter; 6 + pub mod ipod_init; 6 7 pub mod sync; 7 8 pub mod transcode;
+58 -21
src/cmd/sync.rs
··· 1 1 use crate::cmd::*; 2 2 use crate::db; 3 3 use crate::ffmpeg; 4 + use crate::gpod::gpod::IPodHandle; 4 5 use crate::model; 5 6 use anyhow::Ok; 6 7 use anyhow::anyhow; ··· 40 41 /// available. 41 42 #[arg(long)] 42 43 pub threads: Option<usize>, 44 + 45 + /// Treat destination as an iPod. 46 + /// Tracks will appear as if they were transferred with iTunes or any iPod-compatible software. 47 + /// Tunz DB will still be created and used to keep track. 48 + #[arg(long)] 49 + pub ipod: bool, 43 50 } 44 51 45 52 impl Args { ··· 57 64 pub async fn run(args: Args) -> Result<()> { 58 65 args.validate()?; 59 66 60 - let threads = match args.threads { 61 - Some(t) => t, 62 - None => num_cpus::get(), 67 + let threads = match args.ipod { 68 + true => 1, 69 + false => match args.threads { 70 + Some(t) => t, 71 + None => num_cpus::get(), 72 + }, 63 73 }; 64 74 65 75 let dest_dir = args.destination.unwrap(); ··· 68 78 .await 69 79 .with_context(|| "Cannot open local database instance")?; 70 80 71 - let dest_db = db::Instance::new(&dest_dir, true) 81 + let dest_db = db::Instance::new(&dest_dir.clone(), true) 72 82 .await 73 83 .with_context(|| "Cannot open destination database instance")?; 74 84 ··· 119 129 filters.as_ref(), 120 130 args.link, 121 131 threads, 132 + args.ipod, 122 133 ) 123 134 .await?; 124 135 ··· 214 225 filters: Option<&Vec<crate::filter::ScriptRuntime>>, 215 226 link: bool, 216 227 threads: usize, 228 + ipod: bool, 217 229 ) -> Result<()> { 218 230 let tr_settings = dest_db.get_transcoding_settings().await?; 219 231 ··· 226 238 false, 227 239 )?; 228 240 241 + if ipod { 242 + // copy single-threaded as iPods update the database when a file is added 243 + // on top of that, all tracks will be re-encoded to AAC 256kbit/s VBR. 244 + // 245 + // we encode in a temp directory, then copy the file over 246 + 247 + let td = tempdir::TempDir::new("tunz_ipod_copy")?; 248 + let td = td.path().to_string_lossy().to_string(); 249 + 250 + let ih = IPodHandle::new(dest_dir.clone())?; 251 + let ch = ih.copy_process(); 252 + 253 + let transcoding = Some(ffmpeg::Quality { 254 + bitrate: ffmpeg::Bitrate::CBR("256k".to_owned()), 255 + sampling_rate: Some(ffmpeg::SamplingRate::Rate441K), 256 + codec: ffmpeg::Codec::AAC, 257 + }); 258 + for track in tracks { 259 + copy(&track, dest_db, &td, transcoding.clone(), link).await?; 260 + let tr_track_path = std::path::PathBuf::from( 261 + &track.storage_path_with_extension(&td, &ffmpeg::Codec::AAC.extension()), 262 + ); 263 + 264 + log::info!("copying to iPod..."); 265 + 266 + ch.copy_file( 267 + &track, 268 + tr_track_path, 269 + ffmpeg::Codec::AAC.extension(), 270 + 44100, 271 + 256, 272 + )?; 273 + } 274 + return Ok(()); 275 + } 276 + 229 277 stream::iter(tracks) 230 278 .map(|track| { 231 279 let trs = tr_settings.clone(); 232 - async move { do_copy(track, dest_db, dest_dir, trs, link).await } 280 + async move { copy(&track, dest_db, dest_dir, trs, link).await } 233 281 }) 234 282 .buffer_unordered(threads) 235 283 .try_for_each(|_| async move { Ok(()) }) 236 284 .await 237 - } 238 - 239 - async fn do_copy( 240 - track: model::Track, 241 - dest_db: &db::Instance, 242 - dest_dir: &String, 243 - transcoding_settings: Option<ffmpeg::Quality>, 244 - link: bool, 245 - ) -> Result<()> { 246 - if let Some(ts) = transcoding_settings.clone() { 247 - log::info!("copying and transcoding {} ({:?})", track, ts); 248 - } else { 249 - log::info!("copying {}", track); 250 - } 251 - copy(track, &dest_db, &dest_dir, transcoding_settings, link).await 252 285 } 253 286 254 287 async fn dry_run_copy( ··· 338 371 } 339 372 340 373 async fn copy( 341 - track: model::Track, 374 + track: &model::Track, 342 375 dest_db: &db::Instance, 343 376 dest_dir: &String, 344 377 transcoding_settings: Option<ffmpeg::Quality>, ··· 376 409 async_std::task::spawn_blocking(move || { 377 410 // TODO: don't transcode if it's already lossy 378 411 if let Some(ts) = ts_for_thread { 412 + log::info!("copying and transcoding {} ({})", track_for_thread, ts); 413 + 379 414 ffmpeg::convert( 380 415 &track_for_thread, 381 416 std::path::PathBuf::from_str( ··· 386 421 &ts, 387 422 )?; 388 423 } else { 424 + log::info!("copying {}", track_for_thread); 425 + 389 426 if link { 390 427 std::fs::hard_link( 391 428 track_for_thread.file_path.clone(),
+22 -8
src/ffmpeg.rs
··· 117 117 f, 118 118 "{}", 119 119 match self { 120 - Codec::AAC => "aac", 120 + Codec::AAC => "libfdk_aac", 121 121 Codec::MP3 => "mp3", 122 122 Codec::OGG => "libvorbis", 123 123 Codec::Opus => "opus", ··· 209 209 pub codec: Codec, 210 210 } 211 211 212 + impl std::fmt::Display for Quality { 213 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 214 + write!( 215 + f, 216 + "{}/{}/{}", 217 + self.codec.extension(), 218 + self.bitrate, 219 + self.sampling_rate.unwrap_or(SamplingRate::Rate441K) 220 + ) 221 + } 222 + } 223 + 212 224 pub fn convert(track: &Track, dest: PathBuf, quality: &Quality) -> Result<()> { 213 - let res = std::process::Command::new("ffmpeg") 225 + let mut cmd = std::process::Command::new("ffmpeg"); 226 + let cmd = cmd 214 227 .arg("-y") 215 228 .arg("-i") 216 229 .arg(track.file_path.clone()) ··· 236 249 .split(" ") 237 250 .collect::<Vec<&str>>(), 238 251 ) 239 - .arg(format!( 240 - "{}.{}", 241 - dest.display().to_string(), 242 - quality.codec.extension() 243 - )) 244 - .output()?; 252 + .arg(dest.display().to_string()); 253 + 254 + let res = match quality.codec { 255 + Codec::AAC => cmd.args(["-aac_pns", "0"]), 256 + _ => cmd, 257 + } 258 + .output()?; 245 259 246 260 match res.status.success() { 247 261 true => Ok(()),
+2
src/main.rs
··· 5 5 mod ffmpeg; 6 6 mod filter; 7 7 mod fs; 8 + mod gpod; 8 9 mod model; 9 10 10 11 #[async_std::main] ··· 22 23 cli::Commands::Settings { command } => match command { 23 24 cli::Settings::Filter(args) => Ok(cmd::filter::run(args).await?), 24 25 cli::Settings::Transcode(args) => Ok(cmd::transcode::run(args).await?), 26 + cli::Settings::InitializeIpod(args) => Ok(cmd::ipod_init::run(args).await?), 25 27 }, 26 28 } 27 29 }
+6
src/model.rs
··· 62 62 pub title: String, 63 63 pub artist: String, 64 64 pub album: String, 65 + pub genre: String, 66 + pub track_len: i64, 67 + pub year: usize, 65 68 pub number: i64, 66 69 pub file_path: String, 67 70 pub disc_number: i64, ··· 130 133 disc_total: disc.1.unwrap_or_default() as i64, 131 134 file_state: FileState::Unknown, 132 135 extension: String::new(), 136 + genre: track.tags.genre().unwrap_or("Unknown Genre").to_owned(), 137 + track_len: track.tags.duration().unwrap() as i64, 138 + year: track.tags.year().unwrap_or(1970) as usize, 133 139 }; 134 140 135 141 t.track_id = track_hash(&t);