musikr: add metadata builders
This commit is contained in:
parent
1cb2f5026f
commit
6385928150
12 changed files with 327 additions and 5 deletions
7
musikr/src/main/jni/Cargo.lock
generated
7
musikr/src/main/jni/Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -8,6 +8,7 @@ name = "metadatajni"
|
|||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
bytemuck = "1.21.0"
|
||||
cxx = "1.0.137"
|
||||
jni = "0.21.1"
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
214
musikr/src/main/jni/src/jbuilder.rs
Normal file
214
musikr/src/main/jni/src/jbuilder.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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>>>;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() };
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
mod bridge;
|
||||
mod this;
|
||||
pub mod bridge;
|
||||
pub mod this;
|
||||
pub mod iostream;
|
||||
pub mod file_ref;
|
||||
pub mod 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>>;
|
||||
|
|
74
musikr/src/main/jni/src/tagmap.rs
Normal file
74
musikr/src/main/jni/src/tagmap.rs
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue