all: format

This commit is contained in:
Alexander Capehart 2024-12-16 13:34:49 -05:00
parent 18c5b3618c
commit ed102d3414
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 474 additions and 345 deletions

View file

@ -22,12 +22,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.list.DiscDivider import org.oxycblt.auxio.detail.list.DiscDivider
import org.oxycblt.auxio.detail.list.DiscHeader import org.oxycblt.auxio.detail.list.DiscHeader
@ -302,8 +300,8 @@ constructor(
} }
/** /**
* Set a new [currentSong] from it's [Music.UID]. [currentSong] will * Set a new [currentSong] from it's [Music.UID]. [currentSong] will be updated to align with
* be updated to align with the new [Song]. * the new [Song].
* *
* @param uid The UID of the [Song] to load. Must be valid. * @param uid The UID of the [Song] to load. Must be valid.
*/ */
@ -504,9 +502,7 @@ constructor(
}) })
} }
private fun refreshAudioInfo(song: Song) { private fun refreshAudioInfo(song: Song) {}
}
private inline fun <T : MusicParent> refreshDetail( private inline fun <T : MusicParent> refreshDetail(
detail: Detail<T>?, detail: Detail<T>?,

View file

@ -20,7 +20,6 @@ package org.oxycblt.auxio.detail
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.format.Formatter
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
@ -29,13 +28,8 @@ import androidx.navigation.fragment.navArgs
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSongDetailBinding import org.oxycblt.auxio.databinding.DialogSongDetailBinding
import org.oxycblt.auxio.detail.list.SongProperty
import org.oxycblt.auxio.detail.list.SongPropertyAdapter import org.oxycblt.auxio.detail.list.SongPropertyAdapter
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.music.resolve
import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.playback.replaygain.formatDb
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.auxio.util.concatLocalized
@ -75,59 +69,70 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment<DialogSongDetailBindi
} }
private fun updateSong(song: Song?) { private fun updateSong(song: Song?) {
// if (song == null) { // if (song == null) {
L.d("No song to show, navigating away") L.d("No song to show, navigating away")
findNavController().navigateUp() findNavController().navigateUp()
return return
// } // }
// //
// if (info != null) { // if (info != null) {
// val context = requireContext() // val context = requireContext()
// detailAdapter.update( // detailAdapter.update(
// buildList { // buildList {
// add(SongProperty(R.string.lbl_name, song.zipName(context))) // add(SongProperty(R.string.lbl_name, song.zipName(context)))
// add(SongProperty(R.string.lbl_album, song.album.zipName(context))) // add(SongProperty(R.string.lbl_album, song.album.zipName(context)))
// add(SongProperty(R.string.lbl_artists, song.artists.zipNames(context))) // add(SongProperty(R.string.lbl_artists,
// add(SongProperty(R.string.lbl_genres, song.genres.resolveNames(context))) // song.artists.zipNames(context)))
// song.date?.let { add(SongProperty(R.string.lbl_date, it.resolve(context))) } // add(SongProperty(R.string.lbl_genres,
// song.track?.let { // song.genres.resolveNames(context)))
// add(SongProperty(R.string.lbl_track, getString(R.string.fmt_number, it))) // song.date?.let { add(SongProperty(R.string.lbl_date,
// } // it.resolve(context))) }
// song.disc?.let { // song.track?.let {
// val formattedNumber = getString(R.string.fmt_number, it.number) // add(SongProperty(R.string.lbl_track,
// val zipped = // getString(R.string.fmt_number, it)))
// if (it.name != null) { // }
// getString(R.string.fmt_zipped_names, formattedNumber, it.name) // song.disc?.let {
// } else { // val formattedNumber = getString(R.string.fmt_number, it.number)
// formattedNumber // val zipped =
// } // if (it.name != null) {
// add(SongProperty(R.string.lbl_disc, zipped)) // getString(R.string.fmt_zipped_names, formattedNumber,
// } // it.name)
// add(SongProperty(R.string.lbl_path, song.path.resolve(context))) // } else {
// // info.format.resolveName(context)?.let { // formattedNumber
// // add(SongProperty(R.string.lbl_format, it)) // }
// // } // add(SongProperty(R.string.lbl_disc, zipped))
// add( // }
// SongProperty( // add(SongProperty(R.string.lbl_path, song.path.resolve(context)))
// R.string.lbl_size, Formatter.formatFileSize(context, song.size))) // // info.format.resolveName(context)?.let {
// add(SongProperty(R.string.lbl_duration, song.durationMs.formatDurationMs(true))) // // add(SongProperty(R.string.lbl_format, it))
// info.bitrateKbps?.let { // // }
// add(SongProperty(R.string.lbl_bitrate, getString(R.string.fmt_bitrate, it))) // add(
// } // SongProperty(
// info.sampleRateHz?.let { // R.string.lbl_size, Formatter.formatFileSize(context,
// add( // song.size)))
// SongProperty( // add(SongProperty(R.string.lbl_duration,
// R.string.lbl_sample_rate, getString(R.string.fmt_sample_rate, it))) // song.durationMs.formatDurationMs(true)))
// } // info.bitrateKbps?.let {
// song.replayGainAdjustment.track?.let { // add(SongProperty(R.string.lbl_bitrate,
// add(SongProperty(R.string.lbl_replaygain_track, it.formatDb(context))) // getString(R.string.fmt_bitrate, it)))
// } // }
// song.replayGainAdjustment.album?.let { // info.sampleRateHz?.let {
// add(SongProperty(R.string.lbl_replaygain_album, it.formatDb(context))) // add(
// } // SongProperty(
// }, // R.string.lbl_sample_rate,
// UpdateInstructions.Replace(0)) // getString(R.string.fmt_sample_rate, it)))
// } // }
// song.replayGainAdjustment.track?.let {
// add(SongProperty(R.string.lbl_replaygain_track,
// it.formatDb(context)))
// }
// song.replayGainAdjustment.album?.let {
// add(SongProperty(R.string.lbl_replaygain_album,
// it.formatDb(context)))
// }
// },
// UpdateInstructions.Replace(0))
// }
} }
private fun <T : Music> T.zipName(context: Context): String { private fun <T : Music> T.zipName(context: Context): String {

View file

@ -1,105 +1,127 @@
// /*
// Created by oxycblt on 12/12/24. * Copyright (c) 2024 Auxio Project
// * JVMInputStream.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JVMInputStream.h" #include "JVMInputStream.h"
#include <cmath> #include <cmath>
// TODO: Handle stream exceptions // TODO: Handle stream exceptions
JVMInputStream::JVMInputStream(JNIEnv *env, jobject inputStream) : JVMInputStream::JVMInputStream(JNIEnv *env, jobject inputStream)
env(env), inputStream(inputStream) { : env(env), inputStream(inputStream) {
if (!env->IsInstanceOf(inputStream, env->FindClass("org/oxycblt/ktaglib/NativeInputStream"))) { if (!env->IsInstanceOf(
throw std::runtime_error("oStream is not an instance of TagLibOStream"); inputStream,
} env->FindClass("org/oxycblt/ktaglib/NativeInputStream"))) {
jclass inputStreamClass = env->FindClass("org/oxycblt/ktaglib/NativeInputStream"); throw std::runtime_error("oStream is not an instance of TagLibOStream");
inputStreamNameMethod = env->GetMethodID(inputStreamClass, "name", "()Ljava/lang/String;"); }
inputStreamReadBlockMethod = env->GetMethodID(inputStreamClass, "readBlock", "(J)[B"); jclass inputStreamClass =
inputStreamIsOpenMethod = env->GetMethodID(inputStreamClass, "isOpen", "()Z"); env->FindClass("org/oxycblt/ktaglib/NativeInputStream");
inputStreamSeekFromBeginningMethod = env->GetMethodID(inputStreamClass, "seekFromBeginning", inputStreamNameMethod =
"(J)V"); env->GetMethodID(inputStreamClass, "name", "()Ljava/lang/String;");
inputStreamSeekFromCurrentMethod = env->GetMethodID(inputStreamClass, "seekFromCurrent", inputStreamReadBlockMethod =
"(J)V"); env->GetMethodID(inputStreamClass, "readBlock", "(J)[B");
inputStreamSeekFromEndMethod = env->GetMethodID(inputStreamClass, "seekFromEnd", "(J)V"); inputStreamIsOpenMethod = env->GetMethodID(inputStreamClass, "isOpen", "()Z");
inputStreamClearMethod = env->GetMethodID(inputStreamClass, "clear", "()V"); inputStreamSeekFromBeginningMethod =
inputStreamTellMethod = env->GetMethodID(inputStreamClass, "tell", "()J"); env->GetMethodID(inputStreamClass, "seekFromBeginning", "(J)V");
inputStreamLengthMethod = env->GetMethodID(inputStreamClass, "length", "()J"); inputStreamSeekFromCurrentMethod =
env->DeleteLocalRef(inputStreamClass); env->GetMethodID(inputStreamClass, "seekFromCurrent", "(J)V");
inputStreamSeekFromEndMethod =
env->GetMethodID(inputStreamClass, "seekFromEnd", "(J)V");
inputStreamClearMethod = env->GetMethodID(inputStreamClass, "clear", "()V");
inputStreamTellMethod = env->GetMethodID(inputStreamClass, "tell", "()J");
inputStreamLengthMethod = env->GetMethodID(inputStreamClass, "length", "()J");
env->DeleteLocalRef(inputStreamClass);
} }
JVMInputStream::~JVMInputStream() { JVMInputStream::~JVMInputStream() {
// The implicit assumption is that inputStream is managed by the owner, // The implicit assumption is that inputStream is managed by the owner,
// so we don't need to delete any references here // so we don't need to delete any references here
} }
TagLib::FileName JVMInputStream::name() const { TagLib::FileName JVMInputStream::name() const {
auto name = (jstring) env->CallObjectMethod(inputStream, inputStreamNameMethod); auto name =
const char *nameChars = env->GetStringUTFChars(name, nullptr); (jstring)env->CallObjectMethod(inputStream, inputStreamNameMethod);
auto fileName = TagLib::FileName(nameChars); const char *nameChars = env->GetStringUTFChars(name, nullptr);
env->ReleaseStringUTFChars(name, nameChars); auto fileName = TagLib::FileName(nameChars);
return fileName; env->ReleaseStringUTFChars(name, nameChars);
return fileName;
} }
TagLib::ByteVector JVMInputStream::readBlock(size_t length) { TagLib::ByteVector JVMInputStream::readBlock(size_t length) {
auto data = (jbyteArray) env->CallObjectMethod(inputStream, inputStreamReadBlockMethod, length); auto data = (jbyteArray)env->CallObjectMethod(
jsize dataLength = env->GetArrayLength(data); inputStream, inputStreamReadBlockMethod, length);
auto dataBytes = env->GetByteArrayElements(data, nullptr); jsize dataLength = env->GetArrayLength(data);
TagLib::ByteVector byteVector(reinterpret_cast<const char *>(dataBytes), dataLength); auto dataBytes = env->GetByteArrayElements(data, nullptr);
env->ReleaseByteArrayElements(data, dataBytes, JNI_ABORT); TagLib::ByteVector byteVector(reinterpret_cast<const char *>(dataBytes),
return byteVector; dataLength);
env->ReleaseByteArrayElements(data, dataBytes, JNI_ABORT);
return byteVector;
} }
void JVMInputStream::writeBlock(const TagLib::ByteVector &data) { void JVMInputStream::writeBlock(const TagLib::ByteVector &data) {
throw std::runtime_error("Not implemented"); throw std::runtime_error("Not implemented");
} }
void JVMInputStream::insert(const TagLib::ByteVector &data, void JVMInputStream::insert(const TagLib::ByteVector &data,
TagLib::offset_t start, size_t replace) { TagLib::offset_t start, size_t replace) {
throw std::runtime_error("Not implemented"); throw std::runtime_error("Not implemented");
} }
void JVMInputStream::removeBlock(TagLib::offset_t start, size_t length) { void JVMInputStream::removeBlock(TagLib::offset_t start, size_t length) {
throw std::runtime_error("Not implemented"); throw std::runtime_error("Not implemented");
} }
bool JVMInputStream::readOnly() const { bool JVMInputStream::readOnly() const { return true; }
return true;
}
bool JVMInputStream::isOpen() const { bool JVMInputStream::isOpen() const {
return env->CallBooleanMethod(inputStream, inputStreamIsOpenMethod); return env->CallBooleanMethod(inputStream, inputStreamIsOpenMethod);
} }
void JVMInputStream::seek(TagLib::offset_t offset, Position p) { void JVMInputStream::seek(TagLib::offset_t offset, Position p) {
auto joffset = static_cast<jlong>(std::llround(offset)); auto joffset = static_cast<jlong>(std::llround(offset));
switch (p) { switch (p) {
case Beginning: case Beginning:
env->CallVoidMethod(inputStream, inputStreamSeekFromBeginningMethod, joffset); env->CallVoidMethod(inputStream, inputStreamSeekFromBeginningMethod,
break; joffset);
case Current: break;
env->CallVoidMethod(inputStream, inputStreamSeekFromCurrentMethod, joffset); case Current:
break; env->CallVoidMethod(inputStream, inputStreamSeekFromCurrentMethod, joffset);
case End: break;
env->CallVoidMethod(inputStream, inputStreamSeekFromEndMethod, joffset); case End:
break; env->CallVoidMethod(inputStream, inputStreamSeekFromEndMethod, joffset);
} break;
}
} }
void JVMInputStream::clear() { void JVMInputStream::clear() {
env->CallVoidMethod(inputStream, inputStreamClearMethod); env->CallVoidMethod(inputStream, inputStreamClearMethod);
} }
TagLib::offset_t JVMInputStream::tell() const { TagLib::offset_t JVMInputStream::tell() const {
jlong jposition = env->CallLongMethod(inputStream, inputStreamTellMethod); jlong jposition = env->CallLongMethod(inputStream, inputStreamTellMethod);
return static_cast<TagLib::offset_t>(jposition); return static_cast<TagLib::offset_t>(jposition);
} }
TagLib::offset_t JVMInputStream::length() { TagLib::offset_t JVMInputStream::length() {
jlong jlength = env->CallLongMethod(inputStream, inputStreamLengthMethod); jlong jlength = env->CallLongMethod(inputStream, inputStreamLengthMethod);
return static_cast<TagLib::offset_t>(jlength); return static_cast<TagLib::offset_t>(jlength);
} }
void JVMInputStream::truncate(TagLib::offset_t length) { void JVMInputStream::truncate(TagLib::offset_t length) {
throw std::runtime_error("Not implemented"); throw std::runtime_error("Not implemented");
} }

View file

@ -1,134 +1,161 @@
// /*
// Created by oxycblt on 12/12/24. * Copyright (c) 2024 Auxio Project
// * JVMMetadataBuilder.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JVMMetadataBuilder.h" #include "JVMMetadataBuilder.h"
#include <taglib/mp4tag.h> #include <taglib/mp4tag.h>
#include <taglib/textidentificationframe.h> #include <taglib/textidentificationframe.h>
JVMMetadataBuilder::JVMMetadataBuilder(JNIEnv *env) : env(env), id3v2(env), xiph(env), mp4(env), JVMMetadataBuilder::JVMMetadataBuilder(JNIEnv *env)
cover(), properties(nullptr) {} : env(env), id3v2(env), xiph(env), mp4(env), cover(), properties(nullptr) {}
void JVMMetadataBuilder::setMimeType(const std::string_view type) { void JVMMetadataBuilder::setMimeType(const std::string_view type) {
this->mimeType = type; this->mimeType = type;
} }
void JVMMetadataBuilder::setId3v2(const TagLib::ID3v2::Tag &tag) { void JVMMetadataBuilder::setId3v2(const TagLib::ID3v2::Tag &tag) {
for (auto frame: tag.frameList()) { for (auto frame : tag.frameList()) {
if (auto txxxFrame = dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame *>(frame)) { if (auto txxxFrame =
TagLib::StringList frameText = txxxFrame->fieldList(); dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame *>(frame)) {
// Frame text starts with the description then the remaining values TagLib::StringList frameText = txxxFrame->fieldList();
auto begin = frameText.begin(); // Frame text starts with the description then the remaining values
TagLib::String key = TagLib::String(frame->frameID()) + ":" + begin->upper(); auto begin = frameText.begin();
frameText.erase(begin); TagLib::String key =
id3v2.add(key, frameText); TagLib::String(frame->frameID()) + ":" + begin->upper();
} else if (auto textFrame = dynamic_cast<TagLib::ID3v2::TextIdentificationFrame *>(frame)) { frameText.erase(begin);
TagLib::String key = frame->frameID(); id3v2.add(key, frameText);
TagLib::StringList frameText = textFrame->fieldList(); } else if (auto textFrame =
id3v2.add(key, frameText); dynamic_cast<TagLib::ID3v2::TextIdentificationFrame *>(
} else { frame)) {
continue; TagLib::String key = frame->frameID();
} TagLib::StringList frameText = textFrame->fieldList();
id3v2.add(key, frameText);
} else {
continue;
} }
}
} }
void JVMMetadataBuilder::setXiph(const TagLib::Ogg::XiphComment &tag) { void JVMMetadataBuilder::setXiph(const TagLib::Ogg::XiphComment &tag) {
for (auto field: tag.fieldListMap()) { for (auto field : tag.fieldListMap()) {
auto key = field.first.upper(); auto key = field.first.upper();
auto values = field.second; auto values = field.second;
xiph.add(key, values); xiph.add(key, values);
} }
} }
void JVMMetadataBuilder::setMp4(const TagLib::MP4::Tag &tag) { void JVMMetadataBuilder::setMp4(const TagLib::MP4::Tag &tag) {
for (auto item: tag.itemMap()) { for (auto item : tag.itemMap()) {
auto itemName = TagLib::String(item.first); auto itemName = TagLib::String(item.first);
auto itemValue = item.second; auto itemValue = item.second;
auto type = itemValue.type(); auto type = itemValue.type();
// TODO: Handle internal atoms // TODO: Handle internal atoms
// Only read out the atoms for the reasonable tags we are expecting. // Only read out the atoms for the reasonable tags we are expecting.
// None of the crazy binary atoms. // None of the crazy binary atoms.
if (type == TagLib::MP4::Item::Type::StringList) { if (type == TagLib::MP4::Item::Type::StringList) {
auto value = itemValue.toStringList(); auto value = itemValue.toStringList();
mp4.add(itemName, value); mp4.add(itemName, value);
return; return;
}
// Assume that taggers will be unhinged and store track numbers
// as ints, uints, or longs.
if (type == TagLib::MP4::Item::Type::Int) {
auto value = std::to_string(itemValue.toInt());
id3v2.add(itemName, value);
return;
}
if (type == TagLib::MP4::Item::Type::UInt) {
auto value = std::to_string(itemValue.toUInt());
id3v2.add(itemName, value);
return;
}
if (type == TagLib::MP4::Item::Type::LongLong) {
auto value = std::to_string(itemValue.toLongLong());
id3v2.add(itemName, value);
return;
}
if (type == TagLib::MP4::Item::Type::IntPair) {
// It's inefficient going from the integer representation back into
// a string, but I fully expect taggers to just write "NN/TT" strings
// anyway, and musikr doesn't have to do as much fiddly variant handling.
auto value = std::to_string(itemValue.toIntPair().first) + "/" +
std::to_string(itemValue.toIntPair().second);
id3v2.add(itemName, value);
return;
}
// Nothing else makes sense to handle as far as I can tell.
} }
// Assume that taggers will be unhinged and store track numbers
// as ints, uints, or longs.
if (type == TagLib::MP4::Item::Type::Int) {
auto value = std::to_string(itemValue.toInt());
id3v2.add(itemName, value);
return;
}
if (type == TagLib::MP4::Item::Type::UInt) {
auto value = std::to_string(itemValue.toUInt());
id3v2.add(itemName, value);
return;
}
if (type == TagLib::MP4::Item::Type::LongLong) {
auto value = std::to_string(itemValue.toLongLong());
id3v2.add(itemName, value);
return;
}
if (type == TagLib::MP4::Item::Type::IntPair) {
// It's inefficient going from the integer representation back into
// a string, but I fully expect taggers to just write "NN/TT" strings
// anyway, and musikr doesn't have to do as much fiddly variant handling.
auto value = std::to_string(itemValue.toIntPair().first) + "/" +
std::to_string(itemValue.toIntPair().second);
id3v2.add(itemName, value);
return;
}
// Nothing else makes sense to handle as far as I can tell.
}
} }
void JVMMetadataBuilder::setCover(const TagLib::List<TagLib::VariantMap> covers) { void JVMMetadataBuilder::setCover(
if (covers.isEmpty()) { const TagLib::List<TagLib::VariantMap> covers) {
return; if (covers.isEmpty()) {
return;
}
// Find the cover with a "front cover" type
for (auto cover : covers) {
auto type = cover["pictureType"].toString();
if (type == "Front Cover") {
this->cover = cover["data"].toByteVector();
return;
} }
// Find the cover with a "front cover" type }
for (auto cover: covers) { // No front cover, just pick first.
auto type = cover["pictureType"].toString(); // TODO: Consider having cascading fallbacks to increasingly less
if (type == "Front Cover") { // relevant covers perhaps
this->cover = cover["data"].toByteVector(); this->cover = covers.front()["data"].toByteVector();
return;
}
}
// No front cover, just pick first.
// TODO: Consider having cascading fallbacks to increasingly less
// relevant covers perhaps
this->cover = covers.front()["data"].toByteVector();
} }
void JVMMetadataBuilder::setProperties(TagLib::AudioProperties *properties) { void JVMMetadataBuilder::setProperties(TagLib::AudioProperties *properties) {
this->properties = properties; this->properties = properties;
} }
jobject JVMMetadataBuilder::build() { jobject JVMMetadataBuilder::build() {
jclass propertiesClass = env->FindClass("org/oxycblt/ktaglib/Properties"); jclass propertiesClass = env->FindClass("org/oxycblt/ktaglib/Properties");
jmethodID propertiesInit = env->GetMethodID(propertiesClass, "<init>", "(Ljava/lang/String;JII)V"); jmethodID propertiesInit =
jobject propertiesObj = env->NewObject(propertiesClass, propertiesInit, env->GetMethodID(propertiesClass, "<init>", "(Ljava/lang/String;JII)V");
env->NewStringUTF(mimeType.data()), (jlong) properties->lengthInMilliseconds(), jobject propertiesObj = env->NewObject(
properties->bitrate(), properties->sampleRate()); propertiesClass, propertiesInit, env->NewStringUTF(mimeType.data()),
env->DeleteLocalRef(propertiesClass); (jlong)properties->lengthInMilliseconds(), properties->bitrate(),
properties->sampleRate());
env->DeleteLocalRef(propertiesClass);
jclass metadataClass = env->FindClass("org/oxycblt/ktaglib/Metadata"); jclass metadataClass = env->FindClass("org/oxycblt/ktaglib/Metadata");
jmethodID metadataInit = env->GetMethodID(metadataClass, "<init>", "(Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;[BLorg/oxycblt/ktaglib/Properties;)V"); jmethodID metadataInit =
jobject id3v2Map = id3v2.getObject(); env->GetMethodID(metadataClass, "<init>",
jobject xiphMap = xiph.getObject(); "(Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;[BLorg/"
jobject mp4Map = mp4.getObject(); "oxycblt/ktaglib/Properties;)V");
jbyteArray coverArray = nullptr; jobject id3v2Map = id3v2.getObject();
if (cover.has_value()) { jobject xiphMap = xiph.getObject();
auto coverSize = static_cast<jsize>(cover->size()); jobject mp4Map = mp4.getObject();
coverArray = env->NewByteArray(coverSize); jbyteArray coverArray = nullptr;
env->SetByteArrayRegion(coverArray, 0, coverSize, reinterpret_cast<const jbyte *>(cover->data())); if (cover.has_value()) {
} auto coverSize = static_cast<jsize>(cover->size());
jobject metadataObj = env->NewObject(metadataClass, metadataInit, id3v2Map, xiphMap, mp4Map, coverArray, propertiesObj); coverArray = env->NewByteArray(coverSize);
env->DeleteLocalRef(metadataClass); env->SetByteArrayRegion(coverArray, 0, coverSize,
return metadataObj; reinterpret_cast<const jbyte *>(cover->data()));
}
jobject metadataObj =
env->NewObject(metadataClass, metadataInit, id3v2Map, xiphMap, mp4Map,
coverArray, propertiesObj);
env->DeleteLocalRef(metadataClass);
return metadataObj;
} }

View file

@ -1,70 +1,86 @@
// /*
// Created by oxycblt on 12/12/24. * Copyright (c) 2024 Auxio Project
// * JVMTagMap.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "JVMTagMap.h" #include "JVMTagMap.h"
JVMTagMap::JVMTagMap(JNIEnv *env) : env(env) { JVMTagMap::JVMTagMap(JNIEnv *env) : env(env) {
jclass hashMapClass = env->FindClass("java/util/HashMap"); jclass hashMapClass = env->FindClass("java/util/HashMap");
jmethodID init = env->GetMethodID(hashMapClass, "<init>", "()V"); jmethodID init = env->GetMethodID(hashMapClass, "<init>", "()V");
hashMap = env->NewObject(hashMapClass, init); hashMap = env->NewObject(hashMapClass, init);
hashMapGetMethod = env->GetMethodID(hashMapClass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); hashMapGetMethod = env->GetMethodID(hashMapClass, "get",
hashMapPutMethod = env->GetMethodID(hashMapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); "(Ljava/lang/Object;)Ljava/lang/Object;");
env->DeleteLocalRef(hashMapClass); hashMapPutMethod = env->GetMethodID(
hashMapClass, "put",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
env->DeleteLocalRef(hashMapClass);
jclass arrayListClass = env->FindClass("java/util/ArrayList"); jclass arrayListClass = env->FindClass("java/util/ArrayList");
arrayListInitMethod = env->GetMethodID(arrayListClass, "<init>", "()V"); arrayListInitMethod = env->GetMethodID(arrayListClass, "<init>", "()V");
arrayListAddMethod = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z"); arrayListAddMethod =
env->DeleteLocalRef(arrayListClass); env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
env->DeleteLocalRef(arrayListClass);
} }
JVMTagMap::~JVMTagMap() { JVMTagMap::~JVMTagMap() { env->DeleteLocalRef(hashMap); }
env->DeleteLocalRef(hashMap);
}
void JVMTagMap::add(TagLib::String &key, std::string_view value) { void JVMTagMap::add(TagLib::String &key, std::string_view value) {
jstring jKey = env->NewStringUTF(key.toCString(true)); jstring jKey = env->NewStringUTF(key.toCString(true));
jstring jValue = env->NewStringUTF(value.data()); jstring jValue = env->NewStringUTF(value.data());
// check if theres already a value arraylist in the map // check if theres already a value arraylist in the map
jobject existingValue = env->CallObjectMethod(hashMap, hashMapGetMethod, jKey); jobject existingValue =
// if there is, add to the value to the existing arraylist env->CallObjectMethod(hashMap, hashMapGetMethod, jKey);
if (existingValue != nullptr) { // if there is, add to the value to the existing arraylist
env->CallBooleanMethod(existingValue, arrayListAddMethod, jValue); if (existingValue != nullptr) {
} else { env->CallBooleanMethod(existingValue, arrayListAddMethod, jValue);
// if there isn't, create a new arraylist and add the value to it } else {
jclass arrayListClass = env->FindClass("java/util/ArrayList"); // if there isn't, create a new arraylist and add the value to it
jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod); jclass arrayListClass = env->FindClass("java/util/ArrayList");
env->CallBooleanMethod(arrayList, arrayListAddMethod, jValue); jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod);
env->CallObjectMethod(hashMap, hashMapPutMethod, jKey, arrayList); env->CallBooleanMethod(arrayList, arrayListAddMethod, jValue);
env->DeleteLocalRef(arrayListClass); env->CallObjectMethod(hashMap, hashMapPutMethod, jKey, arrayList);
} env->DeleteLocalRef(arrayListClass);
}
} }
void JVMTagMap::add(TagLib::String &key, TagLib::StringList &value) { void JVMTagMap::add(TagLib::String &key, TagLib::StringList &value) {
jstring jKey = env->NewStringUTF(key.toCString(true)); jstring jKey = env->NewStringUTF(key.toCString(true));
// check if theres already a value arraylist in the map // check if theres already a value arraylist in the map
jobject existingValue = env->CallObjectMethod(hashMap, hashMapGetMethod, jKey); jobject existingValue =
// if there is, add to the value to the existing arraylist env->CallObjectMethod(hashMap, hashMapGetMethod, jKey);
if (existingValue != nullptr) { // if there is, add to the value to the existing arraylist
for (auto &val : value) { if (existingValue != nullptr) {
jstring jValue = env->NewStringUTF(val.toCString(true)); for (auto &val : value) {
env->CallBooleanMethod(existingValue, arrayListAddMethod, jValue); jstring jValue = env->NewStringUTF(val.toCString(true));
} env->CallBooleanMethod(existingValue, arrayListAddMethod, jValue);
} else {
// if there isn't, create a new arraylist and add the value to it
jclass arrayListClass = env->FindClass("java/util/ArrayList");
jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod);
for (auto &val : value) {
jstring jValue = env->NewStringUTF(val.toCString(true));
env->CallBooleanMethod(arrayList, arrayListAddMethod, jValue);
}
env->CallObjectMethod(hashMap, hashMapPutMethod, jKey, arrayList);
env->DeleteLocalRef(arrayListClass);
} }
} else {
// if there isn't, create a new arraylist and add the value to it
jclass arrayListClass = env->FindClass("java/util/ArrayList");
jobject arrayList = env->NewObject(arrayListClass, arrayListInitMethod);
for (auto &val : value) {
jstring jValue = env->NewStringUTF(val.toCString(true));
env->CallBooleanMethod(arrayList, arrayListAddMethod, jValue);
}
env->CallObjectMethod(hashMap, hashMapPutMethod, jKey, arrayList);
env->DeleteLocalRef(arrayListClass);
}
} }
jobject JVMTagMap::getObject() { jobject JVMTagMap::getObject() { return hashMap; }
return hashMap;
}

View file

@ -1,3 +1,21 @@
/*
* Copyright (c) 2024 Auxio Project
* taglib_jni.cpp is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <jni.h> #include <jni.h>
#include <string> #include <string>
@ -5,52 +23,52 @@
#include "JVMMetadataBuilder.h" #include "JVMMetadataBuilder.h"
#include "taglib/fileref.h" #include "taglib/fileref.h"
#include "taglib/mpegfile.h"
#include "taglib/mp4file.h"
#include "taglib/flacfile.h" #include "taglib/flacfile.h"
#include "taglib/mp4file.h"
#include "taglib/mpegfile.h"
#include "taglib/opusfile.h" #include "taglib/opusfile.h"
#include "taglib/vorbisfile.h" #include "taglib/vorbisfile.h"
#include "taglib/wavfile.h" #include "taglib/wavfile.h"
extern "C" JNIEXPORT jobject JNICALL extern "C" JNIEXPORT jobject JNICALL
Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative( Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env,
JNIEnv* env, jobject /* this */,
jobject /* this */, jobject inputStream) {
jobject inputStream) { JVMInputStream stream{env, inputStream};
JVMInputStream stream { env, inputStream }; TagLib::FileRef fileRef{&stream};
TagLib::FileRef fileRef { &stream }; if (fileRef.isNull()) {
if (fileRef.isNull()) { return nullptr;
return nullptr; }
} TagLib::File *file = fileRef.file();
TagLib::File *file = fileRef.file(); JVMMetadataBuilder builder{env};
JVMMetadataBuilder builder { env };
if (auto *mpegFile = dynamic_cast<TagLib::MPEG::File *>(file)) { if (auto *mpegFile = dynamic_cast<TagLib::MPEG::File *>(file)) {
builder.setMimeType("audio/mpeg"); builder.setMimeType("audio/mpeg");
builder.setId3v2(*mpegFile->ID3v2Tag()); builder.setId3v2(*mpegFile->ID3v2Tag());
} else if (auto *mp4File = dynamic_cast<TagLib::MP4::File *>(file)) { } else if (auto *mp4File = dynamic_cast<TagLib::MP4::File *>(file)) {
builder.setMimeType("audio/mp4"); builder.setMimeType("audio/mp4");
builder.setMp4(*mp4File->tag()); builder.setMp4(*mp4File->tag());
} else if (auto *flacFile = dynamic_cast<TagLib::FLAC::File *>(file)) { } else if (auto *flacFile = dynamic_cast<TagLib::FLAC::File *>(file)) {
builder.setMimeType("audio/flac"); builder.setMimeType("audio/flac");
builder.setId3v2(*flacFile->ID3v2Tag()); builder.setId3v2(*flacFile->ID3v2Tag());
builder.setXiph(*flacFile->xiphComment()); builder.setXiph(*flacFile->xiphComment());
} else if (auto *opusFile = dynamic_cast<TagLib::Ogg::Opus::File *>(file)) { } else if (auto *opusFile = dynamic_cast<TagLib::Ogg::Opus::File *>(file)) {
builder.setMimeType("audio/opus"); builder.setMimeType("audio/opus");
builder.setXiph(*opusFile->tag()); builder.setXiph(*opusFile->tag());
} else if (auto *vorbisFile = dynamic_cast<TagLib::Ogg::Vorbis::File *>(file)) { } else if (auto *vorbisFile =
builder.setMimeType("audio/vorbis"); dynamic_cast<TagLib::Ogg::Vorbis::File *>(file)) {
builder.setXiph(*vorbisFile->tag()); builder.setMimeType("audio/vorbis");
} else if (auto *wavFile = dynamic_cast<TagLib::RIFF::WAV::File *>(file)) { builder.setXiph(*vorbisFile->tag());
builder.setMimeType("audio/wav"); } else if (auto *wavFile = dynamic_cast<TagLib::RIFF::WAV::File *>(file)) {
builder.setId3v2(*wavFile->ID3v2Tag()); builder.setMimeType("audio/wav");
} else { builder.setId3v2(*wavFile->ID3v2Tag());
// While taglib supports other formats, ExoPlayer does not. Ignore them. } else {
return nullptr; // While taglib supports other formats, ExoPlayer does not. Ignore them.
} return nullptr;
}
builder.setProperties(file->audioProperties()); builder.setProperties(file->audioProperties());
builder.setCover(file->tag()->complexProperties("PICTURE")); builder.setCover(file->tag()->complexProperties("PICTURE"));
return builder.build(); return builder.build();
} }

View file

@ -30,9 +30,9 @@ import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverter import androidx.room.TypeConverter
import androidx.room.TypeConverters import androidx.room.TypeConverters
import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.pipeline.RawSong
import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Date
import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.tag.parse.ParsedTags

View file

@ -1,17 +1,33 @@
/*
* Copyright (c) 2024 Auxio Project
* AndroidInputStream.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.metadata package org.oxycblt.musikr.metadata
import android.content.Context import android.content.Context
import java.io.FileInputStream import java.io.FileInputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
class AndroidInputStream( class AndroidInputStream(context: Context, fileRef: FileRef) : NativeInputStream {
context: Context,
fileRef: FileRef
) : NativeInputStream {
private val fileName = fileRef.fileName private val fileName = fileRef.fileName
private val fd = requireNotNull(context.contentResolver.openFileDescriptor(fileRef.uri, "r")) { private val fd =
"Failed to open file descriptor for ${fileRef.fileName}" requireNotNull(context.contentResolver.openFileDescriptor(fileRef.uri, "r")) {
} "Failed to open file descriptor for ${fileRef.fileName}"
}
private val fis = FileInputStream(fd.fileDescriptor) private val fis = FileInputStream(fd.fileDescriptor)
private val channel = fis.channel private val channel = fis.channel
@ -52,4 +68,4 @@ class AndroidInputStream(
fis.close() fis.close()
fd.close() fd.close()
} }
} }

View file

@ -1,10 +1,27 @@
/*
* Copyright (c) 2024 Auxio Project
* NativeInputStream.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.metadata package org.oxycblt.musikr.metadata
/** /**
* Java interface for the read-only methods in TagLib's IOStream API. * Java interface for the read-only methods in TagLib's IOStream API.
* *
* The vast majority of IO shim between Taglib/KTaglib should occur here * The vast majority of IO shim between Taglib/KTaglib should occur here to minimize JNI calls.
* to minimize JNI calls.
*/ */
interface NativeInputStream { interface NativeInputStream {
fun name(): String fun name(): String
@ -24,4 +41,4 @@ interface NativeInputStream {
fun tell(): Long fun tell(): Long
fun length(): Long fun length(): Long
} }

View file

@ -1,3 +1,21 @@
/*
* Copyright (c) 2024 Auxio Project
* TagLibJNI.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.metadata package org.oxycblt.musikr.metadata
import android.content.Context import android.content.Context
@ -11,8 +29,7 @@ object TagLibJNI {
/** /**
* Open a file and extract a tag. * Open a file and extract a tag.
* *
* Note: This method is blocking and should be handled as such if * Note: This method is blocking and should be handled as such if calling from a coroutine.
* calling from a coroutine.
*/ */
fun open(context: Context, ref: FileRef): Metadata? { fun open(context: Context, ref: FileRef): Metadata? {
val inputStream = AndroidInputStream(context, ref) val inputStream = AndroidInputStream(context, ref)
@ -24,10 +41,7 @@ object TagLibJNI {
private external fun openNative(ioStream: AndroidInputStream): Metadata? private external fun openNative(ioStream: AndroidInputStream): Metadata?
} }
data class FileRef( data class FileRef(val fileName: String, val uri: Uri)
val fileName: String,
val uri: Uri
)
data class Metadata( data class Metadata(
val id3v2: Map<String, List<String>>, val id3v2: Map<String, List<String>>,

View file

@ -28,12 +28,12 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.Storage import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.CacheResult import org.oxycblt.musikr.cache.CacheResult
import org.oxycblt.musikr.cover.Cover import org.oxycblt.musikr.cover.Cover
import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.metadata.MetadataExtractor import org.oxycblt.musikr.metadata.MetadataExtractor
import org.oxycblt.musikr.metadata.Properties
import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.parse.TagParser import org.oxycblt.musikr.tag.parse.TagParser

View file

@ -77,9 +77,7 @@ interface M3U {
} }
} }
private class M3UImpl( private class M3UImpl(private val volumeManager: VolumeManager) : M3U {
private val volumeManager: VolumeManager
) : M3U {
override fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? { override fun read(stream: InputStream, workingDirectory: Path): ImportedPlaylist? {
val volumes = volumeManager.getVolumes() val volumes = volumeManager.getVolumes()
val reader = BufferedReader(InputStreamReader(stream)) val reader = BufferedReader(InputStreamReader(stream))

View file

@ -18,8 +18,8 @@
package org.oxycblt.musikr.tag.parse package org.oxycblt.musikr.tag.parse
import org.oxycblt.musikr.metadata.Metadata
import org.oxycblt.musikr.fs.query.DeviceFile import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.metadata.Metadata
interface TagParser { interface TagParser {
fun parse(file: DeviceFile, metadata: Metadata): ParsedTags fun parse(file: DeviceFile, metadata: Metadata): ParsedTags