···41414242 /// Convert files from one format to another during sync.
4343 Transcode(cmd::transcode::Args),
4444+4545+ /// Initializes an iPod mounted at the provided path.
4646+ InitializeIpod(cmd::ipod_init::Args),
4447}
+19
src/cmd/ipod_init.rs
···11+use anyhow::Result;
22+use clap::Args as ClapArgs;
33+44+use crate::gpod::gpod::IPodHandle;
55+66+#[derive(ClapArgs, Debug)]
77+pub struct Args {
88+ /// Path where iPod is mounted.
99+ #[arg(long)]
1010+ pub path: String,
1111+1212+ /// Model number, can be found in the iPod settings.
1313+ #[arg(long)]
1414+ pub model: String,
1515+}
1616+1717+pub async fn run(args: Args) -> Result<()> {
1818+ IPodHandle::initialize(args.path, args.model)
1919+}
+1
src/cmd/mod.rs
···33pub mod dupes;
44pub mod error;
55pub mod filter;
66+pub mod ipod_init;
67pub mod sync;
78pub mod transcode;
+58-21
src/cmd/sync.rs
···11use crate::cmd::*;
22use crate::db;
33use crate::ffmpeg;
44+use crate::gpod::gpod::IPodHandle;
45use crate::model;
56use anyhow::Ok;
67use anyhow::anyhow;
···4041 /// available.
4142 #[arg(long)]
4243 pub threads: Option<usize>,
4444+4545+ /// Treat destination as an iPod.
4646+ /// Tracks will appear as if they were transferred with iTunes or any iPod-compatible software.
4747+ /// Tunz DB will still be created and used to keep track.
4848+ #[arg(long)]
4949+ pub ipod: bool,
4350}
44514552impl Args {
···5764pub async fn run(args: Args) -> Result<()> {
5865 args.validate()?;
59666060- let threads = match args.threads {
6161- Some(t) => t,
6262- None => num_cpus::get(),
6767+ let threads = match args.ipod {
6868+ true => 1,
6969+ false => match args.threads {
7070+ Some(t) => t,
7171+ None => num_cpus::get(),
7272+ },
6373 };
64746575 let dest_dir = args.destination.unwrap();
···6878 .await
6979 .with_context(|| "Cannot open local database instance")?;
70807171- let dest_db = db::Instance::new(&dest_dir, true)
8181+ let dest_db = db::Instance::new(&dest_dir.clone(), true)
7282 .await
7383 .with_context(|| "Cannot open destination database instance")?;
7484···119129 filters.as_ref(),
120130 args.link,
121131 threads,
132132+ args.ipod,
122133 )
123134 .await?;
124135···214225 filters: Option<&Vec<crate::filter::ScriptRuntime>>,
215226 link: bool,
216227 threads: usize,
228228+ ipod: bool,
217229) -> Result<()> {
218230 let tr_settings = dest_db.get_transcoding_settings().await?;
219231···226238 false,
227239 )?;
228240241241+ if ipod {
242242+ // copy single-threaded as iPods update the database when a file is added
243243+ // on top of that, all tracks will be re-encoded to AAC 256kbit/s VBR.
244244+ //
245245+ // we encode in a temp directory, then copy the file over
246246+247247+ let td = tempdir::TempDir::new("tunz_ipod_copy")?;
248248+ let td = td.path().to_string_lossy().to_string();
249249+250250+ let ih = IPodHandle::new(dest_dir.clone())?;
251251+ let ch = ih.copy_process();
252252+253253+ let transcoding = Some(ffmpeg::Quality {
254254+ bitrate: ffmpeg::Bitrate::CBR("256k".to_owned()),
255255+ sampling_rate: Some(ffmpeg::SamplingRate::Rate441K),
256256+ codec: ffmpeg::Codec::AAC,
257257+ });
258258+ for track in tracks {
259259+ copy(&track, dest_db, &td, transcoding.clone(), link).await?;
260260+ let tr_track_path = std::path::PathBuf::from(
261261+ &track.storage_path_with_extension(&td, &ffmpeg::Codec::AAC.extension()),
262262+ );
263263+264264+ log::info!("copying to iPod...");
265265+266266+ ch.copy_file(
267267+ &track,
268268+ tr_track_path,
269269+ ffmpeg::Codec::AAC.extension(),
270270+ 44100,
271271+ 256,
272272+ )?;
273273+ }
274274+ return Ok(());
275275+ }
276276+229277 stream::iter(tracks)
230278 .map(|track| {
231279 let trs = tr_settings.clone();
232232- async move { do_copy(track, dest_db, dest_dir, trs, link).await }
280280+ async move { copy(&track, dest_db, dest_dir, trs, link).await }
233281 })
234282 .buffer_unordered(threads)
235283 .try_for_each(|_| async move { Ok(()) })
236284 .await
237237-}
238238-239239-async fn do_copy(
240240- track: model::Track,
241241- dest_db: &db::Instance,
242242- dest_dir: &String,
243243- transcoding_settings: Option<ffmpeg::Quality>,
244244- link: bool,
245245-) -> Result<()> {
246246- if let Some(ts) = transcoding_settings.clone() {
247247- log::info!("copying and transcoding {} ({:?})", track, ts);
248248- } else {
249249- log::info!("copying {}", track);
250250- }
251251- copy(track, &dest_db, &dest_dir, transcoding_settings, link).await
252285}
253286254287async fn dry_run_copy(
···338371}
339372340373async fn copy(
341341- track: model::Track,
374374+ track: &model::Track,
342375 dest_db: &db::Instance,
343376 dest_dir: &String,
344377 transcoding_settings: Option<ffmpeg::Quality>,
···376409 async_std::task::spawn_blocking(move || {
377410 // TODO: don't transcode if it's already lossy
378411 if let Some(ts) = ts_for_thread {
412412+ log::info!("copying and transcoding {} ({})", track_for_thread, ts);
413413+379414 ffmpeg::convert(
380415 &track_for_thread,
381416 std::path::PathBuf::from_str(
···386421 &ts,
387422 )?;
388423 } else {
424424+ log::info!("copying {}", track_for_thread);
425425+389426 if link {
390427 std::fs::hard_link(
391428 track_for_thread.file_path.clone(),