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.

*: fix syncing, filtering, finish implementation of ipod bits for deletion

gtk gui is broken, but i'm sure Gemini will have fun fixing it

Gee Sawra 687ecc03 f8f650ec

+193 -388
+1
Cargo.lock
··· 3142 3142 dependencies = [ 3143 3143 "anyhow", 3144 3144 "async-std", 3145 + "async-trait", 3145 3146 "audiotags", 3146 3147 "bindgen", 3147 3148 "clap",
+1
Cargo.toml
··· 55 55 lofty = "0.22.4" 56 56 gtk4 = { version = "0.10.3", features = ["v4_20"] } 57 57 libadwaita = { version = "0.8.1", features = ["v1_6"] } 58 + async-trait = "0.1.89" 58 59 59 60 # Re-enable those and compile with "gui" feature when you're ready 60 61 # to tackle GUI again.
+3 -3
src/cmd/default_filter.rhai
··· 1 1 /* 2 2 Hi! 3 3 4 - This is the filter file, in here you can write Rhai code to filter out tracks 4 + This is the filter file, in here you can write Rhai code to filter out tracks 5 5 during the sync process. 6 6 7 - This file must contain the `filter(track)` function, which will be evaluated for 7 + This file must contain the `filter(track)` function, which will be evaluated for 8 8 every candidate copy track: return `true` to copy the track. 9 9 10 10 If you want to match any String to a regex, call `regex_match(pattern, your_string)`. ··· 15 15 fn filter(track) { 16 16 // add your filtering code here! 17 17 18 - true 18 + false 19 19 }
+5 -21
src/cmd/filter.rs
··· 1 1 use crate::{cmd::error, db, filter}; 2 - use anyhow::{anyhow, Context, Result}; 2 + use anyhow::{Context, Result, anyhow}; 3 3 use clap::Args as ClapArgs; 4 4 5 5 const DEFAULT_FILTER: &'static str = include_str!("default_filter.rhai"); 6 6 7 7 #[derive(ClapArgs)] 8 8 pub struct Args { 9 - /// Path where tunz database is stored. 10 - #[arg(long)] 11 - pub destination: Option<String>, 9 + /// Directory in which tunz will store its local database. 10 + #[arg(short, long, default_value_t = db::default_database_dir().to_str().unwrap().to_owned())] 11 + pub database_path: String, 12 12 13 13 /// Read existing filters off the database. 14 14 #[arg(long)] ··· 21 21 pub file: Option<String>, 22 22 } 23 23 24 - impl Args { 25 - pub fn validate(&self) -> Result<()> { 26 - if let None = self.destination { 27 - return Err(anyhow!(error::Error::ValidationError( 28 - "missing destination".to_owned(), 29 - ))); 30 - }; 31 - 32 - Ok(()) 33 - } 34 - } 35 - 36 24 pub async fn run(args: Args) -> Result<()> { 37 - args.validate()?; 38 - 39 - let destination = args.destination.unwrap(); 40 - 41 - let dest_db = db::Instance::new(&destination, true).await?; 25 + let dest_db = db::Instance::new(&args.database_path, false).await?; 42 26 43 27 let existing_filter = dest_db 44 28 .filter()
+34 -91
src/cmd/sync.rs
··· 1 + use std::path::PathBuf; 2 + use std::str::FromStr; 3 + 1 4 use crate::cmd::*; 2 5 use crate::db; 6 + use crate::device; 7 + use crate::model; 3 8 use crate::sync; 9 + use crate::sync::filter_tracks; 4 10 use anyhow::Ok; 5 11 use anyhow::anyhow; 6 12 use anyhow::{Context, Result}; ··· 19 25 /// Do not delete from destination tracks that are not contained in the local database instance. 20 26 #[arg(long, default_value_t = false)] 21 27 pub no_delete: bool, 22 - 23 - /// Do not attempt to copy any file or write any change on destination database, just 24 - /// print what tracks would be copied over. 25 - #[arg(long, default_value_t = false)] 26 - pub dry_run: bool, 27 28 28 29 /// Amount of threads to use for copy and transcoding, defaults to number of CPUs 29 30 /// available. ··· 62 63 }; 63 64 64 65 let dest_dir = args.destination.unwrap(); 66 + let dest_path = PathBuf::from_str(&dest_dir).unwrap(); 67 + 68 + let dest = match args.ipod { 69 + true => device::IPod::new(dest_path).await?, 70 + false => device::Disk::new(dest_path).await?, 71 + }; 65 72 66 73 let local_db = db::Instance::new(&args.database_path, false) 67 74 .await 68 75 .with_context(|| "Cannot open local database instance")?; 69 76 70 - let dest_db = db::Instance::new(&dest_dir.clone(), true) 71 - .await 72 - .with_context(|| "Cannot open destination database instance")?; 73 - 74 - let diff = db::diff(&local_db, &dest_db) 75 - .await 76 - .with_context(|| "Cannot calculate difference between local and destination databases")?; 77 - 78 - let mut reverse_diff = db::diff(&dest_db, &local_db) 79 - .await 80 - .with_context(|| "Cannot calculate difference between destination and local databases")?; 81 - 82 - let raw_filter = local_db 77 + let filters = match local_db 83 78 .filter() 84 79 .await 85 - .with_context(|| "Could not fetch filters.")?; 86 - 87 - let filters = match raw_filter { 80 + .with_context(|| "Could not fetch filters.")? 81 + { 88 82 Some(raw_filter) => Some( 89 83 crate::filter::evaluate(vec![raw_filter]) 90 84 .with_context(|| "Could not evaluate filters")?, ··· 92 86 None => None, 93 87 }; 94 88 95 - // find any filtered tracks that were already copied 96 - reverse_diff.append( 97 - &mut sync::diff_databases(&local_db, &dest_db, filters.as_ref(), false).await?, 98 - ); 89 + let local_tracks = local_db.tracks_by_state(model::FileState::Copied).await?; 99 90 100 - // now filter out all tracks to copy by using the filters 101 - let diff = sync::filter_tracks_by_id(filters.as_ref(), &local_db, diff).await?; 91 + // filter out tracks that we should ignore 92 + let local_tracks = filter_tracks(local_tracks, filters.as_ref(), !args.no_delete)?; 93 + 94 + let dest_tracks = dest.list_by_state(model::FileState::Copied).await?; 95 + 96 + // tracks that are on local but not on dest 97 + let diff = db::diff_vec(local_tracks.clone(), dest_tracks.clone()) 98 + .await 99 + .with_context(|| "Cannot calculate difference between local and destination databases")?; 102 100 103 - if args.dry_run { 104 - dry_run_copy(&local_db, &dest_dir, diff, filters.as_ref()).await?; 105 - dry_run_delete(&dest_db, &dest_dir, reverse_diff, filters.as_ref()).await?; 106 - return Ok(()); 107 - } 101 + // tracks that are on dest but not on local 102 + let reverse_diff = db::diff_vec(dest_tracks.clone(), local_tracks.clone()) 103 + .await 104 + .with_context(|| "Cannot calculate difference between destination and local databases")?; 108 105 109 106 if !args.no_delete { 110 - sync::run_delete(&dest_db, &dest_dir, reverse_diff, filters.as_ref()).await? 107 + for track in reverse_diff { 108 + log::info!("deleting {}", track); 109 + dest.delete(track).await?; 110 + } 111 111 } 112 112 113 113 log::info!("using {} threads", threads); 114 114 115 - sync::run_copy( 116 - &local_db, 117 - &dest_db, 118 - &dest_dir, 119 - diff, 120 - filters.as_ref(), 121 - threads, 122 - args.ipod, 123 - ) 124 - .await?; 125 - 126 - Ok(()) 127 - } 128 - 129 - async fn dry_run_copy( 130 - local_db: &db::Instance, 131 - dest_dir: &String, 132 - diff: Vec<String>, 133 - filters: Option<&Vec<crate::filter::ScriptRuntime>>, 134 - ) -> Result<()> { 135 - let tracks = sync::filter_tracks( 136 - local_db 137 - .tracks_by_id(diff) 138 - .await 139 - .with_context(|| "Cannot get tracks from local database")?, 140 - filters, 141 - false, 142 - )?; 143 - 144 - for track in tracks { 145 - let track_storage_path = track.storage_path(&dest_dir); 146 - 147 - log::info!("Will copy {} to {}", track.file_path, track_storage_path); 148 - } 115 + sync::run_copy(dest, diff, threads).await?; 149 116 150 117 Ok(()) 151 118 } 152 - 153 - async fn dry_run_delete( 154 - dest_db: &db::Instance, 155 - dest_dir: &String, 156 - diff: Vec<String>, 157 - filters: Option<&Vec<crate::filter::ScriptRuntime>>, 158 - ) -> Result<()> { 159 - let tracks = sync::filter_tracks( 160 - dest_db 161 - .tracks_by_id(diff) 162 - .await 163 - .with_context(|| "Cannot get tracks from destination database")?, 164 - filters, 165 - true, 166 - )?; 167 - 168 - for track in tracks { 169 - let track_storage_path = track.storage_path(&dest_dir); 170 - 171 - log::info!("Will delete {}", track_storage_path) 172 - } 173 - 174 - Ok(()) 175 - }
+29 -1
src/db/lib.rs
··· 1 - use std::{collections::hash_set, path::PathBuf}; 1 + use std::{ 2 + collections::{hash_map, hash_set}, 3 + path::PathBuf, 4 + }; 2 5 3 6 use sqlx::{Error, SqlitePool}; 7 + 8 + use crate::model; 4 9 5 10 use super::instance::Instance; 6 11 ··· 54 59 let d: hash_set::HashSet<&String> = source_ids.difference(&dest_ids).collect(); 55 60 56 61 Ok(d.into_iter().map(|e| e.clone()).collect()) 62 + } 63 + 64 + pub async fn diff_vec( 65 + source: Vec<model::Track>, 66 + destination: Vec<model::Track>, 67 + ) -> Result<Vec<model::Track>, Error> { 68 + let source: hash_map::HashMap<String, model::Track> = source 69 + .iter() 70 + .map(|t| (t.track_id.clone(), t.clone())) 71 + .collect(); 72 + let source_ids: hash_set::HashSet<String> = source.clone().into_iter().map(|t| t.0).collect(); 73 + 74 + let destination: hash_map::HashMap<String, model::Track> = destination 75 + .iter() 76 + .map(|t| (t.track_id.clone(), t.clone())) 77 + .collect(); 78 + let dest_ids: hash_set::HashSet<String> = destination.into_iter().map(|t| t.0).collect(); 79 + 80 + let d: hash_set::HashSet<&String> = source_ids.difference(&dest_ids).collect(); 81 + 82 + Ok(d.into_iter() 83 + .map(|e| source.get(e).unwrap().clone()) 84 + .collect()) 57 85 } 58 86 59 87 pub fn default_database_dir() -> PathBuf {
+1
src/db/mod.rs
··· 4 4 pub use instance::Instance; 5 5 pub use lib::default_database_dir; 6 6 pub use lib::diff; 7 + pub use lib::diff_vec;
+6 -9
src/device/disk.rs
··· 1 1 use crate::{cmd::error, db, device::Device, ffmpeg}; 2 2 use anyhow::{Context, Result, anyhow}; 3 + use async_trait::async_trait; 3 4 use fs_extra::file::{CopyOptions, copy_with_progress}; 4 5 use std::{path::PathBuf, str::FromStr}; 5 6 ··· 9 10 } 10 11 11 12 impl Disk { 12 - pub async fn new(path: PathBuf) -> Result<Self> { 13 + pub async fn new(path: PathBuf) -> Result<Box<dyn Device>> { 13 14 let db = db::Instance::new(&path.to_string_lossy(), true) 14 15 .await 15 16 .with_context(|| "Cannot open database instance")?; 16 17 17 - Ok(Self { 18 + Ok(Box::new(Self { 18 19 root_path: path, 19 20 db_handle: db, 20 - }) 21 + })) 21 22 } 22 23 } 23 24 25 + #[async_trait] 24 26 impl Device for Disk { 25 - fn typ() -> super::DeviceType { 26 - super::DeviceType::Disk 27 - } 28 - 29 27 async fn set_transcoding_settings( 30 28 &self, 31 29 settings: Option<crate::ffmpeg::Quality>, ··· 147 145 } 148 146 149 147 async fn delete(&self, track: crate::model::Track) -> Result<()> { 150 - let track_storage_path = 151 - track.storage_path(&track.storage_path(self.root_path.to_string_lossy().as_ref())); 148 + let track_storage_path = track.storage_path(self.root_path.to_string_lossy().as_ref()); 152 149 153 150 self.db_handle.delete(track.id).await?; 154 151 std::fs::remove_file(track_storage_path.clone())
+26 -10
src/device/ipod.rs
··· 1 1 use crate::{cmd::error, db, device::Device, ffmpeg, gpod}; 2 - use anyhow::{Context, anyhow}; 2 + use anyhow::{Context, Result, anyhow}; 3 + use async_trait::async_trait; 3 4 use fs_extra::file::{CopyOptions, copy_with_progress}; 4 5 use std::path::PathBuf; 5 6 ··· 8 9 mountpoint: PathBuf, 9 10 } 10 11 11 - impl Device for IPod { 12 - fn typ() -> super::DeviceType { 13 - super::DeviceType::IPod 12 + impl IPod { 13 + pub async fn new(path: PathBuf) -> Result<Box<dyn Device>> { 14 + let db = db::Instance::new(&path.to_string_lossy(), true) 15 + .await 16 + .with_context(|| "Cannot open database instance")?; 17 + 18 + Ok(Box::new(Self { 19 + db_handle: db, 20 + mountpoint: path, 21 + })) 14 22 } 23 + } 15 24 25 + #[async_trait] 26 + impl Device for IPod { 16 27 async fn set_transcoding_settings( 17 28 &self, 18 29 settings: Option<crate::ffmpeg::Quality>, ··· 48 59 } 49 60 50 61 async fn add(&self, track: crate::model::Track) -> anyhow::Result<()> { 51 - let ipm = gpod::Manager::new(self.mountpoint.to_string_lossy().to_string())?; 52 - 53 62 let ts = match self.db_handle.get_transcoding_settings().await? { 54 63 Some(s) => s, 55 64 None => ffmpeg::Quality { ··· 66 75 } 67 76 false => (), 68 77 } 69 - let dest_path = ipm.path_for_file(fp.as_path()).unwrap(); 78 + 79 + let dest_path = { 80 + // I'm ashamed lmao 81 + let ipm = gpod::Manager::new(self.mountpoint.to_string_lossy().to_string())?; 82 + ipm.path_for_file(fp.as_path()).unwrap() 83 + }; 70 84 71 85 let is_lossless = track.is_lossless(); 72 86 ··· 132 146 }) 133 147 .await?; 134 148 149 + let ipm = gpod::Manager::new(self.mountpoint.to_string_lossy().to_string())?; 150 + 135 151 // now finalize the copy 136 152 ipm.finalize_track( 137 153 &track.clone(), ··· 149 165 Ok(()) 150 166 } 151 167 152 - async fn delete(&self, _: crate::model::Track) -> anyhow::Result<()> { 153 - // TODO: implement deletion 154 - Ok(()) 168 + async fn delete(&self, track: crate::model::Track) -> anyhow::Result<()> { 169 + let ipm = gpod::Manager::new(self.mountpoint.to_string_lossy().to_string())?; 170 + ipm.remove(track) 155 171 } 156 172 }
+2 -1
src/device/lib.rs
··· 1 1 use crate::{ffmpeg, model}; 2 2 use anyhow::Result; 3 + use async_trait::async_trait; 3 4 4 5 pub enum DeviceType { 5 6 IPod, 6 7 Disk, 7 8 } 8 9 10 + #[async_trait] 9 11 pub trait Device { 10 - fn typ() -> DeviceType; 11 12 async fn set_transcoding_settings(&self, settings: Option<ffmpeg::Quality>) -> Result<()>; 12 13 async fn list_by_state(&self, state: model::FileState) -> Result<Vec<model::Track>>; 13 14 async fn add(&self, track: model::Track) -> Result<()>;
+1
src/device/mod.rs
··· 3 3 mod lib; 4 4 5 5 pub use crate::device::lib::{Device, DeviceType}; 6 + pub use crate::device::{disk::Disk, ipod::IPod};
+27 -1
src/gpod/gpod.rs
··· 113 113 (*ipod_track).artist = c_strdup(&track.artist); 114 114 (*ipod_track).filetype = c_strdup(&format!("{}-file", codec.to_uppercase())); 115 115 (*ipod_track).genre = c_strdup(&track.genre); 116 - (*ipod_track).comment = c_strdup(""); 116 + (*ipod_track).comment = c_strdup(&track.track_id); 117 117 (*ipod_track).tracklen = track.track_len as i32; 118 118 (*ipod_track).track_nr = track.number.try_into().unwrap_or(0); 119 119 (*ipod_track).samplerate = sample_rate; ··· 155 155 error.code, 156 156 err_msg.to_string_lossy() 157 157 )); 158 + } 159 + 160 + Ok(()) 161 + } 162 + } 163 + 164 + pub fn remove(&self, track: model::Track) -> Result<()> { 165 + unsafe { 166 + let mpl = itdb_playlist_mpl(self.db_ptr); 167 + let mut ti = (*mpl).members as *mut GList; 168 + 169 + loop { 170 + if ti.is_null() { 171 + break; 172 + } 173 + let it_track = (*ti).data as *mut Itdb_Track; 174 + ti = (*ti).next; 175 + 176 + if string((*it_track).comment) == track.track_id { 177 + itdb_playlist_remove_track(mpl, it_track); 178 + itdb_track_remove(it_track); 179 + break; 180 + } 181 + 182 + // free 183 + itdb_track_free(it_track); 158 184 } 159 185 160 186 Ok(())
+25 -25
src/gtkgui/gtkgui.rs
··· 614 614 track.album.clone(), 615 615 ); 616 616 617 - let _ = sync::delete(track, &dest_db, &dest_dir).await; 617 + // let _ = sync::delete(track, &dest_db, &dest_dir).await; 618 618 current += 1; 619 619 // Yield 620 620 glib::timeout_future(std::time::Duration::from_millis(1)).await; ··· 666 666 track.storage_path(&dest_dir).into() 667 667 }; 668 668 669 - match sync::copy( 670 - &track, 671 - &dest_db, 672 - dest_path.clone(), 673 - tr_settings.clone(), 674 - ) 675 - .await 676 - { 677 - Ok((copied_track, tr_res)) => { 678 - if let Some(ref mut ipm) = ipod_manager { 679 - let _ = ipm.finalize_track( 680 - &copied_track, 681 - dest_path, 682 - copied_track.extension.clone(), 683 - 44100, 684 - 256, 685 - tr_res 686 - .and_then(|r| r.replaygain) 687 - .map(|r| r.soundcheck_value()), 688 - ); 689 - } 690 - } 691 - Err(e) => eprintln!("Error copying track: {}", e), 692 - } 669 + // match sync::copy( 670 + // &track, 671 + // &dest_db, 672 + // dest_path.clone(), 673 + // tr_settings.clone(), 674 + // ) 675 + // .await 676 + // { 677 + // Ok((copied_track, tr_res)) => { 678 + // if let Some(ref mut ipm) = ipod_manager { 679 + // let _ = ipm.finalize_track( 680 + // &copied_track, 681 + // dest_path, 682 + // copied_track.extension.clone(), 683 + // 44100, 684 + // 256, 685 + // tr_res 686 + // .and_then(|r| r.replaygain) 687 + // .map(|r| r.soundcheck_value()), 688 + // ); 689 + // } 690 + // } 691 + // Err(e) => eprintln!("Error copying track: {}", e), 692 + // } 693 693 current += 1; 694 694 // Yield 695 695 glib::timeout_future(std::time::Duration::from_millis(1)).await;
+32 -226
src/sync.rs
··· 1 - use crate::cmd::error; 2 1 use crate::db; 3 - use crate::ffmpeg; 4 - use crate::ffmpeg::TranscodeResult; 5 - use crate::gpod; 2 + use crate::device; 6 3 use crate::model; 7 - use anyhow::{Context, Ok, Result}; 8 - use fs_extra::file::{CopyOptions, copy_with_progress}; 4 + use anyhow::{Ok, Result}; 5 + use async_std::sync::Mutex; 9 6 use futures::{StreamExt, TryStreamExt, stream}; 7 + use std::collections::hash_map; 10 8 use std::collections::hash_set; 11 - use std::path::PathBuf; 9 + use std::sync::Arc; 12 10 13 11 pub async fn diff_databases( 14 12 source: &db::Instance, ··· 43 41 Ok(d.into_iter().map(|e| e.clone()).collect()) 44 42 } 45 43 46 - pub async fn filter_tracks_by_id( 47 - filters: Option<&Vec<crate::filter::ScriptRuntime>>, 48 - db: &db::Instance, 49 - ids: Vec<String>, 50 - ) -> Result<Vec<String>> { 51 - let tracks = db.tracks_by_id(ids).await?; 44 + pub async fn diff_vec( 45 + source: Vec<model::Track>, 46 + destination: Vec<model::Track>, 47 + ) -> Result<Vec<model::Track>> { 48 + let source: hash_map::HashMap<String, model::Track> = source 49 + .iter() 50 + .map(|t| (t.track_id.clone(), t.clone())) 51 + .collect(); 52 + let source_ids: hash_set::HashSet<String> = source.clone().into_iter().map(|t| t.0).collect(); 53 + 54 + let destination: hash_map::HashMap<String, model::Track> = destination 55 + .iter() 56 + .map(|t| (t.track_id.clone(), t.clone())) 57 + .collect(); 58 + let dest_ids: hash_set::HashSet<String> = destination.into_iter().map(|t| t.0).collect(); 52 59 53 - let tracks = filter_tracks(tracks, filters, false)?; 60 + let d: hash_set::HashSet<&String> = source_ids.difference(&dest_ids).collect(); 54 61 55 - Ok(tracks.into_iter().map(|t| t.track_id).collect()) 62 + Ok(d.into_iter() 63 + .map(|e| source.get(e).unwrap().clone()) 64 + .collect()) 56 65 } 57 66 58 67 pub fn filter_tracks( ··· 79 88 .filter_map(|elem| { 80 89 let (idx, track) = elem; 81 90 82 - if filter_res[idx] && !delete { 91 + if filter_res[idx] && delete { 83 92 return None; 84 93 } 85 94 ··· 92 101 } 93 102 94 103 pub async fn run_copy( 95 - local_db: &db::Instance, 96 - dest_db: &db::Instance, 97 - dest_dir: &String, 98 - diff: Vec<String>, 99 - filters: Option<&Vec<crate::filter::ScriptRuntime>>, 104 + dev: Box<dyn device::Device>, 105 + tracks: Vec<model::Track>, 100 106 threads: usize, 101 - ipod: bool, 102 107 ) -> Result<()> { 103 - let tr_settings = dest_db.get_transcoding_settings().await?; 104 - 105 - let tracks = filter_tracks( 106 - local_db 107 - .tracks_by_id(diff) 108 - .await 109 - .with_context(|| "Cannot get tracks from local database")?, 110 - filters, 111 - false, 112 - )?; 113 - 114 - if ipod { 115 - return copy_ipod(dest_db, dest_dir, threads, tracks).await; 116 - } 108 + let dev = Arc::new(Mutex::new(dev)); 117 109 118 110 stream::iter(tracks) 119 111 .map(|track| { 120 - let trs = tr_settings.clone(); 121 - let dest_path: PathBuf = track.storage_path(dest_dir).into(); 122 - async move { copy(&track, dest_db, dest_path, trs).await } 123 - }) 124 - .buffer_unordered(threads) 125 - .try_for_each(|_| async move { Ok(()) }) 126 - .await 127 - } 128 - 129 - pub async fn copy_ipod( 130 - dest_db: &db::Instance, 131 - dest_dir: &String, 132 - threads: usize, 133 - tracks: Vec<model::Track>, 134 - ) -> Result<()> { 135 - let ipod_manager = gpod::Manager::new(dest_dir.clone())?; 136 - 137 - let transcoding = Some(ffmpeg::Quality { 138 - bitrate: ffmpeg::Bitrate::VBR("5".to_owned()), 139 - sampling_rate: Some(ffmpeg::SamplingRate::Rate441K), 140 - codec: ffmpeg::Codec::AAC, 141 - }); 142 - 143 - let converted: Vec<_> = stream::iter(tracks) 144 - .map(|track| { 145 - let trs = transcoding.clone(); 146 - let ipm = ipod_manager.clone(); 147 - 112 + let dev = dev.clone(); 148 113 async move { 149 - let mut fp: PathBuf = track.file_path.clone().into(); 150 - match track.is_lossless() { 151 - true => { 152 - fp.set_extension(ffmpeg::Codec::AAC.extension()); 153 - } 154 - false => (), 155 - } 156 - let dest_path = ipm.path_for_file(fp.as_path()).unwrap(); 157 - let (copied_track, tr) = copy(&track, dest_db, dest_path.clone(), trs).await?; 158 - 159 - Ok((copied_track, dest_path, tr.unwrap())) 114 + let asd = dev.lock().await.add(track).await; 115 + asd 160 116 } 161 117 }) 162 118 .buffer_unordered(threads) 163 - .try_collect() 164 - .await?; 165 - 166 - log::info!("finalizing..."); 167 - for (track, path, trans_res) in converted { 168 - ipod_manager.finalize_track( 169 - &track.clone(), 170 - path.clone(), 171 - track.extension, 172 - 44100, 173 - 256, 174 - trans_res.replaygain.map(|r| r.soundcheck_value()), 175 - )?; 176 - } 177 - 178 - return Ok(()); 179 - } 180 - 181 - pub async fn run_delete( 182 - dest_db: &db::Instance, 183 - dest_dir: &String, 184 - diff: Vec<String>, 185 - filters: Option<&Vec<crate::filter::ScriptRuntime>>, 186 - ) -> Result<()> { 187 - let diff_len = diff.len(); 188 - 189 - if diff_len == 0 { 190 - return Ok(()); 191 - } 192 - 193 - let tracks = filter_tracks( 194 - dest_db 195 - .tracks_by_id(diff) 196 - .await 197 - .with_context(|| "Cannot get tracks from destination database")?, 198 - filters, 199 - true, 200 - )?; 201 - 202 - for track in tracks { 203 - log::info!("deleting {}", track); 204 - delete(track, &dest_db, &dest_dir).await?; 205 - } 206 - 207 - Ok(()) 208 - } 209 - 210 - pub async fn delete(track: model::Track, dest_db: &db::Instance, dest_dir: &String) -> Result<()> { 211 - let track_storage_path = track.storage_path(&dest_dir); 212 - 213 - dest_db.delete(track.id).await?; 214 - std::fs::remove_file(track_storage_path.clone()) 215 - .with_context(|| format!("Cannot delete file {}", track_storage_path.clone()))?; 216 - Ok(()) 217 - } 218 - 219 - pub async fn copy( 220 - track: &model::Track, 221 - dest_db: &db::Instance, 222 - dest_fname: PathBuf, 223 - transcoding_settings: Option<ffmpeg::Quality>, 224 - ) -> Result<(model::Track, Option<TranscodeResult>)> { 225 - // We replicate src database on dest 1:1 226 - // We should create the same logical database (tracks with the same tags have the same hash) 227 - // ignoring the format. 228 - // When the copy is done we should return the newly-created track. 229 - 230 - let is_lossless = track.is_lossless(); 231 - 232 - let sp = std::path::Path::new(&dest_fname); 233 - 234 - let parent = sp 235 - .parent() 236 - .with_context(|| "Cannot obtain base destination directory")?; 237 - 238 - let mut dest_track = track.clone(); 239 - dest_track.file_state = crate::model::FileState::Copying; 240 - dest_db 241 - .insert_track(&dest_track) 119 + .try_for_each(|_| async move { Ok(()) }) 242 120 .await 243 - .with_context(|| "Cannot insert in-progress copying track in destination database")?; 244 - 245 - std::fs::create_dir_all(parent).with_context(|| { 246 - format!( 247 - "Cannot create destination directory tree {}", 248 - parent.to_str().unwrap() 249 - ) 250 - })?; 251 - 252 - let track_dest_path = if let Some(ref ts) = transcoding_settings 253 - && is_lossless 254 - { 255 - let mut dest_fname = dest_fname.clone(); 256 - dest_fname.set_extension(ts.codec.extension()); 257 - dest_fname 258 - } else { 259 - dest_fname 260 - }; 261 - 262 - let opts = CopyOptions::new().overwrite(true); 263 - 264 - let maybe_transcode_result = async_std::task::spawn_blocking({ 265 - let track_for_thread = track.clone(); 266 - let ts_for_thread = transcoding_settings.clone(); 267 - 268 - move || { 269 - if let Some(ts) = ts_for_thread 270 - && is_lossless 271 - { 272 - log::info!("transcoding & copying {} ({})", track_for_thread, ts); 273 - log::debug!("transcoding path: {}", track_dest_path.display()); 274 - 275 - return Ok(Some(ffmpeg::convert( 276 - &track_for_thread, 277 - track_dest_path, 278 - &ts, 279 - )?)); 280 - } 281 - 282 - log::info!("copying {}", track_for_thread); 283 - log::debug!("YOOO {}", track_dest_path.display()); 284 - 285 - match copy_with_progress( 286 - track_for_thread.file_path.clone(), 287 - track_dest_path.clone(), 288 - &opts, 289 - |_| {}, 290 - ) { 291 - std::result::Result::Ok(_) => {} 292 - Err(err) => { 293 - return Err(error::Error::CopyError(err)).with_context(|| { 294 - format!( 295 - "Cannot copy {} to {}", 296 - track_for_thread.file_path, 297 - track_dest_path.display() 298 - ) 299 - }); 300 - } 301 - }; 302 - 303 - Ok(None) 304 - } 305 - }) 306 - .await?; 307 - 308 - dest_track.file_state = crate::model::FileState::Copied; 309 - dest_db 310 - .insert_track(&dest_track) 311 - .await 312 - .with_context(|| "Cannot insert copy finished track in destination database")?; 313 - 314 - Ok((track.clone(), maybe_transcode_result)) 315 121 }