From 6385928150108f4c47a859ba4e7c16a0098db43f Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 17 Feb 2025 14:24:33 -0700 Subject: [PATCH] musikr: add metadata builders --- musikr/src/main/jni/Cargo.lock | 7 + musikr/src/main/jni/Cargo.toml | 1 + musikr/src/main/jni/shim/id3v2_shim.cpp | 4 + musikr/src/main/jni/shim/id3v2_shim.hpp | 1 + musikr/src/main/jni/src/jbuilder.rs | 214 +++++++++++++++++++++ musikr/src/main/jni/src/lib.rs | 3 + musikr/src/main/jni/src/taglib/bridge.rs | 2 + musikr/src/main/jni/src/taglib/id3v2.rs | 8 +- musikr/src/main/jni/src/taglib/iostream.rs | 1 + musikr/src/main/jni/src/taglib/mod.rs | 4 +- musikr/src/main/jni/src/taglib/tk.rs | 13 +- musikr/src/main/jni/src/tagmap.rs | 74 +++++++ 12 files changed, 327 insertions(+), 5 deletions(-) create mode 100644 musikr/src/main/jni/src/jbuilder.rs create mode 100644 musikr/src/main/jni/src/tagmap.rs diff --git a/musikr/src/main/jni/Cargo.lock b/musikr/src/main/jni/Cargo.lock index a899bbbfc..bfe68402c 100644 --- a/musikr/src/main/jni/Cargo.lock +++ b/musikr/src/main/jni/Cargo.lock @@ -8,6 +8,12 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" + [[package]] name = "bytes" version = "1.10.0" @@ -193,6 +199,7 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" name = "metadatajni" version = "1.0.0" dependencies = [ + "bytemuck", "cxx", "cxx-build", "jni", diff --git a/musikr/src/main/jni/Cargo.toml b/musikr/src/main/jni/Cargo.toml index 9cfda0d2b..1996604ea 100644 --- a/musikr/src/main/jni/Cargo.toml +++ b/musikr/src/main/jni/Cargo.toml @@ -8,6 +8,7 @@ name = "metadatajni" crate-type = ["cdylib"] [dependencies] +bytemuck = "1.21.0" cxx = "1.0.137" jni = "0.21.1" diff --git a/musikr/src/main/jni/shim/id3v2_shim.cpp b/musikr/src/main/jni/shim/id3v2_shim.cpp index b65270146..757446ab8 100644 --- a/musikr/src/main/jni/shim/id3v2_shim.cpp +++ b/musikr/src/main/jni/shim/id3v2_shim.cpp @@ -25,6 +25,10 @@ namespace taglib_shim { return dynamic_cast(frame); } + std::unique_ptr Frame_id(const TagLib::ID3v2::Frame& frame) { + return std::make_unique(frame.frameID()); + } + std::unique_ptr AttachedPictureFrame_picture(const TagLib::ID3v2::AttachedPictureFrame& frame) { return std::make_unique(frame.picture()); } diff --git a/musikr/src/main/jni/shim/id3v2_shim.hpp b/musikr/src/main/jni/shim/id3v2_shim.hpp index 7e6122125..6b8a09898 100644 --- a/musikr/src/main/jni/shim/id3v2_shim.hpp +++ b/musikr/src/main/jni/shim/id3v2_shim.hpp @@ -20,6 +20,7 @@ namespace taglib_shim { std::unique_ptr> FrameList_to_vector(const TagLib::ID3v2::FrameList& list); // Frame type checking and casting + std::unique_ptr Frame_id(const TagLib::ID3v2::Frame &frame); const TagLib::ID3v2::TextIdentificationFrame* Frame_asTextIdentification(const TagLib::ID3v2::Frame* frame); const TagLib::ID3v2::UserTextIdentificationFrame* Frame_asUserTextIdentification(const TagLib::ID3v2::Frame* frame); const TagLib::ID3v2::AttachedPictureFrame* Frame_asAttachedPicture(const TagLib::ID3v2::Frame* frame); diff --git a/musikr/src/main/jni/src/jbuilder.rs b/musikr/src/main/jni/src/jbuilder.rs new file mode 100644 index 000000000..d25e87ef1 --- /dev/null +++ b/musikr/src/main/jni/src/jbuilder.rs @@ -0,0 +1,214 @@ +use jni::{objects::{JByteArray, JClass, JMap, JObject, JString, JValueGen}, sys::jlong, JNIEnv}; +use std::rc::Rc; +use std::cell::RefCell; + +use crate::taglib::{ + id3v1, id3v2, xiph, mp4, tk, audioproperties, +}; + +use crate::tagmap::JTagMap; + +pub struct JMetadataBuilder<'local, 'file_ref> { + env: Rc>>, + id3v2: JTagMap<'local>, + xiph: JTagMap<'local>, + mp4: JTagMap<'local>, + cover: Option>, + properties: Option>, + mime_type: Option, +} + +impl<'local, 'file_ref> JMetadataBuilder<'local, 'file_ref> { + pub fn new(env: Rc>>) -> Self { + Self { + id3v2: JTagMap::new(env.clone()), + xiph: JTagMap::new(env.clone()), + mp4: JTagMap::new(env.clone()), + env, + cover: None, + properties: None, + mime_type: None, + } + } + + pub fn set_mime_type(&mut self, mime_type: impl Into) { + self.mime_type = Some(mime_type.into()); + } + + pub fn set_id3v1(&mut self, tag: &id3v1::ID3v1Tag) { + if let Some(title) = tag.title() { + self.id3v2.add_id("TIT2", title.to_string()); + } + if let Some(artist) = tag.artist() { + self.id3v2.add_id("TPE1", artist.to_string()); + } + if let Some(album) = tag.album() { + self.id3v2.add_id("TALB", album.to_string()); + } + self.id3v2.add_id("TRCK", tag.track().to_string()); + self.id3v2.add_id("TYER", tag.year().to_string()); + + let genre = tag.genre_index(); + if genre != 255 { + self.id3v2.add_id("TCON", genre.to_string()); + } + } + + pub fn set_id3v2(&mut self, tag: &id3v2::ID3v2Tag) { + let mut first_pic = None; + let mut front_cover_pic = None; + + if let Some(frames) = tag.frames() { + for mut frame in frames.to_vec() { + if let Some(text_frame) = frame.as_text_identification() { + if let Some(field_list) = text_frame.field_list() { + let values: Vec = field_list.to_vec() + .into_iter() + .map(|s| s.to_string()) + .collect(); + self.id3v2.add_id_list(frame.id().to_string_lossy(), values); + } + } else if let Some(user_text_frame) = frame.as_user_text_identification() { + if let Some(values) = user_text_frame.values() { + let mut values = values.to_vec(); + if !values.is_empty() { + let description = values.remove(0); + for value in values { + self.id3v2.add_combined( + frame.id().to_string_lossy(), + description.clone(), + value.to_string(), + ); + } + } + } + } else if let Some(picture_frame) = frame.as_attached_picture() { + if first_pic.is_none() { + first_pic = picture_frame.picture().map(|p| p.to_vec()); + } + // TODO: Check for front cover type when bindings are available + if front_cover_pic.is_none() { + front_cover_pic = picture_frame.picture().map(|p| p.to_vec()); + } + } + } + } + + // Prefer front cover, fall back to first picture + self.cover = front_cover_pic.or(first_pic); + } + + pub fn set_xiph(&mut self, tag: &xiph::XiphComment) { + for (key, values) in tag.field_list_map().to_hashmap() { + let values: Vec = values.to_vec().into_iter().map(|s| s.to_string()).collect(); + self.xiph.add_id_list(key.to_uppercase(), values); + } + + // TODO: Handle FLAC pictures when bindings are available + } + + pub fn set_mp4(&mut self, tag: &mp4::MP4Tag) { + let map = tag.item_map().to_hashmap(); + + for (key, item) in map { + if key == "covr" { + if let Some(mp4::MP4Data::CoverArtList(cover_list)) = item.data() { + let covers = cover_list.to_vec(); + // Prefer PNG/JPEG covers + let preferred_cover = covers.iter().find(|c| { + matches!(c.format(), mp4::CoverArtFormat::PNG | mp4::CoverArtFormat::JPEG) + }); + + if let Some(cover) = preferred_cover { + self.cover = Some(cover.data().to_vec()); + } else if let Some(first_cover) = covers.first() { + self.cover = Some(first_cover.data().to_vec()); + } + continue; + } + } + + if let Some(data) = item.data() { + match data { + mp4::MP4Data::StringList(list) => { + let values: Vec = list.to_vec().into_iter().map(|s| s.to_string()).collect(); + if key.starts_with("----") { + if let Some(split_idx) = key.find(':') { + let (atom_name, atom_desc) = key.split_at(split_idx); + let atom_desc = &atom_desc[1..]; // Skip the colon + for value in values { + self.mp4.add_combined(atom_name, atom_desc, value); + } + } + } else { + self.mp4.add_id_list(key, values); + } + } + mp4::MP4Data::Int(v) => self.mp4.add_id(key, v.to_string()), + mp4::MP4Data::UInt(v) => self.mp4.add_id(key, v.to_string()), + mp4::MP4Data::LongLong(v) => self.mp4.add_id(key, v.to_string()), + mp4::MP4Data::IntPair(pair) => { + if let Some((first, second)) = pair.to_tuple() { + self.mp4.add_id(key, format!("{}/{}", first, second)); + } + } + _ => continue, + } + } + } + } + + pub fn set_properties(&mut self, properties: audioproperties::AudioProperties<'file_ref>) { + self.properties = Some(properties); + } + + pub fn build(&self) -> JObject { + // Create Properties object + let properties_class = self.env.borrow_mut().find_class("org/oxycblt/musikr/metadata/Properties").unwrap(); + let properties = if let Some(props) = &self.properties { + let mime_type = self.mime_type.as_deref().unwrap_or("").to_string(); + let j_mime_type = self.env.borrow().new_string(mime_type).unwrap(); + + self.env.borrow_mut().new_object( + properties_class, + "(Ljava/lang/String;JII)V", + &[ + JValueGen::from(&j_mime_type), + (props.length_in_milliseconds() as jlong).into(), + (props.bitrate() as i32).into(), + (props.sample_rate() as i32).into(), + ], + ).unwrap() + } else { + let empty_mime = self.env.borrow().new_string("").unwrap(); + self.env.borrow_mut().new_object( + properties_class, + "(Ljava/lang/String;JII)V", + &[JValueGen::from(&empty_mime), 0i64.into(), 0i32.into(), 0i32.into()], + ).unwrap() + }; + + // Create cover byte array if present + let cover_array = if let Some(cover_data) = &self.cover { + let array = self.env.borrow().new_byte_array(cover_data.len() as i32).unwrap(); + self.env.borrow().set_byte_array_region(&array, 0, bytemuck::cast_slice(cover_data)).unwrap(); + array.into() + } else { + JObject::null() + }; + + // Create Metadata object + let metadata_class = self.env.borrow_mut().find_class("org/oxycblt/musikr/metadata/Metadata").unwrap(); + self.env.borrow_mut().new_object( + metadata_class, + "(Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;[BLorg/oxycblt/musikr/metadata/Properties;)V", + &[ + JValueGen::from(&self.id3v2.get_object()), + JValueGen::from(&self.xiph.get_object()), + JValueGen::from(&self.mp4.get_object()), + JValueGen::from(&cover_array), + JValueGen::from(&properties), + ], + ).unwrap() + } +} diff --git a/musikr/src/main/jni/src/lib.rs b/musikr/src/main/jni/src/lib.rs index 4c27052fc..545930172 100644 --- a/musikr/src/main/jni/src/lib.rs +++ b/musikr/src/main/jni/src/lib.rs @@ -6,9 +6,12 @@ use std::rc::Rc; mod jstream; mod taglib; +mod tagmap; +mod jbuilder; use jstream::JInputStream; use taglib::file_ref::FileRef; +use jbuilder::JMetadataBuilder; type SharedEnv<'local> = Rc>>; diff --git a/musikr/src/main/jni/src/taglib/bridge.rs b/musikr/src/main/jni/src/taglib/bridge.rs index f21193892..8bff301e7 100644 --- a/musikr/src/main/jni/src/taglib/bridge.rs +++ b/musikr/src/main/jni/src/taglib/bridge.rs @@ -171,6 +171,8 @@ mod bridge_impl { #[cxx_name = "Frame"] type CPPID3v2Frame; #[namespace = "taglib_shim"] + fn Frame_id(frame: &CPPID3v2Frame) -> UniquePtr; + #[namespace = "taglib_shim"] unsafe fn Frame_asTextIdentification( frame: *const CPPID3v2Frame, ) -> *const CPPID3v2TextIdentificationFrame; diff --git a/musikr/src/main/jni/src/taglib/id3v2.rs b/musikr/src/main/jni/src/taglib/id3v2.rs index e0d788338..a398f60d2 100644 --- a/musikr/src/main/jni/src/taglib/id3v2.rs +++ b/musikr/src/main/jni/src/taglib/id3v2.rs @@ -2,7 +2,7 @@ use super::bridge::{ self, CPPID3v2AttachedPictureFrame, CPPID3v2Frame, CPPID3v2FrameList, CPPID3v2Tag, CPPID3v2TextIdentificationFrame, CPPID3v2UserTextIdentificationFrame, CPPStringList, CPPByteVector, }; -use super::tk::{ByteVector, StringList, OwnedByteVector, OwnedStringList}; +use super::tk::{self, ByteVector, StringList, OwnedByteVector, OwnedStringList}; use super::this::{OwnedThis, RefThisMut, RefThis, This}; pub struct ID3v2Tag<'file_ref> { @@ -53,6 +53,12 @@ impl<'file_ref> Frame<'file_ref> { Self { this } } + pub fn id(&self) -> tk::OwnedByteVector<'file_ref> { + let id = bridge::Frame_id(self.this.as_ref()); + let this = unsafe { OwnedThis::new(id).unwrap() }; + ByteVector::new(this) + } + pub fn as_text_identification(&mut self) -> Option> { let frame = unsafe { bridge::Frame_asTextIdentification(self.this.ptr()) }; let frame_ref = unsafe { frame.as_ref() }; diff --git a/musikr/src/main/jni/src/taglib/iostream.rs b/musikr/src/main/jni/src/taglib/iostream.rs index f6c506554..5a256973f 100644 --- a/musikr/src/main/jni/src/taglib/iostream.rs +++ b/musikr/src/main/jni/src/taglib/iostream.rs @@ -54,6 +54,7 @@ impl<'io_stream> DynIOStream<'io_stream> { pub fn write(&mut self, data: &[u8]) { self.0.write_all(data).unwrap(); } + pub fn seek(&mut self, offset: i64, whence: i32) { let pos = match whence { 0 => SeekFrom::Start(offset as u64), diff --git a/musikr/src/main/jni/src/taglib/mod.rs b/musikr/src/main/jni/src/taglib/mod.rs index 1f7e5a361..701e75956 100644 --- a/musikr/src/main/jni/src/taglib/mod.rs +++ b/musikr/src/main/jni/src/taglib/mod.rs @@ -1,5 +1,5 @@ -mod bridge; -mod this; +pub mod bridge; +pub mod this; pub mod iostream; pub mod file_ref; pub mod file; diff --git a/musikr/src/main/jni/src/taglib/tk.rs b/musikr/src/main/jni/src/taglib/tk.rs index 6509a5a23..457571ea2 100644 --- a/musikr/src/main/jni/src/taglib/tk.rs +++ b/musikr/src/main/jni/src/taglib/tk.rs @@ -5,7 +5,7 @@ use std::marker::PhantomData; use std::pin::Pin; use std::{ffi::CStr, string::ToString}; -pub(super) struct String<'file_ref, T: This<'file_ref, CPPString>> { +pub struct String<'file_ref, T: This<'file_ref, CPPString>> { _data: PhantomData<&'file_ref ()>, this: T, } @@ -39,7 +39,7 @@ impl<'file_ref, T: This<'file_ref, CPPString>> ToString for String<'file_ref, T> pub type OwnedString<'file_ref> = String<'file_ref, OwnedThis<'file_ref, CPPString>>; pub type RefString<'file_ref> = String<'file_ref, RefThis<'file_ref, CPPString>>; pub type RefStringMut<'file_ref> = String<'file_ref, RefThisMut<'file_ref, CPPString>>; -pub(super) struct StringList<'file_ref, T: This<'file_ref, CPPStringList>> { +pub struct StringList<'file_ref, T: This<'file_ref, CPPStringList>> { _data: PhantomData<&'file_ref ()>, this: T, } @@ -89,6 +89,15 @@ impl<'file_ref, T: This<'file_ref, CPPByteVector>> ByteVector<'file_ref, T> { std::slice::from_raw_parts(data, size).to_vec() } } + + pub fn to_string_lossy(&self) -> std::string::String { + let this = self.this.as_ref(); + let size = this.size().try_into().unwrap(); + let data = this.data(); + let data: *const u8 = data as *const u8; + let slice = unsafe { std::slice::from_raw_parts(data, size) }; + std::string::String::from_utf8_lossy(slice).to_string() + } } pub type OwnedByteVector<'file_ref> = ByteVector<'file_ref, OwnedThis<'file_ref, CPPByteVector>>; diff --git a/musikr/src/main/jni/src/tagmap.rs b/musikr/src/main/jni/src/tagmap.rs new file mode 100644 index 000000000..023ce7e93 --- /dev/null +++ b/musikr/src/main/jni/src/tagmap.rs @@ -0,0 +1,74 @@ +use std::collections::HashMap; +use std::rc::Rc; +use std::cell::RefCell; +use jni::{objects::{JObject, JValueGen}, JNIEnv}; + +pub struct JTagMap<'local> { + env: Rc>>, + map: HashMap>, +} + +impl<'local> JTagMap<'local> { + pub fn new(env: Rc>>) -> Self { + Self { + env, + map: HashMap::new(), + } + } + + pub fn add_id(&mut self, id: impl Into, value: impl Into) { + let id = id.into(); + let value = value.into(); + self.map.entry(id).or_default().push(value); + } + + pub fn add_id_list(&mut self, id: impl Into, values: Vec) { + let id = id.into(); + self.map.entry(id).or_default().extend(values); + } + + pub fn add_combined(&mut self, id: impl Into, description: impl Into, value: impl Into) { + let id = id.into(); + let description = description.into(); + let value = value.into(); + let combined_key = format!("{}:{}", id, description); + self.map.entry(combined_key).or_default().push(value); + } + + pub fn get_object(&self) -> JObject { + let map_class = self.env.borrow_mut().find_class("java/util/HashMap").unwrap(); + let map = self.env.borrow_mut().new_object(&map_class, "()V", &[]).unwrap(); + + for (key, values) in &self.map { + let j_key = self.env.borrow().new_string(key).unwrap(); + let j_values: Vec = values + .iter() + .map(|v| self.env.borrow().new_string(v).unwrap().into()) + .collect(); + + // Create ArrayList for values + let array_list_class = self.env.borrow_mut().find_class("java/util/ArrayList").unwrap(); + let array_list = self.env.borrow_mut().new_object(array_list_class, "()V", &[]).unwrap(); + + for value in j_values { + self.env.borrow_mut() + .call_method( + &array_list, + "add", + "(Ljava/lang/Object;)Z", + &[JValueGen::from(&value)] + ).unwrap(); + } + + self.env.borrow_mut() + .call_method( + &map, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + &[JValueGen::from(&j_key), JValueGen::from(&array_list)] + ).unwrap(); + } + + map + } +} \ No newline at end of file