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.

*: support albumart, slight refactor

Gee Sawra 906a3222 aa70995c

+197 -230
+12
.sqlx/query-5052a7d020ddc527b28aa9def3c13610b870d16a85484d3ca49fa61de30ff979.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 artwork_path\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 ?14\n );\n ", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Right": 14 8 + }, 9 + "nullable": [] 10 + }, 11 + "hash": "5052a7d020ddc527b28aa9def3c13610b870d16a85484d3ca49fa61de30ff979" 12 + }
+7 -1
.sqlx/query-86245ea0c3119d94e08fe0404482b4f8de3241bc067136c0181ff4e0aa2bcc67.json
··· 72 72 "name": "extension", 73 73 "ordinal": 13, 74 74 "type_info": "Text" 75 + }, 76 + { 77 + "name": "artwork_path", 78 + "ordinal": 14, 79 + "type_info": "Text" 75 80 } 76 81 ], 77 82 "parameters": { ··· 91 96 false, 92 97 false, 93 98 false, 94 - false 99 + false, 100 + true 95 101 ] 96 102 }, 97 103 "hash": "86245ea0c3119d94e08fe0404482b4f8de3241bc067136c0181ff4e0aa2bcc67"
+7 -1
.sqlx/query-90d659e4ebb83559ac94ba4618a065c5d9966e99d5c318c5b8f9dcaa670ae5b2.json
··· 72 72 "name": "extension", 73 73 "ordinal": 13, 74 74 "type_info": "Text" 75 + }, 76 + { 77 + "name": "artwork_path", 78 + "ordinal": 14, 79 + "type_info": "Text" 75 80 } 76 81 ], 77 82 "parameters": { ··· 91 96 false, 92 97 false, 93 98 false, 94 - false 99 + false, 100 + true 95 101 ] 96 102 }, 97 103 "hash": "90d659e4ebb83559ac94ba4618a065c5d9966e99d5c318c5b8f9dcaa670ae5b2"
-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 - }
+7 -1
.sqlx/query-f1349b72ca08058d2503c7348ee66ef638dfc1c3ce09cfd2167c01237ed25840.json
··· 72 72 "name": "extension", 73 73 "ordinal": 13, 74 74 "type_info": "Text" 75 + }, 76 + { 77 + "name": "artwork_path", 78 + "ordinal": 14, 79 + "type_info": "Text" 75 80 } 76 81 ], 77 82 "parameters": { ··· 91 96 false, 92 97 false, 93 98 false, 94 - false 99 + false, 100 + true 95 101 ] 96 102 }, 97 103 "hash": "f1349b72ca08058d2503c7348ee66ef638dfc1c3ce09cfd2167c01237ed25840"
+27 -1
src/cmd/add.rs
··· 171 171 .read_from_path(p.clone()) 172 172 .with_context(|| format!("Cannot read tags from {}", p.clone()))?; 173 173 174 - let mut track: model::Track = model::RawTrack { tags, path: p }.into(); 174 + let p_path = std::path::PathBuf::from(p.clone()); 175 + let p_path = p_path.parent().unwrap(); 176 + 177 + let mut track: model::Track = model::RawTrack { 178 + tags, 179 + path: p, 180 + artwork_path: find_cover_image(&p_path), 181 + } 182 + .into(); 175 183 track.file_state = FileState::Copied; 176 184 177 185 log::info!("new track: {}", track); ··· 187 195 188 196 Ok((new_tracks, duplicate)) 189 197 } 198 + 199 + pub fn find_cover_image(base_dir: &std::path::Path) -> Option<String> { 200 + const SEARCH_NAMES: &[&str] = &["Cover", "cover", "folder"]; 201 + const SEARCH_EXTENSIONS: &[&str] = &["png", "jpeg", "jpg"]; 202 + 203 + for &name in SEARCH_NAMES { 204 + for &ext in SEARCH_EXTENSIONS { 205 + let file_name = format!("{}.{}", name, ext); 206 + let full_path = base_dir.join(file_name); 207 + 208 + if full_path.is_file() { 209 + return Some(full_path.to_string_lossy().to_string()); 210 + } 211 + } 212 + } 213 + 214 + None 215 + }
+2 -2
src/cmd/ipod_init.rs
··· 1 1 use anyhow::Result; 2 2 use clap::Args as ClapArgs; 3 3 4 - use crate::gpod::gpod::IPodHandle; 4 + use crate::gpod; 5 5 6 6 #[derive(ClapArgs, Debug)] 7 7 pub struct Args { ··· 15 15 } 16 16 17 17 pub async fn run(args: Args) -> Result<()> { 18 - IPodHandle::initialize(args.path, args.model) 18 + gpod::initialize(args.path, args.model) 19 19 }
+3 -4
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 + use crate::gpod; 5 5 use crate::model; 6 6 use anyhow::Ok; 7 7 use anyhow::anyhow; ··· 247 247 let td = tempdir::TempDir::new("tunz_ipod_copy")?; 248 248 let td = td.path().to_string_lossy().to_string(); 249 249 250 - let ih = IPodHandle::new(dest_dir.clone())?; 251 - let ch = ih.copy_process(); 250 + let ipod_manager = gpod::Manager::new(dest_dir.clone())?; 252 251 253 252 let transcoding = Some(ffmpeg::Quality { 254 253 bitrate: ffmpeg::Bitrate::CBR("256k".to_owned()), ··· 263 262 264 263 log::info!("copying to iPod..."); 265 264 266 - ch.copy_file( 265 + ipod_manager.copy( 267 266 &track, 268 267 tr_track_path, 269 268 ffmpeg::Codec::AAC.extension(),
+8 -2
src/db/instance.rs
··· 232 232 disc_number, 233 233 disc_total, 234 234 file_state, 235 - extension 235 + extension, 236 + artwork_path 236 237 ) VALUES ( 237 238 ?1, 238 239 ?2, ··· 246 247 ?10, 247 248 ?11, 248 249 ?12, 249 - ?13 250 + ?13, 251 + ?14 250 252 ); 251 253 "#, 252 254 track.track_id, ··· 262 264 track.disc_total, 263 265 track.file_state, 264 266 track.extension, 267 + track.artwork_path 265 268 ) 266 269 .execute(&mut *conn) 267 270 .await?; ··· 322 325 genre: r.get("genre"), 323 326 track_len: track_len * 1000, 324 327 year: year as usize, 328 + artwork_path: r.get("artwork_path"), 325 329 } 326 330 }) 327 331 .collect()) ··· 357 361 genre: r.genre, 358 362 track_len: r.track_len, 359 363 year: r.year as usize, 364 + artwork_path: r.artwork_path, 360 365 }) 361 366 .collect::<Vec<model::Track>>()) 362 367 } ··· 541 546 genre: track.genre, 542 547 track_len: track.track_len, 543 548 year: track.year as usize, 549 + artwork_path: track.artwork_path, 544 550 })) 545 551 .await 546 552 .unwrap(),
+2 -1
src/db/migrations/0_base.sql
··· 18 18 disc_total INTEGER NOT NULL, 19 19 file_state INTEGER NOT NULL, 20 20 file_path TEXT NOT NULL, 21 - extension TEXT NOT NULL 21 + extension TEXT NOT NULL, 22 + artwork_path TEXT 22 23 ); 23 24 24 25 CREATE VIEW IF NOT EXISTS albums (
+116 -204
src/gpod/gpod.rs
··· 6 6 use anyhow::{Result, anyhow}; 7 7 use glib::gobject_ffi::g_type_init; 8 8 9 - pub struct IPodHandle { 10 - db_ptr: *mut Itdb_iTunesDB, 11 - device_ptr: *mut Itdb_Device, 12 - } 13 - 14 - // impl Drop for IPodHandle { 15 - // fn drop(&mut self) { 16 - // unsafe { 17 - // itdb_free(self.db_ptr); 18 - // itdb_device_free(self.device_ptr); 19 - // } 20 - // } 21 - // } 22 - 23 - pub struct CopyProcess { 9 + pub struct Manager { 24 10 db_ptr: *mut Itdb_iTunesDB, 25 11 } 26 12 27 - impl Drop for CopyProcess { 13 + impl Drop for Manager { 28 14 fn drop(&mut self) { 29 15 unsafe { 30 16 let mut error: *mut GError = ptr::null_mut(); ··· 49 35 } 50 36 } 51 37 52 - impl CopyProcess { 53 - pub fn copy_file( 38 + impl Manager { 39 + pub fn new(mountpoint: String) -> Result<Self> { 40 + unsafe { 41 + g_type_init(); 42 + let mountpoint = std::ffi::CString::new(mountpoint.clone()).unwrap(); 43 + 44 + let mut error: *mut GError = ptr::null_mut(); 45 + let db_ptr = itdb_parse(mountpoint.as_ptr(), &mut error); 46 + if !error.is_null() { 47 + let error = *error; 48 + let err_msg = std::ffi::CString::from_raw(error.message); 49 + return Err(anyhow!( 50 + "read ipod database, code {}: {}", 51 + error.code, 52 + err_msg.to_string_lossy() 53 + )); 54 + } 55 + 56 + match db_ptr.is_null() { 57 + true => Err(anyhow!("no database found")), 58 + false => { 59 + itdb_start_sync(db_ptr); 60 + 61 + Ok(Self { db_ptr }) 62 + } 63 + } 64 + } 65 + } 66 + 67 + pub fn copy( 54 68 &self, 55 69 track: &Track, 56 70 file: PathBuf, ··· 76 90 77 91 let mut error: *mut GError = ptr::null_mut(); 78 92 let path = CString::new(file.display().to_string()).unwrap(); 93 + if let Some(art) = track.artwork_path.clone() { 94 + log::info!("adding album art: {}", art); 95 + let art = CString::new(art).unwrap(); 96 + if itdb_track_set_thumbnails(ipod_track, art.as_ptr()) != 1 { 97 + log::warn!("could not add album art to track") 98 + } 99 + } 79 100 itdb_track_add(self.db_ptr, ipod_track, -1); 80 101 itdb_playlist_add_track(itdb_playlist_mpl(self.db_ptr), ipod_track, -1); 81 102 let copy_success = itdb_cp_track_to_ipod(ipod_track, path.as_ptr(), &mut error); ··· 100 121 } 101 122 } 102 123 103 - impl IPodHandle { 104 - pub fn initialize(path: String, model: String) -> Result<()> { 105 - unsafe { 106 - g_type_init(); 107 - let mountpoint = std::ffi::CString::new(path).unwrap(); 108 - let model = std::ffi::CString::new(model).unwrap(); 124 + unsafe fn c_strdup(s: &str) -> *mut std::ffi::c_char { 125 + unsafe { 126 + let c_str = std::ffi::CString::new(s).unwrap(); 127 + // libc::strdup allocates memory (malloc) that libgpod can safely read and eventually free. 128 + libc::strdup(c_str.as_ptr()) 129 + } 130 + } 109 131 110 - let mut error: *mut GError = ptr::null_mut(); 111 - if itdb_init_ipod(mountpoint.as_ptr(), model.as_ptr(), ptr::null(), &mut error) != 1 { 112 - if error.is_null() { 113 - return Err(anyhow!("could not initialize ipod, unknown error")); 114 - } 132 + pub fn initialize(path: String, model: String) -> Result<()> { 133 + unsafe { 134 + g_type_init(); 135 + let mountpoint = std::ffi::CString::new(path).unwrap(); 136 + let model = std::ffi::CString::new(model).unwrap(); 115 137 116 - let error = *error; 117 - let err_msg = std::ffi::CString::from_raw(error.message); 118 - return Err(anyhow!( 119 - "initialization error, code {}: {}", 120 - error.code, 121 - err_msg.to_string_lossy() 122 - )); 138 + let mut error: *mut GError = ptr::null_mut(); 139 + if itdb_init_ipod(mountpoint.as_ptr(), model.as_ptr(), ptr::null(), &mut error) != 1 { 140 + if error.is_null() { 141 + return Err(anyhow!("could not initialize ipod, unknown error")); 123 142 } 124 143 125 - let serials = rusb::devices()? 126 - .iter() 127 - .filter(|d| { 128 - let dd = d.device_descriptor(); 129 - let d = d.open(); 130 - if d.is_err() { 131 - return false; 132 - } 133 - let d = d.unwrap(); 134 - 135 - if let Ok(dd) = dd { 136 - match dd.serial_number_string_index() { 137 - Some(_) => (), 138 - None => return false, 139 - }; 140 - 141 - let product_str = d.read_product_string_ascii(&dd).unwrap(); 142 - let serial_str = d.read_serial_number_string_ascii(&dd).unwrap(); 143 - 144 - return dd.vendor_id() == 0x05ac 145 - && product_str == "iPod" 146 - && !serial_str.is_empty(); 147 - } else { 148 - return false; 149 - } 150 - }) 151 - .map(|d| { 152 - let d = d.open().unwrap(); 153 - let dd = d.device().device_descriptor().unwrap(); 154 - 155 - d.read_serial_number_string_ascii(&dd).unwrap() 156 - }) 157 - .collect::<Vec<String>>(); 158 - 159 - match serials.len() { 160 - 0 => return Err(anyhow!("no iPod found")), 161 - 1 => { 162 - let device_ptr = itdb_device_new(); 163 - itdb_device_set_mountpoint(device_ptr, mountpoint.as_ptr()); 164 - let fw_guid_field = CString::new("FirewireGuid").unwrap(); 165 - let serial = 166 - CString::new(format!("0x{}", &serials.first().unwrap()[..16])).unwrap(); 167 - 168 - // FirewireGuid 169 - itdb_device_set_sysinfo(device_ptr, fw_guid_field.as_ptr(), serial.as_ptr()); 170 - let mut error: *mut GError = ptr::null_mut(); 171 - if itdb_device_write_sysinfo(device_ptr, &mut error) != 1 { 172 - if error.is_null() { 173 - return Err(anyhow!( 174 - "could not initialize write sysinfo, unknown error" 175 - )); 176 - } 144 + let error = *error; 145 + let err_msg = std::ffi::CString::from_raw(error.message); 146 + return Err(anyhow!( 147 + "initialization error, code {}: {}", 148 + error.code, 149 + err_msg.to_string_lossy() 150 + )); 151 + } 177 152 178 - let error = *error; 179 - let err_msg = std::ffi::CString::from_raw(error.message); 180 - return Err(anyhow!( 181 - "write sysinfo error, code {}: {}", 182 - error.code, 183 - err_msg.to_string_lossy() 184 - )); 185 - } 153 + let serials = rusb::devices()? 154 + .iter() 155 + .filter(|d| { 156 + let dd = d.device_descriptor(); 157 + let d = d.open(); 158 + if d.is_err() { 159 + return false; 186 160 } 187 - _ => return Err(anyhow!("more than 1 iPod found, please connect just one")), 188 - } 189 - } 161 + let d = d.unwrap(); 190 162 191 - Ok(()) 192 - } 193 - pub fn new(path: String) -> Result<Self> { 194 - unsafe { 195 - g_type_init(); 196 - let mountpoint = std::ffi::CString::new(path).unwrap(); 163 + if let Ok(dd) = dd { 164 + match dd.serial_number_string_index() { 165 + Some(_) => (), 166 + None => return false, 167 + }; 197 168 198 - let device_ptr = itdb_device_new(); 199 - itdb_device_set_mountpoint(device_ptr, mountpoint.as_ptr()); 200 - if itdb_device_read_sysinfo(device_ptr) != 1 { 201 - return Err(anyhow!("could not read sysinfo from iPod")); 202 - }; 169 + let product_str = d.read_product_string_ascii(&dd).unwrap(); 170 + let serial_str = d.read_serial_number_string_ascii(&dd).unwrap(); 203 171 204 - let mut error: *mut GError = ptr::null_mut(); 205 - let db_ptr = itdb_parse(mountpoint.as_ptr(), &mut error); 206 - if !error.is_null() { 207 - let error = *error; 208 - let err_msg = std::ffi::CString::from_raw(error.message); 209 - return Err(anyhow!( 210 - "read ipod database, code {}: {}", 211 - error.code, 212 - err_msg.to_string_lossy() 213 - )); 214 - } 172 + return dd.vendor_id() == 0x05ac 173 + && product_str == "iPod" 174 + && !serial_str.is_empty(); 175 + } else { 176 + return false; 177 + } 178 + }) 179 + .map(|d| { 180 + let d = d.open().unwrap(); 181 + let dd = d.device().device_descriptor().unwrap(); 215 182 216 - match db_ptr.is_null() { 217 - true => Err(anyhow!("no database found")), 218 - false => Ok(Self { db_ptr, device_ptr }), 219 - } 220 - } 221 - } 222 - pub fn info(&self) { 223 - unsafe { 224 - let pi = itdb_device_get_sysinfo( 225 - self.device_ptr, 226 - CString::new("ModelNumStr").unwrap().as_ptr(), 227 - ); 183 + d.read_serial_number_string_ascii(&dd).unwrap() 184 + }) 185 + .collect::<Vec<String>>(); 228 186 229 - let asd = CString::from_raw(pi); 230 - println!("numstr: {}", asd.to_string_lossy()); 187 + match serials.len() { 188 + 0 => return Err(anyhow!("no iPod found")), 189 + 1 => { 190 + let device_ptr = itdb_device_new(); 191 + itdb_device_set_mountpoint(device_ptr, mountpoint.as_ptr()); 192 + let fw_guid_field = CString::new("FirewireGuid").unwrap(); 193 + let serial = 194 + CString::new(format!("0x{}", &serials.first().unwrap()[..16])).unwrap(); 231 195 232 - let pi = itdb_device_get_sysinfo( 233 - self.device_ptr, 234 - CString::new("FirewireGuid").unwrap().as_ptr(), 235 - ); 196 + // FirewireGuid 197 + itdb_device_set_sysinfo(device_ptr, fw_guid_field.as_ptr(), serial.as_ptr()); 198 + let mut error: *mut GError = ptr::null_mut(); 199 + if itdb_device_write_sysinfo(device_ptr, &mut error) != 1 { 200 + if error.is_null() { 201 + return Err(anyhow!("could not initialize write sysinfo, unknown error")); 202 + } 236 203 237 - if pi.is_null() { 238 - println!("could not read FirewireGuid"); 239 - return; 204 + let error = *error; 205 + let err_msg = std::ffi::CString::from_raw(error.message); 206 + return Err(anyhow!( 207 + "write sysinfo error, code {}: {}", 208 + error.code, 209 + err_msg.to_string_lossy() 210 + )); 211 + } 240 212 } 241 - 242 - let asd = CString::from_raw(pi); 243 - println!("numstr: {}", asd.to_string_lossy()); 244 - } 245 - } 246 - 247 - pub fn copy_process(&self) -> CopyProcess { 248 - unsafe { 249 - itdb_start_sync(self.db_ptr); 250 - } 251 - CopyProcess { 252 - db_ptr: self.db_ptr, 213 + _ => return Err(anyhow!("more than 1 iPod found, please connect just one")), 253 214 } 254 215 } 255 - } 256 216 257 - unsafe fn c_strdup(s: &str) -> *mut std::ffi::c_char { 258 - unsafe { 259 - let c_str = std::ffi::CString::new(s).unwrap(); 260 - // libc::strdup allocates memory (malloc) that libgpod can safely read and eventually free. 261 - libc::strdup(c_str.as_ptr()) 262 - } 263 - } 264 - 265 - #[cfg(test)] 266 - mod tests { 267 - use std::str::FromStr; 268 - 269 - use super::*; 270 - 271 - #[test] 272 - fn initialize() { 273 - IPodHandle::initialize("/run/media/geesawra/IPOD".to_owned(), "MA003FB".to_owned()) 274 - .unwrap(); 275 - let ih = IPodHandle::new("/run/media/geesawra/IPOD".to_owned()).unwrap(); 276 - ih.info(); 277 - } 278 - 279 - #[test] 280 - fn copy() { 281 - let ih = IPodHandle::new("/run/media/geesawra/IPOD".to_owned()).unwrap(); 282 - let p = ih.copy_process(); 283 - 284 - let t = Track { 285 - title: "Marinmba".to_owned(), 286 - album: "Hit It!".to_owned(), 287 - artist: "Vianova".to_owned(), 288 - number: 42, 289 - ..Default::default() 290 - }; 291 - 292 - p.copy_file( 293 - &t, 294 - PathBuf::from_str( 295 - "/home/geesawra/Code/tracksync/dest/Vianova/Hit It!/1/Marimba.m4a.m4a", 296 - ) 297 - .unwrap(), 298 - "aac".to_owned(), 299 - 44100, 300 - 128, 301 - ) 302 - .unwrap(); 303 - 304 - drop(p); 305 - } 217 + Ok(()) 306 218 }
+3 -1
src/gpod/mod.rs
··· 8 8 include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 9 9 } 10 10 11 - pub mod gpod; 11 + mod gpod; 12 + pub use gpod::Manager; 13 + pub use gpod::initialize;
+3
src/model.rs
··· 26 26 pub struct RawTrack { 27 27 pub tags: Box<dyn AudioTag + Send + Sync>, 28 28 pub path: String, 29 + pub artwork_path: Option<String>, 29 30 } 30 31 31 32 #[derive(Debug, Clone, Default, rhai::CustomType)] ··· 71 72 pub disc_total: i64, 72 73 pub file_state: FileState, 73 74 pub extension: String, 75 + pub artwork_path: Option<String>, 74 76 } 75 77 76 78 impl std::fmt::Display for Track { ··· 136 138 genre: track.tags.genre().unwrap_or("Unknown Genre").to_owned(), 137 139 track_len: track.tags.duration().unwrap() as i64, 138 140 year: track.tags.year().unwrap_or(1970) as usize, 141 + artwork_path: track.artwork_path, 139 142 }; 140 143 141 144 t.track_id = track_hash(&t);