musikr: add metadata builders

This commit is contained in:
Alexander Capehart 2025-02-17 14:24:33 -07:00
parent 1cb2f5026f
commit 6385928150
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 327 additions and 5 deletions

View file

@ -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",

View file

@ -8,6 +8,7 @@ name = "metadatajni"
crate-type = ["cdylib"]
[dependencies]
bytemuck = "1.21.0"
cxx = "1.0.137"
jni = "0.21.1"

View file

@ -25,6 +25,10 @@ namespace taglib_shim {
return dynamic_cast<const TagLib::ID3v2::AttachedPictureFrame*>(frame);
}
std::unique_ptr<TagLib::ByteVector> Frame_id(const TagLib::ID3v2::Frame& frame) {
return std::make_unique<TagLib::ByteVector>(frame.frameID());
}
std::unique_ptr<TagLib::ByteVector> AttachedPictureFrame_picture(const TagLib::ID3v2::AttachedPictureFrame& frame) {
return std::make_unique<TagLib::ByteVector>(frame.picture());
}

View file

@ -20,6 +20,7 @@ namespace taglib_shim {
std::unique_ptr<std::vector<FramePointer>> FrameList_to_vector(const TagLib::ID3v2::FrameList& list);
// Frame type checking and casting
std::unique_ptr<TagLib::ByteVector> 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);

View file

@ -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<RefCell<JNIEnv<'local>>>,
id3v2: JTagMap<'local>,
xiph: JTagMap<'local>,
mp4: JTagMap<'local>,
cover: Option<Vec<u8>>,
properties: Option<audioproperties::AudioProperties<'file_ref>>,
mime_type: Option<String>,
}
impl<'local, 'file_ref> JMetadataBuilder<'local, 'file_ref> {
pub fn new(env: Rc<RefCell<JNIEnv<'local>>>) -> 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<String>) {
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<String> = 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<String> = 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<String> = 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()
}
}

View file

@ -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<RefCell<JNIEnv<'local>>>;

View file

@ -171,6 +171,8 @@ mod bridge_impl {
#[cxx_name = "Frame"]
type CPPID3v2Frame;
#[namespace = "taglib_shim"]
fn Frame_id(frame: &CPPID3v2Frame) -> UniquePtr<CPPByteVector>;
#[namespace = "taglib_shim"]
unsafe fn Frame_asTextIdentification(
frame: *const CPPID3v2Frame,
) -> *const CPPID3v2TextIdentificationFrame;

View file

@ -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<TextIdentificationFrame<'file_ref>> {
let frame = unsafe { bridge::Frame_asTextIdentification(self.this.ptr()) };
let frame_ref = unsafe { frame.as_ref() };

View file

@ -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),

View file

@ -1,5 +1,5 @@
mod bridge;
mod this;
pub mod bridge;
pub mod this;
pub mod iostream;
pub mod file_ref;
pub mod file;

View file

@ -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>>;

View file

@ -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<RefCell<JNIEnv<'local>>>,
map: HashMap<String, Vec<String>>,
}
impl<'local> JTagMap<'local> {
pub fn new(env: Rc<RefCell<JNIEnv<'local>>>) -> Self {
Self {
env,
map: HashMap::new(),
}
}
pub fn add_id(&mut self, id: impl Into<String>, value: impl Into<String>) {
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<String>, values: Vec<String>) {
let id = id.into();
self.map.entry(id).or_default().extend(values);
}
pub fn add_combined(&mut self, id: impl Into<String>, description: impl Into<String>, value: impl Into<String>) {
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<JObject> = 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
}
}