musikr: improve native error handling
Not an ideal error reporting system, but for the purposes of getting 4.0.0 out as fast as possible it will do.
This commit is contained in:
parent
0785711cd6
commit
d49286981c
9 changed files with 181 additions and 64 deletions
|
@ -22,14 +22,17 @@
|
||||||
|
|
||||||
#include "JClassRef.h"
|
#include "JClassRef.h"
|
||||||
#include "JByteArrayRef.h"
|
#include "JByteArrayRef.h"
|
||||||
|
#include "JStringRef.h"
|
||||||
|
|
||||||
JInputStream::JInputStream(JNIEnv *env, jobject jInputStream) : env(env), jInputStream(
|
JInputStream::JInputStream(JNIEnv *env, jobject jInputStream) : env(env), jInputStream(
|
||||||
jInputStream) {
|
jInputStream) {
|
||||||
JClassRef jInputStreamClass = { env,
|
JClassRef jInputStreamClass = { env,
|
||||||
"org/oxycblt/musikr/metadata/NativeInputStream" };
|
"org/oxycblt/musikr/metadata/NativeInputStream" };
|
||||||
if (!env->IsInstanceOf(jInputStream, *jInputStreamClass)) {
|
if (!env->IsInstanceOf(jInputStream, *jInputStreamClass)) {
|
||||||
throw std::runtime_error("oStream is not an instance of TagLibOStream");
|
throw std::runtime_error("Object is not NativeInputStream");
|
||||||
}
|
}
|
||||||
|
jInputStreamNameMethod = jInputStreamClass.method("name",
|
||||||
|
"()Ljava/lang/String;");
|
||||||
jInputStreamReadBlockMethod = jInputStreamClass.method("readBlock",
|
jInputStreamReadBlockMethod = jInputStreamClass.method("readBlock",
|
||||||
"(J)[B");
|
"(J)[B");
|
||||||
jInputStreamIsOpenMethod = jInputStreamClass.method("isOpen", "()Z");
|
jInputStreamIsOpenMethod = jInputStreamClass.method("isOpen", "()Z");
|
||||||
|
@ -50,7 +53,9 @@ JInputStream::~JInputStream() {
|
||||||
|
|
||||||
TagLib::FileName JInputStream::name() const {
|
TagLib::FileName JInputStream::name() const {
|
||||||
// Not actually used except in FileRef, can safely ignore.
|
// Not actually used except in FileRef, can safely ignore.
|
||||||
return "";
|
JStringRef jName { env, reinterpret_cast<jstring>(env->CallObjectMethod(
|
||||||
|
jInputStream, jInputStreamNameMethod)) };
|
||||||
|
return jName.copy().toCString();
|
||||||
}
|
}
|
||||||
|
|
||||||
TagLib::ByteVector JInputStream::readBlock(size_t length) {
|
TagLib::ByteVector JInputStream::readBlock(size_t length) {
|
||||||
|
|
|
@ -115,6 +115,7 @@ public:
|
||||||
private:
|
private:
|
||||||
JNIEnv *env;
|
JNIEnv *env;
|
||||||
jobject jInputStream;
|
jobject jInputStream;
|
||||||
|
jmethodID jInputStreamNameMethod;
|
||||||
jmethodID jInputStreamReadBlockMethod;
|
jmethodID jInputStreamReadBlockMethod;
|
||||||
jmethodID jInputStreamIsOpenMethod;
|
jmethodID jInputStreamIsOpenMethod;
|
||||||
jmethodID jInputStreamSeekFromBeginningMethod;
|
jmethodID jInputStreamSeekFromBeginningMethod;
|
||||||
|
|
|
@ -19,6 +19,9 @@
|
||||||
#include "JStringRef.h"
|
#include "JStringRef.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
|
JStringRef::JStringRef(JNIEnv *env, jstring jString) : env(env), string(jString) {
|
||||||
|
}
|
||||||
|
|
||||||
JStringRef::JStringRef(JNIEnv *env, const TagLib::String string) {
|
JStringRef::JStringRef(JNIEnv *env, const TagLib::String string) {
|
||||||
this->env = env;
|
this->env = env;
|
||||||
this->string = env->NewStringUTF(string.toCString(true));
|
this->string = env->NewStringUTF(string.toCString(true));
|
||||||
|
@ -28,6 +31,13 @@ JStringRef::~JStringRef() {
|
||||||
env->DeleteLocalRef(string);
|
env->DeleteLocalRef(string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TagLib::String JStringRef::copy() {
|
||||||
|
auto chars = env->GetStringUTFChars(string, nullptr);
|
||||||
|
TagLib::String result = chars;
|
||||||
|
env->ReleaseStringUTFChars(string, chars);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
jstring& JStringRef::operator*() {
|
jstring& JStringRef::operator*() {
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,8 @@
|
||||||
|
|
||||||
class JStringRef {
|
class JStringRef {
|
||||||
public:
|
public:
|
||||||
|
JStringRef(JNIEnv *env, jstring jString);
|
||||||
|
|
||||||
JStringRef(JNIEnv *env, TagLib::String string);
|
JStringRef(JNIEnv *env, TagLib::String string);
|
||||||
|
|
||||||
~JStringRef();
|
~JStringRef();
|
||||||
|
@ -32,6 +34,8 @@ public:
|
||||||
|
|
||||||
JStringRef& operator=(const JStringRef&) = delete;
|
JStringRef& operator=(const JStringRef&) = delete;
|
||||||
|
|
||||||
|
TagLib::String copy();
|
||||||
|
|
||||||
jstring& operator*();
|
jstring& operator*();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -30,82 +30,174 @@
|
||||||
#include "taglib/vorbisfile.h"
|
#include "taglib/vorbisfile.h"
|
||||||
#include "taglib/wavfile.h"
|
#include "taglib/wavfile.h"
|
||||||
|
|
||||||
|
std::unique_ptr<TagLib::FileRef> openFile(JNIEnv *env, jobject inputStream) {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseMpeg(const char *name, TagLib::File *file,
|
||||||
|
JMetadataBuilder &jBuilder) {
|
||||||
|
auto *mpegFile = dynamic_cast<TagLib::MPEG::File*>(file);
|
||||||
|
if (mpegFile == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto id3v1Tag = mpegFile->ID3v1Tag();
|
||||||
|
if (id3v1Tag != nullptr) {
|
||||||
|
try {
|
||||||
|
jBuilder.setId3v1(*id3v1Tag);
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
LOGE("Unable to parse ID3v1 tag in %s: %s", name, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto id3v2Tag = mpegFile->ID3v2Tag();
|
||||||
|
if (id3v2Tag != nullptr) {
|
||||||
|
try {
|
||||||
|
jBuilder.setId3v2(*id3v2Tag);
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseMp4(const char *name, TagLib::File *file,
|
||||||
|
JMetadataBuilder &jBuilder) {
|
||||||
|
auto *mp4File = dynamic_cast<TagLib::MP4::File*>(file);
|
||||||
|
if (mp4File == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto tag = mp4File->tag();
|
||||||
|
if (tag != nullptr) {
|
||||||
|
try {
|
||||||
|
jBuilder.setMp4(*tag);
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
LOGE("Unable to parse MP4 tag in %s: %s", name, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseFlac(const char *name, TagLib::File *file,
|
||||||
|
JMetadataBuilder &jBuilder) {
|
||||||
|
auto *flacFile = dynamic_cast<TagLib::FLAC::File*>(file);
|
||||||
|
if (flacFile == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto id3v1Tag = flacFile->ID3v1Tag();
|
||||||
|
if (id3v1Tag != nullptr) {
|
||||||
|
try {
|
||||||
|
jBuilder.setId3v1(*id3v1Tag);
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
LOGE("Unable to parse ID3v1 tag in %s: %s", name, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto id3v2Tag = flacFile->ID3v2Tag();
|
||||||
|
if (id3v2Tag != nullptr) {
|
||||||
|
try {
|
||||||
|
jBuilder.setId3v2(*id3v2Tag);
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto xiphComment = flacFile->xiphComment();
|
||||||
|
if (xiphComment != nullptr) {
|
||||||
|
try {
|
||||||
|
jBuilder.setXiph(*xiphComment);
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
LOGE("Unable to parse Xiph comment in %s: %s", name, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto pics = flacFile->pictureList();
|
||||||
|
jBuilder.setFlacPictures(pics);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseOpus(const char *name, TagLib::File *file,
|
||||||
|
JMetadataBuilder &jBuilder) {
|
||||||
|
auto *opusFile = dynamic_cast<TagLib::Ogg::Opus::File*>(file);
|
||||||
|
if (opusFile == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto tag = opusFile->tag();
|
||||||
|
if (tag != nullptr) {
|
||||||
|
try {
|
||||||
|
jBuilder.setXiph(*tag);
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
LOGE("Unable to parse Xiph comment in %s: %s", name, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseVorbis(const char *name, TagLib::File *file,
|
||||||
|
JMetadataBuilder &jBuilder) {
|
||||||
|
auto *vorbisFile = dynamic_cast<TagLib::Ogg::Vorbis::File*>(file);
|
||||||
|
if (vorbisFile == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto tag = vorbisFile->tag();
|
||||||
|
if (tag != nullptr) {
|
||||||
|
try {
|
||||||
|
jBuilder.setXiph(*tag);
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
LOGE("Unable to parse Xiph comment %s: %s", name, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseWav(const char *name, TagLib::File *file,
|
||||||
|
JMetadataBuilder &jBuilder) {
|
||||||
|
auto *wavFile = dynamic_cast<TagLib::RIFF::WAV::File*>(file);
|
||||||
|
if (wavFile == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto tag = wavFile->ID3v2Tag();
|
||||||
|
if (tag != nullptr) {
|
||||||
|
try {
|
||||||
|
jBuilder.setId3v2(*tag);
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
LOGE("Unable to parse ID3v2 tag in %s: %s", name, e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT jobject JNICALL
|
extern "C" JNIEXPORT jobject JNICALL
|
||||||
Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env,
|
Java_org_oxycblt_musikr_metadata_TagLibJNI_openNative(JNIEnv *env,
|
||||||
jobject /* this */,
|
jobject /* this */,
|
||||||
jobject inputStream) {
|
jobject inputStream) {
|
||||||
|
const char *name = nullptr;
|
||||||
try {
|
try {
|
||||||
JInputStream jStream {env, inputStream};
|
JInputStream jStream {env, inputStream};
|
||||||
|
name = jStream.name();
|
||||||
TagLib::FileRef fileRef {&jStream};
|
TagLib::FileRef fileRef {&jStream};
|
||||||
if (fileRef.isNull()) {
|
if (fileRef.isNull()) {
|
||||||
LOGE("Error opening file");
|
throw std::runtime_error("Invalid file");
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
TagLib::File *file = fileRef.file();
|
TagLib::File *file = fileRef.file();
|
||||||
JMetadataBuilder jBuilder {env};
|
JMetadataBuilder jBuilder {env};
|
||||||
|
|
||||||
if (auto *mpegFile = dynamic_cast<TagLib::MPEG::File *>(file)) {
|
|
||||||
jBuilder.setMimeType("audio/mpeg");
|
|
||||||
auto id3v1Tag = mpegFile->ID3v1Tag();
|
|
||||||
if (id3v1Tag != nullptr) {
|
|
||||||
jBuilder.setId3v1(*id3v1Tag);
|
|
||||||
}
|
|
||||||
auto id3v2Tag = mpegFile->ID3v2Tag();
|
|
||||||
if (id3v2Tag != nullptr) {
|
|
||||||
jBuilder.setId3v2(*id3v2Tag);
|
|
||||||
}
|
|
||||||
} else if (auto *mp4File = dynamic_cast<TagLib::MP4::File *>(file)) {
|
|
||||||
jBuilder.setMimeType("audio/mp4");
|
|
||||||
auto tag = mp4File->tag();
|
|
||||||
if (tag != nullptr) {
|
|
||||||
jBuilder.setMp4(*tag);
|
|
||||||
}
|
|
||||||
} else if (auto *flacFile = dynamic_cast<TagLib::FLAC::File *>(file)) {
|
|
||||||
jBuilder.setMimeType("audio/flac");
|
|
||||||
auto id3v1Tag = flacFile->ID3v1Tag();
|
|
||||||
if (id3v1Tag != nullptr) {
|
|
||||||
jBuilder.setId3v1(*id3v1Tag);
|
|
||||||
}
|
|
||||||
auto id3v2Tag = flacFile->ID3v2Tag();
|
|
||||||
if (id3v2Tag != nullptr) {
|
|
||||||
jBuilder.setId3v2(*id3v2Tag);
|
|
||||||
}
|
|
||||||
auto xiphComment = flacFile->xiphComment();
|
|
||||||
if (xiphComment != nullptr) {
|
|
||||||
jBuilder.setXiph(*xiphComment);
|
|
||||||
}
|
|
||||||
auto pics = flacFile->pictureList();
|
|
||||||
jBuilder.setFlacPictures(pics);
|
|
||||||
} else if (auto *opusFile = dynamic_cast<TagLib::Ogg::Opus::File *>(file)) {
|
|
||||||
jBuilder.setMimeType("audio/opus");
|
|
||||||
auto tag = opusFile->tag();
|
|
||||||
if (tag != nullptr) {
|
|
||||||
jBuilder.setXiph(*tag);
|
|
||||||
}
|
|
||||||
} else if (auto *vorbisFile =
|
|
||||||
dynamic_cast<TagLib::Ogg::Vorbis::File *>(file)) {
|
|
||||||
jBuilder.setMimeType("audio/vorbis");
|
|
||||||
auto tag = vorbisFile->tag();
|
|
||||||
if (tag != nullptr) {
|
|
||||||
jBuilder.setXiph(*tag);
|
|
||||||
}
|
|
||||||
} else if (auto *wavFile = dynamic_cast<TagLib::RIFF::WAV::File *>(file)) {
|
|
||||||
jBuilder.setMimeType("audio/wav");
|
|
||||||
auto tag = wavFile->ID3v2Tag();
|
|
||||||
if (tag != nullptr) {
|
|
||||||
jBuilder.setId3v2(*tag);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// While taglib supports other formats, ExoPlayer does not. Ignore them.
|
|
||||||
LOGD("Unsupported file format");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
jBuilder.setProperties(file->audioProperties());
|
jBuilder.setProperties(file->audioProperties());
|
||||||
return jBuilder.build();
|
|
||||||
} catch (std::runtime_error e) {
|
// TODO: Make some type of composable logger so I don't
|
||||||
LOGD("Error opening file: %s", e.what());
|
// have to shoehorn this into the native code.
|
||||||
|
if (parseMpeg(name, file, jBuilder)) {
|
||||||
|
jBuilder.setMimeType("audio/mpeg");
|
||||||
|
} else if (parseMp4(name, file, jBuilder)) {
|
||||||
|
jBuilder.setMimeType("audio/mp4");
|
||||||
|
} else if (parseFlac(name, file, jBuilder)) {
|
||||||
|
jBuilder.setMimeType("audio/flac");
|
||||||
|
} else if (parseOpus(name, file, jBuilder)) {
|
||||||
|
jBuilder.setMimeType("audio/opus");
|
||||||
|
} else if (parseVorbis(name, file, jBuilder)) {
|
||||||
|
jBuilder.setMimeType("audio/vorbis");
|
||||||
|
} else if (parseWav(name, file, jBuilder)) {
|
||||||
|
jBuilder.setMimeType("audio/wav");
|
||||||
|
} else {
|
||||||
|
LOGE("File format in %s is not supported", name);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return jBuilder.build();
|
||||||
|
} catch (std::exception &e) {
|
||||||
|
LOGE("Unable to parse metadata in %s: %s", name != nullptr ? name : "unknown file", e.what());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,10 @@ import android.os.ParcelFileDescriptor
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.oxycblt.musikr.fs.DeviceFile
|
||||||
|
|
||||||
internal interface MetadataExtractor {
|
internal interface MetadataExtractor {
|
||||||
suspend fun extract(fd: ParcelFileDescriptor): Metadata?
|
suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor): Metadata?
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun new(): MetadataExtractor = MetadataExtractorImpl
|
fun new(): MetadataExtractor = MetadataExtractorImpl
|
||||||
|
@ -32,9 +33,9 @@ internal interface MetadataExtractor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private object MetadataExtractorImpl : MetadataExtractor {
|
private object MetadataExtractorImpl : MetadataExtractor {
|
||||||
override suspend fun extract(fd: ParcelFileDescriptor) =
|
override suspend fun extract(deviceFile: DeviceFile, fd: ParcelFileDescriptor) =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val fis = FileInputStream(fd.fileDescriptor)
|
val fis = FileInputStream(fd.fileDescriptor)
|
||||||
TagLibJNI.open(fis).also { fis.close() }
|
TagLibJNI.open(deviceFile, fis).also { fis.close() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,13 @@ package org.oxycblt.musikr.metadata
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import org.oxycblt.musikr.fs.DeviceFile
|
||||||
|
|
||||||
internal class NativeInputStream(fis: FileInputStream) {
|
internal class NativeInputStream(private val deviceFile: DeviceFile, fis: FileInputStream) {
|
||||||
private val channel = fis.channel
|
private val channel = fis.channel
|
||||||
|
|
||||||
|
fun name() = requireNotNull(deviceFile.path.name)
|
||||||
|
|
||||||
fun readBlock(length: Long): ByteArray? {
|
fun readBlock(length: Long): ByteArray? {
|
||||||
try {
|
try {
|
||||||
val buffer = ByteBuffer.allocate(length.toInt())
|
val buffer = ByteBuffer.allocate(length.toInt())
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
package org.oxycblt.musikr.metadata
|
package org.oxycblt.musikr.metadata
|
||||||
|
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
import org.oxycblt.musikr.fs.DeviceFile
|
||||||
|
|
||||||
internal object TagLibJNI {
|
internal object TagLibJNI {
|
||||||
init {
|
init {
|
||||||
|
@ -30,12 +31,12 @@ internal object TagLibJNI {
|
||||||
*
|
*
|
||||||
* Note: This method is blocking and should be handled as such if calling from a coroutine.
|
* Note: This method is blocking and should be handled as such if calling from a coroutine.
|
||||||
*/
|
*/
|
||||||
fun open(fis: FileInputStream): Metadata? {
|
fun open(deviceFile: DeviceFile, fis: FileInputStream): Metadata? {
|
||||||
val inputStream = NativeInputStream(fis)
|
val inputStream = NativeInputStream(deviceFile, fis)
|
||||||
val tag = openNative(inputStream)
|
val tag = openNative(inputStream)
|
||||||
inputStream.close()
|
inputStream.close()
|
||||||
return tag
|
return tag
|
||||||
}
|
}
|
||||||
|
|
||||||
private external fun openNative(ioStream: NativeInputStream): Metadata?
|
private external fun openNative(inputStream: NativeInputStream): Metadata?
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,7 @@ private class ExtractStepImpl(
|
||||||
fds.mapNotNull { fileWith ->
|
fds.mapNotNull { fileWith ->
|
||||||
wrap(fileWith.file) { _ ->
|
wrap(fileWith.file) { _ ->
|
||||||
metadataExtractor
|
metadataExtractor
|
||||||
.extract(fileWith.with)
|
.extract(fileWith.file, fileWith.with)
|
||||||
?.let { FileWith(fileWith.file, it) }
|
?.let { FileWith(fileWith.file, it) }
|
||||||
.also { withContext(Dispatchers.IO) { fileWith.with.close() } }
|
.also { withContext(Dispatchers.IO) { fileWith.with.close() } }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue