diff --git a/.gitignore b/.gitignore index dc0462c..ac307cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ target Cargo.lock fixtures/temp.mp3 + +.DS_Store +.idea/** diff --git a/Cargo.toml b/Cargo.toml index 471e401..f42b906 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "taglib" description = "Rust bindings for TagLib" -version = "1.0.0" +version = "2.0.2" authors = ["Emmanuele Bassi ", "Chris Down "] license = "MIT" repository = "https://github.com/ebassi/taglib-rust/" @@ -14,11 +14,16 @@ name = "taglib" path = "src/lib.rs" [dependencies] +lazy_static = "1.4.0" libc = "0.2" [dependencies.taglib-sys] path = "taglib-sys" -version = "1.0.0" +version = "2.0.0" + +[target.'cfg(target_os = "windows")'.dependencies] +codepage = "0.1.1" +windows-sys = { version = "0.52", features = ["Win32_Globalization"] } [features] default = [] diff --git a/examples/tagreader.rs b/examples/tagreader.rs index b49f809..15f123a 100644 --- a/examples/tagreader.rs +++ b/examples/tagreader.rs @@ -3,6 +3,8 @@ extern crate taglib; use std::env; pub fn main() { + const EMPTY: &str = ""; + let args: Vec = env::args().collect(); for i in 1..args.len() { @@ -23,10 +25,38 @@ pub fn main() { println!("title - {}", t.title().unwrap_or_default()); println!("artist - {}", t.artist().unwrap_or_default()); println!("album - {}", t.album().unwrap_or_default()); - println!("year - {}", t.year().unwrap_or_default()); + println!("year - {}", t.year() + .map_or_else(|| EMPTY.to_string(), |t| t.to_string())); println!("comment - {}", t.comment().unwrap_or_default()); - println!("track - {}", t.track().unwrap_or_default()); + println!("track - {}", t.track() + .map_or_else(|| EMPTY.to_string(), |t| t.to_string())); println!("genre - {}", t.genre().unwrap_or_default()); + + println!("-- File TAG --"); + println!("album artist - {}", t.album_artist().unwrap_or_default()); + println!("composer - {}", t.composer().unwrap_or_default()); + println!("track total - {}", t.track_total() + .map_or_else(|| EMPTY.to_string(), |t| t.to_string())); + println!("disc number - {}", t.disc_number() + .map_or_else(|| EMPTY.to_string(), |t| t.to_string())); + println!("disc total - {}", t.disc_total() + .map_or_else(|| EMPTY.to_string(), |t| t.to_string())); + + println!("-- PROPERTY --"); + let result_keys = file.keys(); + if result_keys.is_ok() { + let keys = result_keys.unwrap(); + println!("{} keys.", keys.len()); + for key in keys { + println!("{}: {:?}", + key, + file.get_property(&key).unwrap_or_default()); + } + } else { + println!("No available properties for {} (error: {:?})", + arg, + result_keys.err().unwrap()); + } } Err(e) => { println!("No available tags for {} (error: {:?})", arg, e); diff --git a/fixtures/pic.jpg b/fixtures/pic.jpg new file mode 100644 index 0000000..8a32213 Binary files /dev/null and b/fixtures/pic.jpg differ diff --git a/fixtures/test.flac b/fixtures/test.flac new file mode 100644 index 0000000..a9814aa Binary files /dev/null and b/fixtures/test.flac differ diff --git a/src/lib.rs b/src/lib.rs index 21bb379..de40c53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,14 +21,27 @@ #![crate_name = "taglib"] #![crate_type = "lib"] +#[cfg(target_os = "windows")] +extern crate codepage; +extern crate lazy_static; extern crate libc; extern crate taglib_sys as sys; +#[cfg(target_os = "windows")] +extern crate windows_sys as windows; -use libc::c_char; -use std::ffi::{CString, CStr}; -use std::path::Path; +use std::{mem, ptr, slice}; +use std::collections::HashSet; +use std::convert::TryInto; +use std::ffi::{CStr, CString, OsStr}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::ptr::null_mut; +use std::str::Utf8Error; +use lazy_static::lazy_static; +use libc::c_char; use sys as ll; +use sys::{TagLib_Complex_Property_Attribute, TagLib_Complex_Property_Picture_Data, TagLib_Variant, TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_BYTE_VECTOR, TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_STRING, TagLib_Variant_Value_Union}; fn c_str_to_str(c_str: *const c_char) -> Option { if c_str.is_null() { @@ -168,6 +181,58 @@ impl<'a> Tag<'a> { ll::taglib_tag_set_track(self.raw, track); } } + + pub fn album_artist(&self) -> Option { + self.file.album_artist() + } + + pub fn composer(&self) -> Option { + self.file.composer() + } + + pub fn copyright(&self) -> Option { + self.file.copyright() + } + + pub fn lyrics(&self) -> Option { + self.file.lyrics() + } + + pub fn track_number(&self) -> Option { + self.file.track_number() + } + + pub fn track_number_string(&self) -> Option { + self.file.track_number_string() + } + + pub fn track_total(&self) -> Option { + self.file.track_total() + } + + pub fn track_total_string(&self) -> Option { + self.file.track_total_string() + } + + pub fn disc_number(&self) -> Option { + self.file.disc_number() + } + + pub fn disc_number_string(&self) -> Option { + self.file.disc_number_string() + } + + pub fn disc_total(&self) -> Option { + self.file.disc_total() + } + + pub fn disc_total_string(&self) -> Option { + self.file.disc_total_string() + } + + pub fn date(&self) -> Option { + self.file.date() + } } impl<'a> AudioProperties<'a> { @@ -195,6 +260,8 @@ impl<'a> AudioProperties<'a> { } } +const MUT_PTR_C_CHAR_LEN: usize = mem::size_of::<*mut c_char>(); + #[derive(Copy, Clone, PartialEq)] pub enum FileType { /// MPEG file @@ -217,6 +284,181 @@ pub enum FileType { MP4 = ll::TAGLIB_FILE_MP4 as isize, /// ASF file ASF = ll::TAGLIB_FILE_ASF as isize, + /// AIFF file + AIFF = ll::TAGLIB_FILE_AIFF as isize, + /// WAV file + WAV = ll::TAGLIB_FILE_WAV as isize, + /// APE file + APE = ll::TAGLIB_FILE_APE as isize, + /// IT file + IT = ll::TAGLIB_FILE_IT as isize, + /// MOD file + MOD = ll::TAGLIB_FILE_MOD as isize, + /// S3M file + S3M = ll::TAGLIB_FILE_S3M as isize, + /// XM file + XM = ll::TAGLIB_FILE_XM as isize, + /// OPUS file + OPUS = ll::TAGLIB_FILE_OPUS as isize, + /// DSF file + DSF = ll::TAGLIB_FILE_DSF as isize, + /// DSDIFF file + DFF = ll::TAGLIB_FILE_DSDIFF as isize, +} + +lazy_static! { + static ref MPEG_SUFFIX: Vec<&'static str> = vec![".mp3", ".aac"]; + static ref OGG_VORBIS_SUFFIX: Vec<&'static str> = vec![".ogg"]; + static ref FLAC_SUFFIX: Vec<&'static str> = vec![".flac"]; + static ref MPC_SUFFIX: Vec<&'static str> = vec![".mpc"]; + static ref OGG_FLAC_SUFFIX: Vec<&'static str> = vec![".flac", ".oga"]; + + static ref WAV_PACK_SUFFIX: Vec<&'static str> = vec![".wv"]; + static ref SPEEX_SUFFIX: Vec<&'static str> = vec![".spx"]; + static ref TRUE_AUDIO_SUFFIX: Vec<&'static str> = vec![".tta"]; + static ref MP4_SUFFIX: Vec<&'static str> = vec![".mp4", ".m4a", "m4b", "m4p", "m4v", "isom", "3g2"]; + static ref ASF_SUFFIX: Vec<&'static str> = vec![".asf", ".wma"]; + + static ref AIFF_SUFFIX: Vec<&'static str> = vec![".aif", ".aiff", ".aifc"]; + static ref WAV_SUFFIX: Vec<&'static str> = vec![".wav"]; + static ref APE_SUFFIX: Vec<&'static str> = vec![".ape"]; + static ref IT_SUFFIX: Vec<&'static str> = vec![".it"]; + static ref MOD_SUFFIX: Vec<&'static str> = vec![".mod"]; + + static ref S3M_SUFFIX: Vec<&'static str> = vec![".s3m"]; + static ref XM_SUFFIX: Vec<&'static str> = vec![".xm"]; + static ref OPUS_SUFFIX: Vec<&'static str> = vec![".opus"]; + static ref DSF_SUFFIX: Vec<&'static str> = vec![".dsf"]; + static ref DFF_SUFFIX: Vec<&'static str> = vec![".dff"]; + + static ref EMPTY_SUFFIX: Vec<&'static str> = vec![]; + + static ref ALL_SUFFIX: HashSet<&'static str> = { + let mut m = HashSet::new(); + m.extend(MPEG_SUFFIX.iter().cloned().collect::>()); + m.extend(OGG_VORBIS_SUFFIX.iter().cloned().collect::>()); + m.extend(FLAC_SUFFIX.iter().cloned().collect::>()); + m.extend(MPC_SUFFIX.iter().cloned().collect::>()); + m.extend(OGG_FLAC_SUFFIX.iter().cloned().collect::>()); + + m.extend(WAV_PACK_SUFFIX.iter().cloned().collect::>()); + m.extend(SPEEX_SUFFIX.iter().cloned().collect::>()); + m.extend(TRUE_AUDIO_SUFFIX.iter().cloned().collect::>()); + m.extend(MP4_SUFFIX.iter().cloned().collect::>()); + m.extend(ASF_SUFFIX.iter().cloned().collect::>()); + + m.extend(AIFF_SUFFIX.iter().cloned().collect::>()); + m.extend(WAV_SUFFIX.iter().cloned().collect::>()); + m.extend(APE_SUFFIX.iter().cloned().collect::>()); + m.extend(IT_SUFFIX.iter().cloned().collect::>()); + m.extend(MOD_SUFFIX.iter().cloned().collect::>()); + + m.extend(S3M_SUFFIX.iter().cloned().collect::>()); + m.extend(XM_SUFFIX.iter().cloned().collect::>()); + m.extend(OPUS_SUFFIX.iter().cloned().collect::>()); + m.extend(DSF_SUFFIX.iter().cloned().collect::>()); + m.extend(DFF_SUFFIX.iter().cloned().collect::>()); + + m + }; +} + +impl FileType { + pub fn name(&self) -> &'static str { + if self == &FileType::MPEG { + "MPEG" + } else if self == &FileType::OggVorbis { + "OggVorbis" + } else if self == &FileType::FLAC { + "FLAC" + } else if self == &FileType::MPC { + "MPC" + } else if self == &FileType::OggFlac { + "OggFlac" + } else if self == &FileType::WavPack { + "WavPack" + } else if self == &FileType::Speex { + "Speex" + } else if self == &FileType::TrueAudio { + "TrueAudio" + } else if self == &FileType::MP4 { + "MP4" + } else if self == &FileType::ASF { + "ASF" + } else if self == &FileType::AIFF { + "AIFF" + } else if self == &FileType::WAV { + "WAV" + } else if self == &FileType::APE { + "APE" + } else if self == &FileType::IT { + "IT" + } else if self == &FileType::MOD { + "MOD" + } else if self == &FileType::S3M { + "S3M" + } else if self == &FileType::XM { + "XM" + } else if self == &FileType::OPUS { + "OPUS" + } else if self == &FileType::DSF { + "DSF" + } else if self == &FileType::DFF { + "DFF" + } else { + "" + } + } + + pub fn suffix(&self) -> &'static Vec<&str> { + if self == &FileType::MPEG { + &MPEG_SUFFIX + } else if self == &FileType::OggVorbis { + &OGG_VORBIS_SUFFIX + } else if self == &FileType::FLAC { + &FLAC_SUFFIX + } else if self == &FileType::MPC { + &MPC_SUFFIX + } else if self == &FileType::OggFlac { + &OGG_FLAC_SUFFIX + } else if self == &FileType::WavPack { + &WAV_PACK_SUFFIX + } else if self == &FileType::Speex { + &SPEEX_SUFFIX + } else if self == &FileType::TrueAudio { + &TRUE_AUDIO_SUFFIX + } else if self == &FileType::MP4 { + &MP4_SUFFIX + } else if self == &FileType::ASF { + &ASF_SUFFIX + } else if self == &FileType::AIFF { + &AIFF_SUFFIX + } else if self == &FileType::WAV { + &WAV_SUFFIX + } else if self == &FileType::APE { + &APE_SUFFIX + } else if self == &FileType::IT { + &IT_SUFFIX + } else if self == &FileType::MOD { + &MOD_SUFFIX + } else if self == &FileType::S3M { + &S3M_SUFFIX + } else if self == &FileType::XM { + &XM_SUFFIX + } else if self == &FileType::OPUS { + &OPUS_SUFFIX + } else if self == &FileType::DSF { + &DSF_SUFFIX + } else if self == &FileType::DFF { + &DSF_SUFFIX + } else { + &EMPTY_SUFFIX + } + } + + pub fn all_suffix() -> &'static HashSet<&'static str> { + &*ALL_SUFFIX + } } #[derive(Debug)] @@ -239,11 +481,56 @@ impl Drop for File { } } +// Define keys for get / set properties +const KEY_ALBUM_ARTIST: &'static str = "ALBUMARTIST"; +const KEY_COMPOSER: &'static str = "COMPOSER"; +const KEY_COPYRIGHT: &'static str = "COPYRIGHT"; +const KEY_LYRICS: &'static str = "LYRICS"; +const KEY_DATE: &'static str = "DATE"; + +// key for property, value like 01/02, first is disc_number, last is disc_total +const KEY_DISC_NUMBER: &'static str = "DISCNUMBER"; +// key for property, value like 01/10, first is track_number, last is track_total +const KEY_TRACK_NUMBER: &'static str = "TRACKNUMBER"; +// key for property, value like 10, only contains track_total +const KEY_TRACK_TOTAL: &'static str = "TRACKTOTAL"; + +const KEY_PICTURE: &'static str = "PICTURE"; + +#[cfg(target_os = "windows")] +fn acp_encode(s: &str) -> Option> { + let acp = unsafe { windows::Win32::Globalization::GetACP() }; + let e = codepage::to_encoding(acp as u16)?; + + let (res, _e, has_error) = e.encode(s); + if !has_error { + let vec = res.iter().cloned().collect(); + Some(vec) + } else { + None + } +} + +#[cfg(not(target_os = "windows"))] +fn acp_encode(_s: &str) -> Option> { + None +} + +fn get_filename_c(filename: &str) -> Result { + acp_encode(filename) + .map_or_else(|| CString::new(filename), + |v| { + let from_vec = unsafe { CString::from_vec_unchecked(v) }; + Ok(from_vec) + }) + .map_err(|_| FileError::InvalidFileName) +} + impl File { /// Creates a new `taglib::File` for the given `filename`. pub fn new>(path: P) -> Result { let filename = path.as_ref().to_str().ok_or(FileError::InvalidFileName)?; - let filename_c = CString::new(filename).ok().ok_or(FileError::InvalidFileName)?; + let filename_c = get_filename_c(filename)?; let filename_c_ptr = filename_c.as_ptr(); let f = unsafe { ll::taglib_file_new(filename_c_ptr) }; @@ -256,13 +543,12 @@ impl File { /// Creates a new `taglib::File` for the given `filename` and type of file. pub fn new_type(filename: &str, filetype: FileType) -> Result { - let filename_c = match CString::new(filename) { - Ok(s) => s, - _ => return Err(FileError::InvalidFileName), - }; - + let filename_c = get_filename_c(filename)?; let filename_c_ptr = filename_c.as_ptr(); - let f = unsafe { ll::taglib_file_new_type(filename_c_ptr, filetype as u32) }; + + let f = unsafe { + ll::taglib_file_new_type(filename_c_ptr, (filetype as u32).try_into().unwrap()) + }; if f.is_null() { return Err(FileError::InvalidFile); } @@ -303,24 +589,626 @@ impl File { } } + pub fn album_artist(&self) -> Option { + self.get_first_property(KEY_ALBUM_ARTIST) + } + + pub fn set_album_artist(&mut self, value: &str) { + self.set_property(KEY_ALBUM_ARTIST, value); + } + + pub fn remove_album_artist(&mut self) { + self.remove_property(KEY_ALBUM_ARTIST); + } + + pub fn composer(&self) -> Option { + self.get_first_property(KEY_COMPOSER) + } + + pub fn set_composer(&mut self, value: &str) { + self.set_property(KEY_COMPOSER, value); + } + + pub fn remove_composer(&mut self) { + self.remove_property(KEY_COMPOSER); + } + + pub fn copyright(&self) -> Option { + self.get_first_property(KEY_COPYRIGHT) + } + + pub fn set_copyright(&mut self, value: &str) { + self.set_property(KEY_COPYRIGHT, value); + } + + pub fn remove_copyright(&mut self) { + self.remove_property(KEY_COPYRIGHT); + } + + pub fn lyrics(&self) -> Option { + self.get_first_property(KEY_LYRICS) + } + + pub fn set_lyrics(&mut self, value: &str) { + self.set_property(KEY_LYRICS, value); + } + + pub fn remove_lyrics(&mut self) { + self.remove_property(KEY_LYRICS); + } + + pub fn date(&self) -> Option { + self.get_first_property(KEY_DATE) + } + + pub fn set_date(&mut self, value: &str) { + self.set_property(KEY_DATE, value); + } + + pub fn remove_date(&mut self) { + self.remove_property(KEY_DATE); + } + + pub fn track_number(&self) -> Option { + self.tag().unwrap().track() + } + + pub fn track_number_string(&self) -> Option { + if let Some(track) = self.tag().unwrap().track() { + if let Some(track_string_from_prop) = self.track_number_string_from_prop() { + if let Some(track_from_prop) = track_string_from_prop.parse::().ok() { + if track_from_prop == track { + return Some(track_string_from_prop); + } + } + } + return Some(track.to_string()); + } + None + } + + pub fn set_track_number(&mut self, value: u32, padding: usize) { + let t = self.tag().unwrap(); + unsafe { + ll::taglib_tag_set_track(t.raw, value); + } + + if let Some(track_total_string) = self.get_first_property(KEY_TRACK_TOTAL) { + if let Some(track_total) = track_total_string.parse::().ok() { + self.set_property(KEY_TRACK_TOTAL, + &decimal_to_padding_string(track_total, padding)); + self.set_property_split_num(KEY_TRACK_NUMBER, + &Some(value), + &Some(track_total), + padding); + return; + } + } + + let track_total = self.track_total_from_prop_track_number(); + if let Some(t) = track_total { + self.set_property(KEY_TRACK_TOTAL, &decimal_to_padding_string(t, padding)); + } + self.set_property_split_num(KEY_TRACK_NUMBER, &Some(value), &track_total, padding); + } + + pub fn remove_track_number(&mut self) { + let t = self.tag().unwrap(); + unsafe { + ll::taglib_tag_set_track(t.raw, 0); + } + + let track_total = self.track_total_string(); + self.set_property_split_text(KEY_TRACK_NUMBER, &None, &track_total); + } + + pub fn track_total(&self) -> Option { + if let Some(track_total_string) = self.get_first_property(KEY_TRACK_TOTAL) { + let track_total = track_total_string.parse::().ok(); + if track_total.is_some() { + return track_total; + } + } + self.track_total_from_prop_track_number() + } + + pub fn track_total_string(&self) -> Option { + let track_total = self.get_first_property(KEY_TRACK_TOTAL); + if track_total.is_some() { + track_total + } else { + self.track_total_string_from_prop_track_number() + } + } + + pub fn set_track_total(&mut self, value: u32, padding: usize) { + self.set_property(KEY_TRACK_TOTAL, &decimal_to_padding_string(value, padding)); + + let track_number = self.track_number_from_prop(); + self.set_property_split_num(KEY_TRACK_NUMBER, &track_number, &Some(value), padding); + } + + pub fn remove_track_total(&mut self) { + self.remove_property(KEY_TRACK_TOTAL); + + let track_number = self.track_number_string(); + self.set_property_split_text(KEY_TRACK_NUMBER, &track_number, &None); + } + + fn track_number_from_prop(&mut self) -> Option { + let (track_number, _) = self.number_pair_by_key(KEY_TRACK_NUMBER); + track_number + } + + fn track_number_string_from_prop(&self) -> Option { + let (track_number, _) = self.text_pair_by_key(KEY_TRACK_NUMBER); + track_number + } + + fn track_total_from_prop_track_number(&self) -> Option { + let (_, track_total) = self.number_pair_by_key(KEY_TRACK_NUMBER); + track_total + } + + fn track_total_string_from_prop_track_number(&self) -> Option { + let (_, track_total) = self.text_pair_by_key(KEY_TRACK_NUMBER); + track_total + } + + pub fn disc_number(&self) -> Option { + let (disc_number, _) = self.number_pair_by_key(KEY_DISC_NUMBER); + disc_number + } + + pub fn disc_number_string(&self) -> Option { + let (disc_number, _) = self.text_pair_by_key(KEY_DISC_NUMBER); + disc_number + } + + pub fn set_disc_number(&mut self, value: u32, padding: usize) { + let disc_total = self.disc_total(); + self.set_property_split_num(KEY_DISC_NUMBER, &Some(value), &disc_total, padding); + } + + pub fn remove_disc_number(&mut self) { + let (_, disc_total) = self.text_pair_by_key(KEY_DISC_NUMBER); + self.set_property_split_text(KEY_DISC_NUMBER, &None, &disc_total); + } + + pub fn disc_total(&self) -> Option { + let (_, disc_total) = self.number_pair_by_key(KEY_DISC_NUMBER); + disc_total + } + + pub fn disc_total_string(&self) -> Option { + let (_, disc_total) = self.text_pair_by_key(KEY_DISC_NUMBER); + disc_total + } + + pub fn set_disc_total(&mut self, total_disc: u32, padding: usize) { + let disc_number = self.disc_number(); + self.set_property_split_num(KEY_DISC_NUMBER, &disc_number, &Some(total_disc), padding); + } + + pub fn remove_disc_total(&mut self) { + let (disc_number, _) = self.text_pair_by_key(KEY_DISC_NUMBER); + self.set_property_split_text(KEY_DISC_NUMBER, &disc_number, &None); + } + + fn set_property_split_text(&mut self, + key: &str, + first: &Option, + last: &Option) { + self.remove_property(key); + if let Some(ref value) = text_pair_to_string(first, last) { + self.set_property(key, value); + } + } + + fn set_property_split_num(&mut self, + key: &str, + first: &Option, + last: &Option, + padding: usize) { + self.remove_property(key); + if let Some(ref value) = num_pair_to_string(first, last, padding) { + self.set_property(key, value); + } + } + + fn number_pair_by_key(&self, key: &str) -> (Option, Option) { + if let Some(ref text) = self.get_first_property(key) { + get_number_pair(text) + } else { + (None, None) + } + } + + fn text_pair_by_key(&self, key: &str) -> (Option, Option) { + if let Some(ref text) = self.get_first_property(key) { + get_text_pair(text) + } else { + (None, None) + } + } + + pub fn get_first_property(&self, key: &str) -> Option { + let vec = self.get_property(key).ok()?; + if !vec.is_empty() { + Some(vec.first().unwrap().clone()) + } else { + None + } + } + + pub fn get_property(&self, key: &str) -> Result, Utf8Error> { + let cs = CString::new(key).unwrap(); + let s = cs.as_ptr(); + let call_res = unsafe { + ll::taglib_property_get(self.raw, s) + }; + c_char_to_vec_string_free(call_res) + } + + pub fn keys(&self) -> Result, Utf8Error> { + let call_res = unsafe { + ll::taglib_property_keys(self.raw) + }; + c_char_to_vec_string_free(call_res) + } + + pub fn set_property(&mut self, key: &str, value: &str) { + let cs = CString::new(key).unwrap(); + let s = cs.as_ptr(); + + let vs = CString::new(value).unwrap(); + let v = vs.as_ptr(); + unsafe { + ll::taglib_property_set(self.raw, s, v); + } + } + + pub fn set_append_property(&mut self, key: &str, value: &str) { + let cs = CString::new(key).unwrap(); + let s = cs.as_ptr(); + + let vs = CString::new(value).unwrap(); + let v = vs.as_ptr(); + unsafe { + ll::taglib_property_set_append(self.raw, s, v); + } + } + + pub fn remove_property(&mut self, key: &str) { + let cs = CString::new(key).unwrap(); + let s = cs.as_ptr(); + unsafe { + ll::taglib_property_set(self.raw, s, ptr::null()); + } + } + + pub fn extract_pic_file(&self, path: &Path, stem_suffix: &OsStr) -> Option { + let cs = CString::new(KEY_PICTURE).unwrap(); + let mut picture: TagLib_Complex_Property_Picture_Data = TagLib_Complex_Property_Picture_Data { + mime_type: null_mut(), + description: null_mut(), + picture_type: null_mut(), + data: null_mut(), + size: 0, + }; + + let props: *mut *mut *mut TagLib_Complex_Property_Attribute; + unsafe { + props = ll::taglib_complex_property_get(self.raw, cs.as_ptr()); + ll::taglib_picture_from_complex_property(props, &mut picture); + } + + let description = c_str_to_str(picture.description); + let picture_type = c_str_to_str(picture.picture_type); + let mime_type = c_str_to_str(picture.mime_type); + + let size = picture.size as usize; + println!("extract pic - mime_type: {:?}, size: {}, description: {:?}, picture_type: {:?}", + &mime_type, size, &description, &picture_type); + let res = if mime_type.is_some() && size > 0 { + let mut pic_path = PathBuf::from(path); + if !stem_suffix.is_empty() { + pic_path.set_extension(""); + pic_path.as_mut_os_string().push(stem_suffix); + } + pic_path.set_extension(get_ext(mime_type).unwrap()); + + match std::fs::File::create(&pic_path) { + Ok(mut pic_file) => { + let u8slice = unsafe { + slice::from_raw_parts(picture.data as *const u8, size) + }; + + if let Some(_) = pic_file.write_all(u8slice).ok() { + Some(pic_path) + } else { + None + } + } + Err(_) => None, + } + } else { + None + }; + + unsafe { + ll::taglib_complex_property_free(props); + } + res + } + + pub fn set_pic(&mut self, + data: &mut Vec, + size: usize, + mime_type: &str) { + self.set_pic_detail(data, + size, + mime_type, + "Written by TagLib", + "Front Cover") + } + + pub fn set_pic_detail(&mut self, + data: &mut Vec, + size: usize, + mime_type: &str, + description: &str, + picture_type: &str) { + let key_data_cstr = CString::new("data").unwrap(); + let key_mime_type_cstr = CString::new("mimeType").unwrap(); + let key_description_cstr = CString::new("description").unwrap(); + let key_picture_type_cstr = CString::new("pictureType").unwrap(); + println!("key_data_cstr: {:?}, key_mime_type_cstr: {:?}, \ + key_description_cstr: {:?}, key_picture_type_cstr: {:?}", + &key_data_cstr, &key_mime_type_cstr, + &key_description_cstr, &key_picture_type_cstr); + + let mime_type_cstr = CString::new(mime_type).unwrap(); + let description_cstr = CString::new(description).unwrap(); + let picture_type_cstr = CString::new(picture_type).unwrap(); + println!("mime_type_cstr: {:?}, description_cstr: {:?}, picture_type_cstr: {:?}", + &mime_type, description_cstr, picture_type_cstr); + + data.push(b'\0'); + + let attrs: [TagLib_Complex_Property_Attribute; 4] = [ + TagLib_Complex_Property_Attribute { + key: key_data_cstr.into_bytes().into_iter() + .map(|e| e as c_char) + .collect::>() + .as_mut_ptr(), + value: TagLib_Variant { + r#type: TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_BYTE_VECTOR, + size: size as u32, + value: TagLib_Variant_Value_Union { byte_vector_value: data.as_mut_ptr() as *mut c_char }, + }, + }, + TagLib_Complex_Property_Attribute { + key: key_mime_type_cstr.into_bytes().into_iter() + .map(|e| e as c_char) + .collect::>() + .as_mut_ptr(), + value: TagLib_Variant { + r#type: TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_STRING, + size: 0, + value: TagLib_Variant_Value_Union { + string_value: mime_type_cstr.into_bytes().into_iter() + .map(|e| e as c_char) + .collect::>() + .as_mut_ptr() + }, + }, + }, + TagLib_Complex_Property_Attribute { + key: key_description_cstr.into_bytes().into_iter() + .map(|e| e as c_char) + .collect::>() + .as_mut_ptr(), + value: TagLib_Variant { + r#type: TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_STRING, + size: 0, + value: TagLib_Variant_Value_Union { + string_value: description_cstr.into_bytes().into_iter() + .map(|e| e as c_char) + .collect::>() + .as_mut_ptr() + }, + }, + }, + TagLib_Complex_Property_Attribute { + key: key_picture_type_cstr.into_bytes().into_iter() + .map(|e| e as c_char) + .collect::>() + .as_mut_ptr(), + value: TagLib_Variant { + r#type: TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_STRING, + size: 0, + value: TagLib_Variant_Value_Union { + string_value: picture_type_cstr.into_bytes().into_iter() + .map(|e| e as c_char) + .collect::>() + .as_mut_ptr() + }, + }, + }, + ]; + + let argv: [*const TagLib_Complex_Property_Attribute; 5] = + [ + &attrs[0], + &attrs[1], + &attrs[2], + &attrs[3], + ptr::null(), + ]; + + self.remove_pic(); + let cs = CString::new(KEY_PICTURE).unwrap(); + unsafe { + ll::taglib_complex_property_set(self.raw, cs.as_ptr(), &mut (argv.as_ptr() as *const _)); + ll::taglib_file_save(self.raw); + } + } + + pub fn remove_pic(&mut self) { + let cs = CString::new(KEY_PICTURE).unwrap(); + unsafe { + ll::taglib_complex_property_set(self.raw, cs.as_ptr(), null_mut()); + } + } + /// Updates the meta-data of the file. pub fn save(&self) -> bool { unsafe { ll::taglib_file_save(self.raw) != 0 } } } +fn get_ext(mime_type: Option) -> Option<&'static str> { + mime_type.map_or_else(|| None, + |ref mime_str| { + if mime_str.eq("image/png") { + Some("png") + } else { + Some("jpg") + } + }) +} + +fn text_pair_to_string(first: &Option, last: &Option) -> Option { + match (first, last) { + (None, None) => None, + (None, Some(b)) => Some("/".to_owned() + b), + (Some(a), None) => Some(a.to_owned()), + (Some(a), Some(b)) => Some(a.to_owned() + "/" + b), + } +} + +fn num_pair_to_string(first: &Option, + second: &Option, + padding: usize) -> Option { + match (first, second) { + (None, None) => None, + (None, Some(b)) => Some("/".to_owned() + &decimal_to_padding_string(*b, padding)), + (Some(a), None) => Some(decimal_to_padding_string(*a, padding)), + (Some(a), Some(b)) => { + Some(decimal_to_padding_string(*a, padding) + "/" + + &decimal_to_padding_string(*b, padding)) + } + } +} + +fn get_text_pair(text: &str) -> (Option, Option) { + let mut split = text.split('/'); + let first = get_text(&split.next()); + let second = get_text(&split.next()); + (first, second) +} + +fn get_text(input: &Option<&str>) -> Option { + if let Some(string) = input { + let string = string.trim(); + if !string.is_empty() { + return Some(string.to_owned()); + } + } + None +} + +fn get_number_pair(text: &str) -> (Option, Option) { + let mut split = text.split('/'); + let first = get_num(&split.next()); + let second = get_num(&split.next()); + (first, second) +} + +fn get_num(input: &Option<&str>) -> Option { + if let Some(string) = input { + string.trim().parse::().ok() + } else { + None + } +} + +fn c_char_to_vec_string_free(ptr: *mut *mut c_char) -> Result, Utf8Error> { + if ptr.is_null() { + Ok(Vec::new()) + } else { + unsafe { + let res = convert_double_pointer_to_vec(ptr); + ll::taglib_property_free(ptr); + res + } + } +} + +unsafe fn convert_double_pointer_to_vec(data: *mut *mut c_char) -> Result, Utf8Error> { + let mut p = data; + let mut res: Vec = vec![]; + while p.as_ref().unwrap().as_ref().is_some() { + let ele = CStr::from_ptr(p.as_ref().unwrap().as_ref().unwrap()) + .to_str().map(ToString::to_string)?; + res.push(ele); + p = p.byte_offset(MUT_PTR_C_CHAR_LEN as isize); + } + Ok(res) +} + +fn decimal_to_padding_string(decimal: u32, padding: usize) -> String { + format!("{:0width$}", decimal, width = padding) +} /// Fixture creation: /// ffmpeg -t 0.01 -f s16le -i /dev/zero test.mp3 /// kid3-cli -c 'set artist "Artist"' test.mp3 #[cfg(test)] mod test { - const TEST_MP3: &'static str = "fixtures/test.mp3"; - - use super::*; use std::fs; use std::path::PathBuf; + use super::*; + + const TEST_MP3: &'static str = "fixtures/test.mp3"; + const TEST_FLAC: &'static str = "fixtures/test.flac"; + + #[test] + fn test_get_number_pair() { + assert_eq!(get_number_pair(""), (None, None)); + assert_eq!(get_number_pair("/"), (None, None)); + assert_eq!(get_number_pair(" / "), (None, None)); + + assert_eq!(get_number_pair("2"), (Some(2), None)); + assert_eq!(get_number_pair("02"), (Some(2), None)); + assert_eq!(get_number_pair(" 2 "), (Some(2), None)); + assert_eq!(get_number_pair("12"), (Some(12), None)); + + assert_eq!(get_number_pair("/15"), (None, Some(15))); + assert_eq!(get_number_pair(" /15"), (None, Some(15))); + + assert_eq!(get_number_pair("02/15"), (Some(2), Some(15))); + } + + #[test] + fn test_get_text_pair() { + assert_eq!(get_text_pair(""), (None, None)); + assert_eq!(get_text_pair("/"), (None, None)); + assert_eq!(get_text_pair(" / "), (None, None)); + + assert_eq!(get_text_pair("2"), (Some("2".to_owned()), None)); + assert_eq!(get_text_pair("02"), (Some("02".to_owned()), None)); + assert_eq!(get_text_pair(" 2 "), (Some("2".to_owned()), None)); + assert_eq!(get_text_pair("12"), (Some("12".to_owned()), None)); + + assert_eq!(get_text_pair("/15"), (None, Some("15".to_owned()))); + assert_eq!(get_text_pair(" /15"), (None, Some("15".to_owned()))); + + assert_eq!(get_text_pair("02/15"), (Some("02".to_owned()), Some("15".to_owned()))); + } + #[test] fn test_get_tag() { let file = File::new(TEST_MP3).unwrap(); @@ -373,4 +1261,93 @@ mod test { fs::remove_file(temp_fn).unwrap(); } + + #[test] + fn test_extract_pic_ok() { + let temp_fn = "fixtures/pic_temp.flac"; + fs::copy(TEST_FLAC, temp_fn).unwrap(); + let file = File::new(temp_fn).unwrap(); + + let path = Path::new(temp_fn); + match file.extract_pic_file(path, "_cover".as_ref()) { + Some(ref path) => { + assert!(true); + + assert!(path.is_file()); + assert!(path.metadata().unwrap().len() > 0); + + fs::remove_file(path).unwrap(); + } + None => { + // fail! + assert!(false); + } + } + + fs::remove_file(temp_fn).unwrap(); + } + + #[test] + fn test_extract_pic_empty() { + let temp_fn = "fixtures/pic_temp.mp3"; + fs::copy(TEST_MP3, temp_fn).unwrap(); + let file = File::new(temp_fn).unwrap(); + + let path = Path::new(temp_fn); + let res = file.extract_pic_file(path, "_cover".as_ref()); + assert!(res.is_none()); + + fs::remove_file(temp_fn).unwrap(); + } + + #[test] + fn test_set_pic() { + let temp_fn = "fixtures/set_pic_temp.flac"; + fs::copy(TEST_FLAC, temp_fn).unwrap(); + let mut file = File::new(temp_fn).unwrap(); + + let mut data: Vec = fs::read("fixtures/pic.jpg").unwrap(); + let len = data.len(); + println!("size: {}", len); + //println!("data: {:?}", &data); + + file.set_pic(&mut data, len, "image/jpeg"); + assert!(true); + + let path = Path::new(temp_fn); + match file.extract_pic_file(path, "_cover".as_ref()) { + Some(ref path) => { + assert!(true); + + assert!(path.is_file()); + assert!(path.metadata().unwrap().len() > 0); + + fs::remove_file(path).unwrap(); + } + None => { + // fail! should NOT in the branch! + assert!(false); + } + } + + fs::remove_file(temp_fn).unwrap(); + } + + #[test] + fn test_remove_pic() { + let temp_fn = "fixtures/remove_pic_temp.flac"; + fs::copy(TEST_FLAC, temp_fn).unwrap(); + let mut file = File::new(temp_fn).unwrap(); + + file.remove_pic(); + file.save(); + assert!(true); + + let file = File::new(temp_fn).unwrap(); + let path = Path::new(temp_fn); + let res = file.extract_pic_file(path, "_cover".as_ref()); + assert!(res.is_none()); + + fs::remove_file(temp_fn).unwrap(); + } } diff --git a/taglib-sys/Cargo.toml b/taglib-sys/Cargo.toml index 938bb79..90e745b 100644 --- a/taglib-sys/Cargo.toml +++ b/taglib-sys/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "taglib-sys" description = "Raw TagLib bindings for Rust, used internally by TagLib-Rust" -version = "1.0.0" +version = "2.0.0" authors = ["Emmanuele Bassi ", "Chris Down "] repository = "https://github.com/ebassi/taglib-rust" license = "MIT" diff --git a/taglib-sys/build.rs b/taglib-sys/build.rs index eb32353..f7afe45 100644 --- a/taglib-sys/build.rs +++ b/taglib-sys/build.rs @@ -1,10 +1,88 @@ #[cfg(feature = "pkg-config")] extern crate pkg_config; +extern crate core; +use std::collections::HashSet; +use std::env; +use std::path::PathBuf; + +const KEY_TAGLIB_STATIC: &'static str = "TAGLIB_STATIC"; +const KEY_TAGLIB_DIRS: &'static str = "TAGLIB_LIB_DIRS"; +const KEY_TAGLIB_EXTRA_LIBS: &'static str = "TAGLIB_EXTRA_LIBS"; + +// if not empty and not zero, build as static link, default is dynamic link (dll/so/dylib), example: +// TAGLIB_STATIC=1 +// multiple dir separated by char `:` in Unix/Linux/Mac, ';' in Windows, example: +// TAGLIB_LIB_DIRS=/others/lib:/opt/usr/local/lib +// multiple name separated by char `:`, example: +// TAGLIB_EXTRA_LIBS=zlib fn main() { if !build_pkgconfig() { - println!("cargo:rustc-flags=-l tag_c -l tag"); + build_env(); + } +} + +fn build_env() { + let sep = get_sep(); + + let lib_dirs = get_lib_dirs(sep); + for dir in &lib_dirs { + if !dir.exists() { + panic!("library directory does not exist: {}", dir.to_string_lossy()); + } + println!("cargo:rustc-link-search=native={}", dir.to_string_lossy()); } + + let kind = get_link_mode(); + let extra_libs = get_extra_libs(); + for lib in &extra_libs { + println!("cargo:rustc-link-lib={}={}", kind, lib); + } + + println!("cargo:rustc-link-lib={}={}", kind, "tag_c"); + println!("cargo:rustc-link-lib={}={}", kind, "tag"); +} + +fn get_extra_libs() -> HashSet { + get_env_hashset_string(KEY_TAGLIB_EXTRA_LIBS, ':') +} + +fn get_sep() -> char { + match env::var("TARGET") { + Ok(ref t) => { + if t.to_lowercase().contains("windows") { + ';' + } else { + ':' + } + } + _ => ':' + } +} + +fn get_lib_dirs(sep: char) -> Vec { + get_env_hashset_string(KEY_TAGLIB_DIRS, sep).into_iter() + .map(PathBuf::from).collect::>() +} + +fn get_env_hashset_string(env_key: &str, sep: char) -> HashSet { + println!("cargo:rerun-if-env-changed={}", env_key); + env::var(env_key) + .map_or_else(|_| HashSet::new(), + |v| v.split(sep) + .map(|e| e.trim().to_owned()).collect::>()) +} + +fn get_link_mode() -> &'static str { + match &get_env_string(KEY_TAGLIB_STATIC) { + None => "dylib", + Some(v) => if v.eq("0") { "dylib" } else { "static" } + } +} + +fn get_env_string(env_key: &str) -> Option { + println!("cargo:rerun-if-env-changed={}", env_key); + env::var(KEY_TAGLIB_STATIC).ok() } #[cfg(not(feature = "pkg-config"))] diff --git a/taglib-sys/src/lib.rs b/taglib-sys/src/lib.rs index 774b319..689b416 100644 --- a/taglib-sys/src/lib.rs +++ b/taglib-sys/src/lib.rs @@ -21,7 +21,7 @@ #![allow(non_camel_case_types)] extern crate libc; -use libc::{c_int, c_uint, c_char, c_void}; +use libc::{c_int, c_uint, c_char, c_void, c_longlong, c_ulonglong}; // Public types; these are all opaque pointer types pub type TagLib_File = c_void; @@ -41,6 +41,16 @@ pub const TAGLIB_FILE_SPEEX: TagLib_FileType = 6; pub const TAGLIB_FILE_TRUE_AUDIO: TagLib_FileType = 7; pub const TAGLIB_FILE_MP4: TagLib_FileType = 8; pub const TAGLIB_FILE_ASF: TagLib_FileType = 9; +pub const TAGLIB_FILE_AIFF: TagLib_FileType = 10; +pub const TAGLIB_FILE_WAV: TagLib_FileType = 11; +pub const TAGLIB_FILE_APE: TagLib_FileType = 12; +pub const TAGLIB_FILE_IT: TagLib_FileType = 13; +pub const TAGLIB_FILE_MOD: TagLib_FileType = 14; +pub const TAGLIB_FILE_S3M: TagLib_FileType = 15; +pub const TAGLIB_FILE_XM: TagLib_FileType = 16; +pub const TAGLIB_FILE_OPUS: TagLib_FileType = 17; +pub const TAGLIB_FILE_DSF: TagLib_FileType = 18; +pub const TAGLIB_FILE_DSDIFF: TagLib_FileType = 19; // tag_c.h extern "C" { @@ -71,8 +81,155 @@ extern "C" { pub fn taglib_tag_set_track(tag: *mut TagLib_Tag, track: c_uint); pub fn taglib_tag_free_strings(); + #[doc = " Get the keys of the property map.\n\n \ + \\return NULL terminated array of C-strings (char *), only NULL if empty.\n \ + It must be freed by the client using taglib_property_free()."] + pub fn taglib_property_keys(file: *const TagLib_File) -> *mut *mut c_char; + + #[doc = " Get value(s) of property \\a prop.\n\n \ + \\return NULL terminated array of C-strings (char *), only NULL if empty.\n \ + It must be freed by the client using taglib_property_free()."] + pub fn taglib_property_get(file: *const TagLib_File, + prop: *const c_char, ) -> *mut *mut c_char; + + #[doc = " Sets the property \\a prop with \\a value. \ + Use \\a value = NULL to remove\n the property, otherwise it will be replaced."] + pub fn taglib_property_set(file: *mut TagLib_File, + prop: *const c_char, + value: *const c_char); + + #[doc = " Appends \\a value to the property \\a prop (sets it if non-existing).\n \ + Use \\a value = NULL to remove all values associated with the property."] + pub fn taglib_property_set_append(file: *mut TagLib_File, + prop: *const c_char, + value: *const c_char); + + #[doc = " Frees the NULL terminated array \\a props and the C-strings it contains."] + pub fn taglib_property_free(props: *mut *mut c_char); + pub fn taglib_audioproperties_length(properties: *const TagLib_AudioProperties) -> c_int; pub fn taglib_audioproperties_bitrate(properties: *const TagLib_AudioProperties) -> c_int; pub fn taglib_audioproperties_samplerate(properties: *const TagLib_AudioProperties) -> c_int; pub fn taglib_audioproperties_channels(properties: *const TagLib_AudioProperties) -> c_int; } + +pub const TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_VOID: TagLib_Variant_Type = 0; +pub const TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_BOOL: TagLib_Variant_Type = 1; +pub const TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_INT: TagLib_Variant_Type = 2; +pub const TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_UINT: TagLib_Variant_Type = 3; +pub const TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_LONG_LONG: TagLib_Variant_Type = 4; +pub const TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_ULONG_LONG: TagLib_Variant_Type = 5; +pub const TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_DOUBLE: TagLib_Variant_Type = 6; +pub const TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_STRING: TagLib_Variant_Type = 7; +pub const TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_STRING_LIST: TagLib_Variant_Type = 8; +pub const TAGLIB_VARIANT_TYPE_TAGLIB_VARIANT_BYTE_VECTOR: TagLib_Variant_Type = 9; + +#[doc = " Types which can be stored in a TagLib_Variant.\n\n \ +related TagLib::Variant::Type\n \ +These correspond to TagLib::Variant::Type, but ByteVectorList, VariantList,\n \ +VariantMap are not supported and will be returned as their string\n \ +representation."] +pub type TagLib_Variant_Type = c_uint; + +#[doc = " Discriminated union used in complex property attributes.\n\n \ +e type must be set according to the \ +e value union used.\n \ +e size is only required for TagLib_Variant_ByteVector and must contain\n \ +the number of bytes.\n\n \ +related TagLib::Variant."] +#[repr(C)] +#[derive(Copy, Clone)] +pub struct TagLib_Variant { + pub r#type: TagLib_Variant_Type, + pub size: c_uint, + pub value: TagLib_Variant_Value_Union, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub union TagLib_Variant_Value_Union { + pub string_value: *mut c_char, + pub byte_vector_value: *mut c_char, + pub string_list_value: *mut *mut c_char, + pub bool_value: c_int, + pub int_value: c_int, + pub u_int_value: c_uint, + pub long_long_value: c_longlong, + pub u_long_long_value: c_ulonglong, + pub double_value: f64, +} + +#[doc = " Attribute of a complex property.\n \ +Complex properties consist of a NULL-terminated array of pointers to\n \ +this structure with \\e key and \\e value."] +#[repr(C)] +#[derive(Copy, Clone)] +pub struct TagLib_Complex_Property_Attribute { + pub key: *mut c_char, + pub value: TagLib_Variant, +} + +#[doc = " Picture data extracted from a complex property by the convenience function\n \ +taglib_picture_from_complex_property()."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct TagLib_Complex_Property_Picture_Data { + pub mime_type: *mut c_char, + pub description: *mut c_char, + pub picture_type: *mut c_char, + pub data: *mut c_char, + pub size: c_uint, +} + +extern "C" { + #[doc = " Sets the complex property \\a key with \\a value. Use \\a value = NULL to\n \ + remove the property, otherwise it will be replaced with the NULL\n \ + terminated array of attributes in \\a value.\n\n \ + A picture can be set with the TAGLIB_COMPLEX_PROPERTY_PICTURE macro"] + pub fn taglib_complex_property_set( + file: *mut TagLib_File, + key: *const c_char, + value: *mut *const TagLib_Complex_Property_Attribute, + ) -> c_int; + + #[doc = " Appends \\a value to the complex property \\a key (sets it if non-existing).\n \ + Use \\a value = NULL to remove all values associated with the \\a key."] + pub fn taglib_complex_property_set_append( + file: *mut TagLib_File, + key: *const c_char, + value: *mut *const TagLib_Complex_Property_Attribute, + ) -> c_int; + + #[doc = " Get the keys of the complex properties.\n\n \ + return NULL terminated array of C-strings (char *), only NULL if empty.\n \ + It must be freed by the client using taglib_complex_property_free_keys()."] + pub fn taglib_complex_property_keys( + file: *const TagLib_File, + ) -> *mut *mut c_char; + + #[doc = " Get value(s) of complex property \\a key.\n\n \ + return NULL terminated array of property values, which are themselves an\n \ + array of property attributes, only NULL if empty.\n \ + It must be freed by the client using taglib_complex_property_free()."] + pub fn taglib_complex_property_get( + file: *const TagLib_File, + key: *const c_char, + ) -> *mut *mut *mut TagLib_Complex_Property_Attribute; + + #[doc = " Extract the complex property values of a picture.\n\n \ + This function can be used to get the data from a \"PICTURE\" complex property\n \ + without having to traverse the whole variant map. A picture can be\n retrieved."] + pub fn taglib_picture_from_complex_property( + properties: *mut *mut *mut TagLib_Complex_Property_Attribute, + picture: *mut TagLib_Complex_Property_Picture_Data, + ); + + #[doc = " Frees the NULL terminated array \\a keys (as returned by\n \ + taglib_complex_property_keys()) and the C-strings it contains."] + pub fn taglib_complex_property_free_keys(keys: *mut *mut c_char); + + #[doc = " Frees the NULL terminated array \\a props of property attribute arrays\n \ + (as returned by taglib_complex_property_get()) and the data such as\n \ + C-strings and byte vectors contained in these attributes."] + pub fn taglib_complex_property_free(props: *mut *mut *mut TagLib_Complex_Property_Attribute); +}